google-analytics-cli 0.3.0__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/PKG-INFO +10 -1
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/README.md +9 -0
- google_analytics_cli-0.4.0/SPEC.md +440 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/pyproject.toml +1 -1
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/commands/agent_cmd.py +7 -1
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/commands/reports.py +107 -2
- google_analytics_cli-0.4.0/tasks/plan.md +283 -0
- google_analytics_cli-0.4.0/tasks/todo.md +56 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_reports.py +323 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_reports_batch.py +60 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_reports_pivot.py +51 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/uv.lock +1 -1
- google_analytics_cli-0.3.0/SPEC.md +0 -462
- google_analytics_cli-0.3.0/tasks/plan.md +0 -379
- google_analytics_cli-0.3.0/tasks/todo.md +0 -83
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/.api-snapshots/analyticsadmin_v1alpha.json +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/.api-snapshots/analyticsadmin_v1beta.json +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/.api-snapshots/analyticsdata_v1alpha.json +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/.api-snapshots/analyticsdata_v1beta.json +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/.github/workflows/api-watch.yml +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/.github/workflows/ci.yml +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/.github/workflows/release.yml +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/.gitignore +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/.python-version +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/install.sh +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/scripts/check_api_changes.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/__init__.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/api/__init__.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/api/client.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/auth/__init__.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/auth/credentials.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/auth/oauth.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/auth/service_account.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/commands/__init__.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/commands/access_bindings.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/commands/access_reports.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/commands/account_summaries.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/commands/accounts.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/commands/annotations.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/commands/audiences.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/commands/auth_cmd.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/commands/bigquery_links.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/commands/calculated_metrics.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/commands/channel_groups.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/commands/completions_cmd.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/commands/config_cmd.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/commands/custom_dimensions.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/commands/custom_metrics.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/commands/data_retention.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/commands/data_streams.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/commands/enhanced_measurement.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/commands/event_create_rules.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/commands/event_edit_rules.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/commands/firebase_links.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/commands/google_ads_links.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/commands/key_events.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/commands/mp_secrets.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/commands/properties.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/commands/property_settings.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/commands/upgrade_cmd.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/commands/user_provided_data.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/config/__init__.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/config/chat_session.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/config/constants.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/config/store.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/main.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/utils/__init__.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/utils/describe.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/utils/dry_run.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/utils/errors.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/utils/filters.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/utils/output.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/src/ga_cli/utils/pagination.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/__init__.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/conftest.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_access_bindings.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_access_reports.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_account_summaries.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_accounts.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_agent_cmd.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_annotations.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_api_client.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_api_diff.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_audiences.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_auth_cmd.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_bigquery_links.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_calculated_metrics.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_channel_groups.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_chat_session.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_completions_cmd.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_config_cmd.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_credentials.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_custom_dimensions.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_custom_metrics.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_data_retention.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_data_streams.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_describe.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_dry_run.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_enhanced_measurement.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_errors.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_event_create_rules.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_event_edit_rules.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_filters.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_firebase_links.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_google_ads_links.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_install_script.sh +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_key_events.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_mp_secrets.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_output.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_pagination.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_properties.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_property_settings.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_quiet_nocolor.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_reports_chat.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_reports_funnel.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_service_account.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_upgrade_cmd.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_user_provided_data.py +0 -0
- {google_analytics_cli-0.3.0 → google_analytics_cli-0.4.0}/tests/test_version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: google-analytics-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Command-line interface for Google Analytics 4
|
|
5
5
|
Project-URL: Homepage, https://github.com/daidalytics/google-analytics-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/daidalytics/google-analytics-cli
|
|
@@ -251,6 +251,15 @@ ga accounts list --output table # Table output (default)
|
|
|
251
251
|
ga accounts list --output compact # Minimal ID + name output
|
|
252
252
|
```
|
|
253
253
|
|
|
254
|
+
> **Breaking change (post-0.3.0):** `ga reports run` and `ga reports build` with
|
|
255
|
+
> `-o json` return a `{"rows": [...], "metadata": {...}}` envelope instead of a
|
|
256
|
+
> bare rows array. `metadata` is the API's
|
|
257
|
+
> [`ResponseMetaData`](https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/ResponseMetaData)
|
|
258
|
+
> — sampling, thresholding, and data-truncation information that was previously
|
|
259
|
+
> discarded. In table output the same information renders as a **Data Notes**
|
|
260
|
+
> section under the results whenever present; unaffected reports look exactly as
|
|
261
|
+
> before.
|
|
262
|
+
|
|
254
263
|
## Chat
|
|
255
264
|
|
|
256
265
|
Ask questions about a property in plain language:
|
|
@@ -219,6 +219,15 @@ ga accounts list --output table # Table output (default)
|
|
|
219
219
|
ga accounts list --output compact # Minimal ID + name output
|
|
220
220
|
```
|
|
221
221
|
|
|
222
|
+
> **Breaking change (post-0.3.0):** `ga reports run` and `ga reports build` with
|
|
223
|
+
> `-o json` return a `{"rows": [...], "metadata": {...}}` envelope instead of a
|
|
224
|
+
> bare rows array. `metadata` is the API's
|
|
225
|
+
> [`ResponseMetaData`](https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/ResponseMetaData)
|
|
226
|
+
> — sampling, thresholding, and data-truncation information that was previously
|
|
227
|
+
> discarded. In table output the same information renders as a **Data Notes**
|
|
228
|
+
> section under the results whenever present; unaffected reports look exactly as
|
|
229
|
+
> before.
|
|
230
|
+
|
|
222
231
|
## Chat
|
|
223
232
|
|
|
224
233
|
Ask questions about a property in plain language:
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
# Spec: Surface `ResponseMetaData` in report commands
|
|
2
|
+
|
|
3
|
+
Status: **Implemented — pending release** (branch `feat/response-metadata`)
|
|
4
|
+
Target version: 0.4.0 (tentative)
|
|
5
|
+
API: `analyticsdata` v1beta / v1alpha, revision `20260909`
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Objective
|
|
10
|
+
|
|
11
|
+
`reports run`, `reports build`, `reports batch`, and `reports pivot` all call GA4 Data API
|
|
12
|
+
methods whose response carries a `ResponseMetaData` (aka `metadata`) object — sampling info,
|
|
13
|
+
thresholding flags, schema restrictions, empty-report reasons, and (newly, as of the 2026-09-09
|
|
14
|
+
API revision) `dataTruncationReasons`. Today this object is **silently discarded**:
|
|
15
|
+
|
|
16
|
+
- `run` and `build` output only the transformed `rows` array in every format, including
|
|
17
|
+
`--output json` (`reports.py:334`, `:1517`) — there is currently no way to see this metadata
|
|
18
|
+
at all through those two commands.
|
|
19
|
+
- `batch` and `pivot` already pass the raw API response through for `--output json`, but their
|
|
20
|
+
table-mode rendering shows nothing for it.
|
|
21
|
+
|
|
22
|
+
**Users:**
|
|
23
|
+
1. **Humans at a terminal** running `ga reports run` — should see a plain-language warning when
|
|
24
|
+
their numbers are sampled, thresholded, restricted, or truncated, without having to guess.
|
|
25
|
+
2. **AI agents / scripts** consuming `-o json` — should be able to detect these conditions
|
|
26
|
+
programmatically instead of silently treating partial/sampled data as complete.
|
|
27
|
+
|
|
28
|
+
**Trigger:** the 2026-09-09 GA4 Discovery revision added `DataTruncationDateRange` and
|
|
29
|
+
`DataTruncationReason` schemas and a `dataTruncationReasons` array on `ResponseMetaData`
|
|
30
|
+
(`analyticsdata` v1beta/v1alpha only — `analyticsadmin` had a revision bump with no schema
|
|
31
|
+
diff). `.api-snapshots/analyticsdata_v1beta.json` and `_v1alpha.json` were already refreshed to
|
|
32
|
+
this revision in commit `5680e22`; **no `--update` run is needed for this work.**
|
|
33
|
+
|
|
34
|
+
**Success looks like:** running a report against a sampled or thresholded property shows a
|
|
35
|
+
metadata section under the results (table mode) or a `metadata` key in the JSON response,
|
|
36
|
+
without requiring any new flag.
|
|
37
|
+
|
|
38
|
+
### Assumptions
|
|
39
|
+
|
|
40
|
+
1. `reports realtime`'s response (`RunRealtimeReportResponse`) has **no `metadata` field** —
|
|
41
|
+
confirmed against the v1beta snapshot. Real-time is out of scope for this feature entirely.
|
|
42
|
+
2. `reports funnel` (v1alpha `runFunnelReport`) is **not investigated** here — its response shape
|
|
43
|
+
(`funnelTable`) was not checked for a `metadata` field. Treated as out of scope unless a
|
|
44
|
+
follow-up confirms it needs the same treatment.
|
|
45
|
+
3. `reports check-compatibility` and `reports metadata` (the `getMetadata` endpoint, unrelated to
|
|
46
|
+
`ResponseMetaData` despite the name collision) are out of scope — neither returns a report.
|
|
47
|
+
4. `reports chat` is unrelated (separate `ChatResponse` shape, already raw-passthrough for JSON)
|
|
48
|
+
and untouched by this change.
|
|
49
|
+
5. Changing `run`/`build`'s JSON shape from a bare array to `{rows, metadata}` is an accepted
|
|
50
|
+
**breaking change** for any existing script parsing `ga reports run -o json` as a flat array.
|
|
51
|
+
This is a pre-1.0 CLI (`0.3.0`), so the project's version-bump convention (not semver-strict
|
|
52
|
+
back-compat) applies; the change ships with a version bump and a release-notes callout.
|
|
53
|
+
6. Metadata display in table/compact mode is **automatic** whenever the API returns a non-empty
|
|
54
|
+
`metadata` object — no new flag, consistent with how `_display_aggregations` already works
|
|
55
|
+
whenever `--metric-aggregation` is passed. (Unlike `propertyQuota`, metadata isn't gated by a
|
|
56
|
+
request-side opt-in flag on the GA4 API — the API returns it unconditionally when applicable.)
|
|
57
|
+
7. `batch`'s per-sub-report metadata (each item in `RunReportResponse.metadata`) is displayed
|
|
58
|
+
per sub-report, directly under that sub-report's `--- Report N ---` block — consistent with
|
|
59
|
+
how `batch` already repeats `_display_aggregations`-style output per sub-report today (it
|
|
60
|
+
doesn't currently, but `row_count` is shown per sub-report at `reports.py:808`).
|
|
61
|
+
→ Correct any of these now or I'll proceed with them.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## API Surface (from `.api-snapshots/analyticsdata_v1beta.json`)
|
|
66
|
+
|
|
67
|
+
```jsonc
|
|
68
|
+
// ResponseMetaData (RunReportResponse.metadata / RunPivotReportResponse.metadata)
|
|
69
|
+
{
|
|
70
|
+
"samplingMetadatas": [ SamplingMetadata ], // one per date range, only if sampled
|
|
71
|
+
"schemaRestrictionResponse": SchemaRestrictionResponse,
|
|
72
|
+
"subjectToThresholding": boolean,
|
|
73
|
+
"dataTruncationReasons": [ DataTruncationReason ], // NEW in 2026-09-09 revision
|
|
74
|
+
"emptyReason": string // enum-like; only set when report is empty
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// SamplingMetadata (per date range)
|
|
78
|
+
{ "samplingSpaceSize": string /* int64 */, "samplesReadCount": string /* int64 */ }
|
|
79
|
+
|
|
80
|
+
// SchemaRestrictionResponse
|
|
81
|
+
{ "activeMetricRestrictions": [
|
|
82
|
+
{ "metricName": string, "restrictedMetricTypes": ["COST_DATA" | "REVENUE_DATA" | ...] }
|
|
83
|
+
] }
|
|
84
|
+
|
|
85
|
+
// DataTruncationReason (the field driving this spec)
|
|
86
|
+
{
|
|
87
|
+
"dataTruncationType": "DATA_TRUNCATION_TYPE_RULES_BASED_MODELS" | "...DATA_DRIVEN_ATTRIBUTION"
|
|
88
|
+
| "...DV360" | "...CM360" | "...ITEM_SCOPED_ECOMMERCE_METRICS"
|
|
89
|
+
| "...EVENT_SCOPED_ECOMMERCE_METRICS" | "...DATE_RANGE" | "...PROPERTY"
|
|
90
|
+
| "...CONVERSIONS" | "...GOOGLE_ADS" | "...UNSPECIFIED",
|
|
91
|
+
"dataTruncationMessage": string,
|
|
92
|
+
"dataTruncationDate": string, // YYYY-MM-DD — data before this date is truncated
|
|
93
|
+
"dataTruncationDateRanges": [ { "startDate": string, "endDate": string } ]
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Notes that drive the design:
|
|
98
|
+
- `RunReportResponse.metadata` and `RunPivotReportResponse.metadata` both `$ref` the same
|
|
99
|
+
`ResponseMetaData` schema — one rendering helper covers `run`, `build`, `batch`, and `pivot`.
|
|
100
|
+
- Every field on `ResponseMetaData` is optional/absent when not applicable. The common case
|
|
101
|
+
(a small, unsampled, unrestricted, untruncated report) has an empty or near-empty `metadata`
|
|
102
|
+
object — the display helper must render **nothing** in that case, not an empty section.
|
|
103
|
+
- `dataTruncationReasons` can contain multiple entries (e.g. both a `DATE_RANGE` truncation and a
|
|
104
|
+
`GOOGLE_ADS` retention truncation on the same report) — render as a list, not a single line.
|
|
105
|
+
- `samplingMetadatas` is parallel to `dateRanges` by index, not named — if there are 2 date
|
|
106
|
+
ranges, expect at most 2 entries in the same order.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Design Decisions
|
|
111
|
+
|
|
112
|
+
### 1. One shared helper: `_display_response_metadata()`
|
|
113
|
+
|
|
114
|
+
New module-level function in `reports.py`, same shape as the existing `_display_quota()` /
|
|
115
|
+
`_display_aggregations()`:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
def _display_response_metadata(metadata: dict | None, effective_format: str) -> None:
|
|
119
|
+
"""Render sampling/thresholding/restriction/truncation warnings, if present.
|
|
120
|
+
|
|
121
|
+
Mirrors _display_quota(): a no-op when the API didn't return anything
|
|
122
|
+
interesting, so unaffected reports see no new output.
|
|
123
|
+
"""
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Called from `run_cmd`, `build_cmd` (after the existing `_display_quota` call), from `batch_cmd`
|
|
127
|
+
once per sub-report, and from `pivot_cmd`'s table-mode branch. Table/compact and JSON share the
|
|
128
|
+
same gating logic (render only if `metadata` is non-empty) but different output paths (see below).
|
|
129
|
+
|
|
130
|
+
### 2. Table/compact rendering
|
|
131
|
+
|
|
132
|
+
Printed as a `[bold]Data Notes[/bold]` section (mirroring the existing `\n[bold]Aggregations[/bold]`
|
|
133
|
+
header at `reports.py:232`), after aggregations/quota, using `warn()`/`info()`-style lines rather
|
|
134
|
+
than a table — these are prose warnings, not tabular data:
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
[dim]150 total rows[/dim]
|
|
138
|
+
|
|
139
|
+
Data Notes
|
|
140
|
+
⚠ Truncated (DATE_RANGE): Query date range may not be fully served.
|
|
141
|
+
⚠ Truncated (GOOGLE_ADS): Google Ads data is truncated due to a 36-month retention policy
|
|
142
|
+
(before 2023-09-01).
|
|
143
|
+
⚠ Subject to data thresholds — some low-volume data may be withheld.
|
|
144
|
+
ℹ Sampled: 42,103 of 1,204,500 events analyzed (3.5%).
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Rendering rules:
|
|
148
|
+
- `dataTruncationReasons`: one line per entry, `dataTruncationType` humanized (strip the
|
|
149
|
+
`DATA_TRUNCATION_TYPE_` prefix, title-case) + `dataTruncationMessage`; append
|
|
150
|
+
`(before {dataTruncationDate})` when the date is present.
|
|
151
|
+
- `subjectToThresholding`: one fixed line when `true`. Omit entirely when `false`/absent — a
|
|
152
|
+
`false` is not noteworthy.
|
|
153
|
+
- `samplingMetadatas`: one line per entry with the computed percentage
|
|
154
|
+
(`samplesReadCount / samplingSpaceSize`), guarding divide-by-zero.
|
|
155
|
+
- `schemaRestrictionResponse.activeMetricRestrictions`: one line per restricted metric, e.g.
|
|
156
|
+
`⚠ Metric 'purchaseRevenue' restricted (REVENUE_DATA) — values withheld by your role.`
|
|
157
|
+
- `emptyReason`: shown as a single `info()` line when the report has zero rows and this is set —
|
|
158
|
+
replaces the current unconditional "No results found." with the API's actual reason where
|
|
159
|
+
available (table renderer's existing fallback stays for when it's absent).
|
|
160
|
+
- **compact format:** same content, one tab-free line per item to stderr via `warn()`/`info()`
|
|
161
|
+
(keeping stdout pipeable, matching how `chat`'s session ID and quota already go to stderr in
|
|
162
|
+
compact mode).
|
|
163
|
+
|
|
164
|
+
### 3. JSON rendering — breaking shape change for `run`/`build`
|
|
165
|
+
|
|
166
|
+
`run_cmd` and `build_cmd` currently do:
|
|
167
|
+
```python
|
|
168
|
+
output(rows, effective_format, columns=columns, headers=headers)
|
|
169
|
+
```
|
|
170
|
+
which for JSON prints `rows` as a bare array. This becomes:
|
|
171
|
+
```python
|
|
172
|
+
if effective_format == "json":
|
|
173
|
+
output({"rows": rows, "metadata": result.get("metadata", {})}, effective_format)
|
|
174
|
+
else:
|
|
175
|
+
output(rows, effective_format, columns=columns, headers=headers)
|
|
176
|
+
_display_response_metadata(result.get("metadata"), effective_format)
|
|
177
|
+
```
|
|
178
|
+
Table/compact keep the existing flat-rows path (metadata rendered separately, as prose, not
|
|
179
|
+
merged into row data). Only the JSON shape changes: `{"rows": [...], "metadata": {...}}` instead
|
|
180
|
+
of a bare array.
|
|
181
|
+
|
|
182
|
+
`batch_cmd` and `pivot_cmd` already return the *entire* raw API response for `-o json`
|
|
183
|
+
(`output(result, effective_format); return` at `reports.py:525`, `:800`), which already includes
|
|
184
|
+
`metadata` — **no JSON-side change needed for those two**, only their table-mode branches gain
|
|
185
|
+
the new `_display_response_metadata()` call.
|
|
186
|
+
|
|
187
|
+
### 4. `batch` — per-sub-report metadata
|
|
188
|
+
|
|
189
|
+
Each item in `BatchRunReportsResponse.reports` is itself a `RunReportResponse` with its own
|
|
190
|
+
`metadata`. In table mode, call `_display_response_metadata(report.get("metadata"), ...)` right
|
|
191
|
+
after that sub-report's row count line (`reports.py:808-809`), inside the existing per-report
|
|
192
|
+
loop — so metadata attaches visibly to the sub-report it describes, not lumped at the end.
|
|
193
|
+
|
|
194
|
+
### 5. Why automatic, not flag-gated
|
|
195
|
+
|
|
196
|
+
`--return-property-quota` needs a flag because `propertyQuota` is only populated when the
|
|
197
|
+
*request* sets `returnPropertyQuota: true` — it's an opt-in on the wire. `ResponseMetaData`
|
|
198
|
+
fields have no such request-side toggle; the API includes them whenever applicable regardless of
|
|
199
|
+
what the CLI asks for. Gating display behind a new flag would mean users silently miss sampling/
|
|
200
|
+
truncation warnings unless they already knew to ask — the opposite of what this feature is for.
|
|
201
|
+
Precedent: `_display_aggregations()` already renders unconditionally whenever its trigger
|
|
202
|
+
condition (`metric_aggregations` truthy) is met, no separate display flag.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Commands
|
|
207
|
+
|
|
208
|
+
No new flags or command signatures. Behavior changes only:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
ga reports run -p 123 -m sessions,totalRevenue -d date # table: may show "Data Notes"
|
|
212
|
+
ga reports run -p 123 -m sessions -o json # {"rows": [...], "metadata": {...}}
|
|
213
|
+
ga reports build -p 123 # same table-mode addition
|
|
214
|
+
ga reports batch -p 123 -c batch.json # per-sub-report "Data Notes"
|
|
215
|
+
ga reports pivot -p 123 -m sessions -d date --pivot-field date # table-mode addition
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Dev commands (unchanged)
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
pytest
|
|
222
|
+
pytest tests/test_reports.py -v
|
|
223
|
+
ruff check src/ tests/
|
|
224
|
+
uv sync
|
|
225
|
+
uv build
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Project Structure
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
src/ga_cli/
|
|
234
|
+
└── commands/reports.py # MODIFY — add _display_response_metadata(); wire into
|
|
235
|
+
# run_cmd, build_cmd, batch_cmd, pivot_cmd
|
|
236
|
+
|
|
237
|
+
tests/
|
|
238
|
+
└── test_reports.py # MODIFY — new metadata fixtures + assertions (see Testing Strategy)
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
No new files, no new dependencies, no config/auth changes — this is a pure `reports.py` rendering
|
|
242
|
+
change plus a JSON-shape change on two existing commands.
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## Code Style
|
|
247
|
+
|
|
248
|
+
Follows existing `reports.py` conventions: `_`-prefixed module-level helpers, `.get()` with
|
|
249
|
+
defaults throughout (API fields are optional), `console.print` with a leading `\n[bold]...[/bold]`
|
|
250
|
+
header for table sections, `warn()`/`info()` for compact/non-table prose.
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
_TRUNCATION_TYPE_PREFIX = "DATA_TRUNCATION_TYPE_"
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _humanize_truncation_type(raw: str) -> str:
|
|
257
|
+
"""DATA_TRUNCATION_TYPE_GOOGLE_ADS -> Google Ads."""
|
|
258
|
+
stripped = raw.removeprefix(_TRUNCATION_TYPE_PREFIX)
|
|
259
|
+
return stripped.replace("_", " ").title()
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _display_response_metadata(metadata: dict | None, effective_format: str) -> None:
|
|
263
|
+
"""Render sampling/thresholding/restriction/truncation notes, if present.
|
|
264
|
+
|
|
265
|
+
A no-op when metadata is empty or absent, matching _display_quota()'s
|
|
266
|
+
behavior for reports that don't trigger any of these conditions.
|
|
267
|
+
"""
|
|
268
|
+
if not metadata:
|
|
269
|
+
return
|
|
270
|
+
|
|
271
|
+
lines: list[str] = []
|
|
272
|
+
|
|
273
|
+
for reason in metadata.get("dataTruncationReasons", []):
|
|
274
|
+
kind = _humanize_truncation_type(reason.get("dataTruncationType", ""))
|
|
275
|
+
message = reason.get("dataTruncationMessage", "")
|
|
276
|
+
date = reason.get("dataTruncationDate")
|
|
277
|
+
suffix = f" (before {date})" if date else ""
|
|
278
|
+
lines.append(f"Truncated ({kind}): {message}{suffix}")
|
|
279
|
+
|
|
280
|
+
if metadata.get("subjectToThresholding"):
|
|
281
|
+
lines.append("Subject to data thresholds — some low-volume data may be withheld.")
|
|
282
|
+
|
|
283
|
+
for sm in metadata.get("samplingMetadatas", []):
|
|
284
|
+
space = int(sm.get("samplingSpaceSize", 0) or 0)
|
|
285
|
+
read = int(sm.get("samplesReadCount", 0) or 0)
|
|
286
|
+
pct = f"{(read / space * 100):.1f}%" if space else "?"
|
|
287
|
+
lines.append(f"Sampled: {read:,} of {space:,} events analyzed ({pct}).")
|
|
288
|
+
|
|
289
|
+
for restriction in metadata.get("schemaRestrictionResponse", {}).get(
|
|
290
|
+
"activeMetricRestrictions", []
|
|
291
|
+
):
|
|
292
|
+
types = ", ".join(restriction.get("restrictedMetricTypes", []))
|
|
293
|
+
lines.append(
|
|
294
|
+
f"Metric '{restriction.get('metricName')}' restricted ({types}) — "
|
|
295
|
+
"values withheld by your role."
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
if not lines:
|
|
299
|
+
return
|
|
300
|
+
|
|
301
|
+
if effective_format == "table":
|
|
302
|
+
console.print("\n[bold]Data Notes[/bold]")
|
|
303
|
+
for line in lines:
|
|
304
|
+
console.print(f" [yellow]![/yellow] {line}")
|
|
305
|
+
elif effective_format == "compact":
|
|
306
|
+
for line in lines:
|
|
307
|
+
warn(line)
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
**Conventions this encodes:**
|
|
311
|
+
- `.get(..., [])` / `.get(..., {})` chains throughout — every `ResponseMetaData` field is
|
|
312
|
+
optional, matching the defensive style already used for `dimensionHeaders`/`metricHeaders` in
|
|
313
|
+
`_transform_report_rows`.
|
|
314
|
+
- Divide-by-zero guarded (`if space else "?"`) rather than assumed present, per `int64` fields
|
|
315
|
+
arriving as strings from the JSON API (`NumericValue`-style string-encoded integers elsewhere
|
|
316
|
+
in this schema family).
|
|
317
|
+
- No new Rich `Table` — a warnings list is prose, not tabular, unlike `_display_aggregations`.
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## Testing Strategy
|
|
322
|
+
|
|
323
|
+
`pytest` + `typer.testing.CliRunner`, class-based grouping, all API calls mocked via
|
|
324
|
+
`unittest.mock.patch`, per existing `tests/test_reports.py` conventions. **No real API calls.**
|
|
325
|
+
|
|
326
|
+
New fixtures needed in `tests/test_reports.py`: a `SAMPLE_REPORT_RESPONSE_WITH_METADATA` (or
|
|
327
|
+
per-field variants) alongside the existing `SAMPLE_REPORT_RESPONSE`.
|
|
328
|
+
|
|
329
|
+
| Class | Cases |
|
|
330
|
+
|---|---|
|
|
331
|
+
| `TestResponseMetadataDisplay` | table mode: `dataTruncationReasons` renders one line per entry with type+message+date; `subjectToThresholding: true` renders its fixed line, `false`/absent renders nothing; `samplingMetadatas` renders a correct percentage, and handles `samplingSpaceSize: "0"` without a ZeroDivisionError; `activeMetricRestrictions` renders per-metric; empty/absent `metadata` renders **no** "Data Notes" section at all (regression guard against the always-on trigger from Design Decision 5) |
|
|
332
|
+
| `TestResponseMetadataJson` | `run -o json` returns `{"rows": [...], "metadata": {...}}` (not a bare array) — update the existing `test_run_json_output` assertion for the new envelope; `metadata` key present-but-empty (`{}`) when the API returns none; `build -o json`, `batch -o json`, `pivot -o json` already raw-passthrough — assert `metadata` key survives unchanged (regression guard, not new behavior) |
|
|
333
|
+
| `TestBatchPerReportMetadata` | two sub-reports, only the second has `dataTruncationReasons` — table output shows "Data Notes" attached to the second block only, not the first |
|
|
334
|
+
| `TestResponseMetadataCompact` | `-o compact` sends truncation/thresholding/sampling lines to stderr via `warn()`, stdout stays exactly the row-only output |
|
|
335
|
+
|
|
336
|
+
Also verify (no new test needed, but confirm during implementation): `test_run_default_metrics`
|
|
337
|
+
and other existing table-mode tests that use `SAMPLE_REPORT_RESPONSE` (no `metadata` key) still
|
|
338
|
+
pass unchanged — the no-op path must not alter output for responses that predate this field.
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
## Boundaries
|
|
343
|
+
|
|
344
|
+
**Always:**
|
|
345
|
+
- Run `pytest` and `ruff check src/ tests/` before committing.
|
|
346
|
+
- Mock every API call in tests; add fixtures rather than hitting a real property.
|
|
347
|
+
- Treat every `ResponseMetaData` sub-field as optional (`.get()` with a default), since GA4 only
|
|
348
|
+
populates fields that apply to that specific report.
|
|
349
|
+
- Keep the "no metadata → no output" no-op guarantee — this must be silent for the overwhelming
|
|
350
|
+
majority of reports that trigger none of these conditions.
|
|
351
|
+
|
|
352
|
+
**Ask first:**
|
|
353
|
+
- Adding `reports realtime` or `reports funnel` to this feature (both were scoped out above;
|
|
354
|
+
realtime's response has no `metadata` field at all, funnel wasn't investigated).
|
|
355
|
+
- Any dependency addition (none should be needed — this is stdlib dict traversal + existing Rich
|
|
356
|
+
console).
|
|
357
|
+
- Reformatting `_transform_report_rows`/`_transform_pivot_rows` beyond what's needed to thread
|
|
358
|
+
`metadata` through — this spec's diff should stay additive.
|
|
359
|
+
- Bumping the version or tagging a release.
|
|
360
|
+
|
|
361
|
+
**Never:**
|
|
362
|
+
- Merge `metadata` fields into individual row dicts — it describes the whole report, not any one
|
|
363
|
+
row, and must stay a sibling key (`{"rows": ..., "metadata": ...}`), not inlined per-row.
|
|
364
|
+
- Silently swallow the JSON shape change — the release notes/CHANGELOG must call out that
|
|
365
|
+
`ga reports run -o json` and `ga reports build -o json` now return an object, not a bare array.
|
|
366
|
+
- Run `scripts/check_api_changes.py --update` as part of this work — the snapshots are already
|
|
367
|
+
current (commit `5680e22`); re-running it is out of scope and risks picking up unrelated drift.
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## Success Criteria
|
|
372
|
+
|
|
373
|
+
1. `ga reports run` against a sampled property (or a mocked response with `samplingMetadatas`)
|
|
374
|
+
shows a "Data Notes" section with a correct percentage in table mode.
|
|
375
|
+
2. `ga reports run -o json` against the same mocked response returns
|
|
376
|
+
`{"rows": [...], "metadata": {"samplingMetadatas": [...]}}`.
|
|
377
|
+
3. `ga reports run` against an unsampled, unrestricted, untruncated property shows **no** "Data
|
|
378
|
+
Notes" section and JSON output has `"metadata": {}` — output for existing users is otherwise
|
|
379
|
+
unchanged.
|
|
380
|
+
4. A mocked `dataTruncationReasons` response renders each reason as a separate, human-readable
|
|
381
|
+
line including the truncation date when present.
|
|
382
|
+
5. `ga reports batch` with a two-report config where only one sub-report has metadata attaches
|
|
383
|
+
the "Data Notes" section to the correct sub-report block.
|
|
384
|
+
6. `ga reports pivot -o json` and `ga reports batch -o json` continue to raw-passthrough
|
|
385
|
+
`metadata` unchanged (no regression from this change, since they already did).
|
|
386
|
+
7. `pytest` and `ruff check src/ tests/` pass, including the new/updated cases in the table above.
|
|
387
|
+
8. `ga reports run --help` / `ga reports build --help` require no changes (no new flags) but the
|
|
388
|
+
CHANGELOG documents the JSON shape change as breaking.
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
## Implementation Order
|
|
393
|
+
|
|
394
|
+
1. **Shared helper** — `_display_response_metadata()` + `_humanize_truncation_type()` in
|
|
395
|
+
`reports.py`, unit-tested in isolation against hand-built `metadata` dicts covering each field
|
|
396
|
+
and the empty case. *Verify:* `TestResponseMetadataDisplay`.
|
|
397
|
+
2. **Wire into `run_cmd`** — JSON envelope change + table-mode call. *Verify:*
|
|
398
|
+
`TestResponseMetadataJson` (run cases), update `test_run_json_output`.
|
|
399
|
+
3. **Wire into `build_cmd`** — same pattern, reusing the helper. *Verify:* existing `build` tests
|
|
400
|
+
still pass; add a JSON+table case.
|
|
401
|
+
4. **Wire into `batch_cmd`** — per-sub-report call inside the existing loop. *Verify:*
|
|
402
|
+
`TestBatchPerReportMetadata`.
|
|
403
|
+
5. **Wire into `pivot_cmd`** — table-mode call only (JSON already passes through). *Verify:*
|
|
404
|
+
pivot table-mode test with a mocked `metadata` key.
|
|
405
|
+
6. **Compact-mode pass + CHANGELOG entry** — `TestResponseMetadataCompact`; document the JSON
|
|
406
|
+
breaking change. *Verify:* full `pytest` + `ruff check src/ tests/`.
|
|
407
|
+
|
|
408
|
+
Steps 2–5 are independent once step 1 lands (each touches a different command) and could be done
|
|
409
|
+
in any order or in parallel; step 6 depends on all of them.
|
|
410
|
+
|
|
411
|
+
---
|
|
412
|
+
|
|
413
|
+
## Open Questions — all resolved
|
|
414
|
+
|
|
415
|
+
1. ~~**`reports funnel` scope.**~~ **Resolved during planning:** the v1alpha snapshot shows
|
|
416
|
+
`RunFunnelReportResponse` has no `metadata` field at all (`funnelVisualization`, `kind`,
|
|
417
|
+
`propertyQuota`, `funnelTable`). Funnel is definitively out of scope.
|
|
418
|
+
2. ~~**Percentage formatting.**~~ **Resolved:** one decimal place (`3.5%`), no existing
|
|
419
|
+
precedent to match.
|
|
420
|
+
3. ~~**CHANGELOG mechanics.**~~ **Resolved:** breaking-change note lives in README's
|
|
421
|
+
"Output formats" section; the GitHub release notes carry it again at tag time. No
|
|
422
|
+
`CHANGELOG.md` introduced.
|
|
423
|
+
|
|
424
|
+
## Implementation Notes (2026-09-18)
|
|
425
|
+
|
|
426
|
+
Deviations and findings recorded while implementing (`tasks/plan.md`, `tasks/todo.md`):
|
|
427
|
+
|
|
428
|
+
- **The live API populates `metadata` on every response** with `currencyCode` and `timeZone`
|
|
429
|
+
(verified against a real property) — real-world `metadata` is never an empty dict. The
|
|
430
|
+
no-op guarantee therefore rests on the helper's *rendered-lines* guard, not the empty-dict
|
|
431
|
+
check; pinned by `test_currency_and_timezone_only_render_no_data_notes`.
|
|
432
|
+
- **`emptyReason` renders as a regular Data Notes line**, not the separate `info()` line this
|
|
433
|
+
spec proposed — uniform with the other notes, and it composes naturally under the table
|
|
434
|
+
renderer's existing "No results found." fallback, which stays untouched.
|
|
435
|
+
- **The Code Style sketch above under-escapes:** interpolating API text directly into a
|
|
436
|
+
markup-styled `console.print` line would let a message containing `[bold]` be parsed as a
|
|
437
|
+
style tag. The implementation wraps API-sourced text in `rich.markup.escape()`; pinned by
|
|
438
|
+
`test_api_text_with_rich_markup_renders_literally`.
|
|
439
|
+
- Truncation-type humanization keeps words containing digits uppercase (`DV360`, `CM360`)
|
|
440
|
+
rather than title-casing them into `Dv360`.
|
|
@@ -243,7 +243,7 @@ Requires both `--property-id` and `--stream-id`. `get` retrieves current setting
|
|
|
243
243
|
|
|
244
244
|
### Reports
|
|
245
245
|
```bash
|
|
246
|
-
ga reports run [-p PROPERTY_ID] -m metrics -d dimensions --start-date DATE --end-date DATE [--limit N] [-o json] # pipe: | jq '[.[] | {date, sessions, totalUsers}]'
|
|
246
|
+
ga reports run [-p PROPERTY_ID] -m metrics -d dimensions --start-date DATE --end-date DATE [--limit N] [-o json] # pipe: | jq '[.rows[] | {date, sessions, totalUsers}]'
|
|
247
247
|
ga reports pivot [-p PROPERTY_ID] -m metrics -d dimensions --pivot-field FIELD [--start-date DATE] [-o json]
|
|
248
248
|
ga reports check-compatibility [-p PROPERTY_ID] [-m metrics] [-d dimensions] [-o json]
|
|
249
249
|
ga reports metadata [-p PROPERTY_ID] [--type metrics|dimensions] [--search TEXT] [-o json]
|
|
@@ -252,6 +252,12 @@ ga reports chat "QUESTION" [-p PROPERTY_ID] [--session-id ID] [--continue] [-i]
|
|
|
252
252
|
```
|
|
253
253
|
Dates: `today`, `yesterday`, `7daysAgo`, `30daysAgo`, `90daysAgo`, or `YYYY-MM-DD`
|
|
254
254
|
|
|
255
|
+
`reports run` JSON output is a `{rows, metadata}` envelope. `metadata` is the API's
|
|
256
|
+
`ResponseMetaData`: always `currencyCode`/`timeZone`, plus — when applicable —
|
|
257
|
+
`samplingMetadatas`, `subjectToThresholding`, and `dataTruncationReasons`. Check it
|
|
258
|
+
before treating report data as complete; `pivot`/`batch` return it in their raw
|
|
259
|
+
responses under the same key.
|
|
260
|
+
|
|
255
261
|
### Upgrade
|
|
256
262
|
```bash
|
|
257
263
|
ga upgrade [--check] [--force]
|