google-analytics-cli 0.1.0__py3-none-any.whl
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.
- ga_cli/__init__.py +8 -0
- ga_cli/api/__init__.py +0 -0
- ga_cli/api/client.py +142 -0
- ga_cli/auth/__init__.py +35 -0
- ga_cli/auth/credentials.py +126 -0
- ga_cli/auth/oauth.py +322 -0
- ga_cli/auth/service_account.py +155 -0
- ga_cli/commands/__init__.py +0 -0
- ga_cli/commands/access_bindings.py +254 -0
- ga_cli/commands/access_reports.py +201 -0
- ga_cli/commands/account_summaries.py +68 -0
- ga_cli/commands/accounts.py +297 -0
- ga_cli/commands/agent_cmd.py +776 -0
- ga_cli/commands/annotations.py +264 -0
- ga_cli/commands/audiences.py +223 -0
- ga_cli/commands/auth_cmd.py +205 -0
- ga_cli/commands/bigquery_links.py +309 -0
- ga_cli/commands/calculated_metrics.py +312 -0
- ga_cli/commands/channel_groups.py +223 -0
- ga_cli/commands/completions_cmd.py +55 -0
- ga_cli/commands/config_cmd.py +113 -0
- ga_cli/commands/custom_dimensions.py +272 -0
- ga_cli/commands/custom_metrics.py +305 -0
- ga_cli/commands/data_retention.py +153 -0
- ga_cli/commands/data_streams.py +277 -0
- ga_cli/commands/event_create_rules.py +250 -0
- ga_cli/commands/event_edit_rules.py +292 -0
- ga_cli/commands/firebase_links.py +142 -0
- ga_cli/commands/google_ads_links.py +225 -0
- ga_cli/commands/key_events.py +269 -0
- ga_cli/commands/mp_secrets.py +265 -0
- ga_cli/commands/properties.py +330 -0
- ga_cli/commands/property_settings.py +287 -0
- ga_cli/commands/reports.py +726 -0
- ga_cli/commands/upgrade_cmd.py +148 -0
- ga_cli/config/__init__.py +0 -0
- ga_cli/config/constants.py +61 -0
- ga_cli/config/store.py +115 -0
- ga_cli/main.py +110 -0
- ga_cli/utils/__init__.py +20 -0
- ga_cli/utils/describe.py +129 -0
- ga_cli/utils/dry_run.py +40 -0
- ga_cli/utils/errors.py +150 -0
- ga_cli/utils/output.py +209 -0
- ga_cli/utils/pagination.py +93 -0
- google_analytics_cli-0.1.0.dist-info/METADATA +321 -0
- google_analytics_cli-0.1.0.dist-info/RECORD +49 -0
- google_analytics_cli-0.1.0.dist-info/WHEEL +4 -0
- google_analytics_cli-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""Property management commands."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import questionary
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from ..api.client import get_admin_client, get_data_alpha_client
|
|
9
|
+
from ..config.store import get_effective_value
|
|
10
|
+
from ..utils import (
|
|
11
|
+
handle_dry_run,
|
|
12
|
+
handle_error,
|
|
13
|
+
info,
|
|
14
|
+
output,
|
|
15
|
+
require_options,
|
|
16
|
+
resolve_output_format,
|
|
17
|
+
success,
|
|
18
|
+
)
|
|
19
|
+
from ..utils.pagination import paginate_all
|
|
20
|
+
|
|
21
|
+
properties_app = typer.Typer(name="properties", help="Manage GA4 properties", no_args_is_help=True)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@properties_app.command("list")
|
|
25
|
+
def list_cmd(
|
|
26
|
+
account_id: Optional[str] = typer.Option(None, "--account-id", "-a", help="Account ID"),
|
|
27
|
+
output_format: Optional[str] = typer.Option(
|
|
28
|
+
None, "--output", "-o", help="Output format (json, table, compact)"
|
|
29
|
+
),
|
|
30
|
+
):
|
|
31
|
+
"""List GA4 properties for an account."""
|
|
32
|
+
try:
|
|
33
|
+
effective_account = get_effective_value(account_id, "default_account_id")
|
|
34
|
+
require_options({"account_id": effective_account}, ["account_id"])
|
|
35
|
+
effective_format = resolve_output_format(output_format)
|
|
36
|
+
|
|
37
|
+
admin = get_admin_client()
|
|
38
|
+
properties = paginate_all(
|
|
39
|
+
lambda **kw: (
|
|
40
|
+
admin.properties()
|
|
41
|
+
.list(filter=f"parent:accounts/{effective_account}", **kw)
|
|
42
|
+
.execute()
|
|
43
|
+
),
|
|
44
|
+
"properties",
|
|
45
|
+
pageSize=200,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
output(
|
|
49
|
+
properties,
|
|
50
|
+
effective_format,
|
|
51
|
+
columns=["name", "displayName", "timeZone", "currencyCode"],
|
|
52
|
+
headers=["Resource Name", "Display Name", "Time Zone", "Currency"],
|
|
53
|
+
)
|
|
54
|
+
except Exception as e:
|
|
55
|
+
handle_error(e)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@properties_app.command("get")
|
|
59
|
+
def get_cmd(
|
|
60
|
+
property_id: Optional[str] = typer.Option(
|
|
61
|
+
None, "--property-id", "-p", help="Property ID (numeric)"
|
|
62
|
+
),
|
|
63
|
+
output_format: Optional[str] = typer.Option(
|
|
64
|
+
None, "--output", "-o", help="Output format (json, table, compact)"
|
|
65
|
+
),
|
|
66
|
+
):
|
|
67
|
+
"""Get details for a specific property."""
|
|
68
|
+
try:
|
|
69
|
+
effective_property = get_effective_value(property_id, "default_property_id")
|
|
70
|
+
require_options({"property_id": effective_property}, ["property_id"])
|
|
71
|
+
effective_format = resolve_output_format(output_format)
|
|
72
|
+
|
|
73
|
+
admin = get_admin_client()
|
|
74
|
+
prop = admin.properties().get(name=f"properties/{effective_property}").execute()
|
|
75
|
+
output(prop, effective_format)
|
|
76
|
+
except Exception as e:
|
|
77
|
+
handle_error(e)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@properties_app.command("create")
|
|
81
|
+
def create_cmd(
|
|
82
|
+
display_name: str = typer.Option(..., "--name", help="Property display name"),
|
|
83
|
+
account_id: Optional[str] = typer.Option(None, "--account-id", "-a", help="Account ID"),
|
|
84
|
+
timezone: str = typer.Option("America/Los_Angeles", "--timezone", help="Reporting time zone"),
|
|
85
|
+
currency: str = typer.Option("USD", "--currency", help="Currency code"),
|
|
86
|
+
dry_run: bool = typer.Option(
|
|
87
|
+
False, "--dry-run", help="Preview the request without executing"
|
|
88
|
+
),
|
|
89
|
+
output_format: Optional[str] = typer.Option(
|
|
90
|
+
None, "--output", "-o", help="Output format (json, table, compact)"
|
|
91
|
+
),
|
|
92
|
+
):
|
|
93
|
+
"""Create a new GA4 property."""
|
|
94
|
+
try:
|
|
95
|
+
effective_account = get_effective_value(account_id, "default_account_id")
|
|
96
|
+
require_options({"account_id": effective_account}, ["account_id"])
|
|
97
|
+
effective_format = resolve_output_format(output_format)
|
|
98
|
+
|
|
99
|
+
body = {
|
|
100
|
+
"parent": f"accounts/{effective_account}",
|
|
101
|
+
"displayName": display_name,
|
|
102
|
+
"timeZone": timezone,
|
|
103
|
+
"currencyCode": currency,
|
|
104
|
+
}
|
|
105
|
+
if dry_run:
|
|
106
|
+
handle_dry_run("create", "POST", f"accounts/{effective_account}", body)
|
|
107
|
+
|
|
108
|
+
admin = get_admin_client()
|
|
109
|
+
prop = admin.properties().create(body=body).execute()
|
|
110
|
+
output(prop, effective_format)
|
|
111
|
+
except typer.Exit:
|
|
112
|
+
raise
|
|
113
|
+
except Exception as e:
|
|
114
|
+
handle_error(e)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@properties_app.command("update")
|
|
118
|
+
def update_cmd(
|
|
119
|
+
property_id: Optional[str] = typer.Option(
|
|
120
|
+
None, "--property-id", "-p", help="Property ID (numeric)"
|
|
121
|
+
),
|
|
122
|
+
name: Optional[str] = typer.Option(None, "--name", help="New display name"),
|
|
123
|
+
timezone: Optional[str] = typer.Option(
|
|
124
|
+
None, "--timezone", help="Reporting time zone (e.g., America/New_York)"
|
|
125
|
+
),
|
|
126
|
+
currency: Optional[str] = typer.Option(
|
|
127
|
+
None, "--currency", help="Currency code (e.g., USD, EUR)"
|
|
128
|
+
),
|
|
129
|
+
industry: Optional[str] = typer.Option(None, "--industry", help="Industry category"),
|
|
130
|
+
dry_run: bool = typer.Option(
|
|
131
|
+
False, "--dry-run", help="Preview the request without executing"
|
|
132
|
+
),
|
|
133
|
+
output_format: Optional[str] = typer.Option(
|
|
134
|
+
None, "--output", "-o", help="Output format (json, table, compact)"
|
|
135
|
+
),
|
|
136
|
+
):
|
|
137
|
+
"""Update a GA4 property."""
|
|
138
|
+
try:
|
|
139
|
+
effective_property = get_effective_value(property_id, "default_property_id")
|
|
140
|
+
require_options({"property_id": effective_property}, ["property_id"])
|
|
141
|
+
effective_format = resolve_output_format(output_format)
|
|
142
|
+
|
|
143
|
+
# Map CLI options to API field names
|
|
144
|
+
field_map = {
|
|
145
|
+
"displayName": name,
|
|
146
|
+
"timeZone": timezone,
|
|
147
|
+
"currencyCode": currency,
|
|
148
|
+
"industryCategory": industry,
|
|
149
|
+
}
|
|
150
|
+
body = {k: v for k, v in field_map.items() if v is not None}
|
|
151
|
+
|
|
152
|
+
if not body:
|
|
153
|
+
raise typer.BadParameter(
|
|
154
|
+
"At least one of --name, --timezone, --currency, or --industry must be specified."
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
update_mask = ",".join(body.keys())
|
|
158
|
+
|
|
159
|
+
if dry_run:
|
|
160
|
+
handle_dry_run(
|
|
161
|
+
"update", "PATCH", f"properties/{effective_property}",
|
|
162
|
+
body, update_mask=update_mask,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
admin = get_admin_client()
|
|
166
|
+
prop = (
|
|
167
|
+
admin.properties()
|
|
168
|
+
.patch(
|
|
169
|
+
name=f"properties/{effective_property}",
|
|
170
|
+
body=body,
|
|
171
|
+
updateMask=update_mask,
|
|
172
|
+
)
|
|
173
|
+
.execute()
|
|
174
|
+
)
|
|
175
|
+
output(prop, effective_format)
|
|
176
|
+
except (typer.BadParameter, typer.Exit):
|
|
177
|
+
raise
|
|
178
|
+
except Exception as e:
|
|
179
|
+
handle_error(e)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@properties_app.command("delete")
|
|
183
|
+
def delete_cmd(
|
|
184
|
+
property_id: Optional[str] = typer.Option(
|
|
185
|
+
None, "--property-id", "-p", help="Property ID (numeric)"
|
|
186
|
+
),
|
|
187
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
|
|
188
|
+
dry_run: bool = typer.Option(
|
|
189
|
+
False, "--dry-run", help="Preview the request without executing"
|
|
190
|
+
),
|
|
191
|
+
):
|
|
192
|
+
"""Delete a GA4 property (soft delete)."""
|
|
193
|
+
try:
|
|
194
|
+
effective_property = get_effective_value(property_id, "default_property_id")
|
|
195
|
+
require_options({"property_id": effective_property}, ["property_id"])
|
|
196
|
+
|
|
197
|
+
if dry_run:
|
|
198
|
+
handle_dry_run("delete", "DELETE", f"properties/{effective_property}", None)
|
|
199
|
+
|
|
200
|
+
if not yes:
|
|
201
|
+
confirmed = questionary.confirm(
|
|
202
|
+
f"Delete property {effective_property}? This cannot be undone."
|
|
203
|
+
).ask()
|
|
204
|
+
if not confirmed:
|
|
205
|
+
info("Cancelled.")
|
|
206
|
+
raise typer.Exit()
|
|
207
|
+
|
|
208
|
+
admin = get_admin_client()
|
|
209
|
+
admin.properties().delete(name=f"properties/{effective_property}").execute()
|
|
210
|
+
success(f"Property {effective_property} deleted.")
|
|
211
|
+
except typer.Exit:
|
|
212
|
+
raise
|
|
213
|
+
except Exception as e:
|
|
214
|
+
handle_error(e)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
_UDC_ACKNOWLEDGEMENT = (
|
|
218
|
+
"I acknowledge that I have the necessary privacy disclosures and rights "
|
|
219
|
+
"from my end users for the collection and processing of their data, "
|
|
220
|
+
"including the association of such data with the visitation information "
|
|
221
|
+
"Google Analytics collects from my site and/or app property."
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@properties_app.command("acknowledge-udc")
|
|
226
|
+
def acknowledge_udc_cmd(
|
|
227
|
+
property_id: Optional[str] = typer.Option(
|
|
228
|
+
None, "--property-id", "-p", help="Property ID (numeric)"
|
|
229
|
+
),
|
|
230
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
|
|
231
|
+
dry_run: bool = typer.Option(
|
|
232
|
+
False, "--dry-run", help="Preview the request without executing"
|
|
233
|
+
),
|
|
234
|
+
):
|
|
235
|
+
"""Acknowledge user data collection for a property.
|
|
236
|
+
|
|
237
|
+
Required before Measurement Protocol secrets can be created.
|
|
238
|
+
"""
|
|
239
|
+
try:
|
|
240
|
+
effective_property = get_effective_value(property_id, "default_property_id")
|
|
241
|
+
require_options({"property_id": effective_property}, ["property_id"])
|
|
242
|
+
|
|
243
|
+
if dry_run:
|
|
244
|
+
handle_dry_run(
|
|
245
|
+
"acknowledge", "POST", f"properties/{effective_property}",
|
|
246
|
+
{"acknowledgement": _UDC_ACKNOWLEDGEMENT},
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
if not yes:
|
|
250
|
+
info(f'You are about to acknowledge:\n\n "{_UDC_ACKNOWLEDGEMENT}"\n')
|
|
251
|
+
confirmed = questionary.confirm("Proceed with acknowledgement?").ask()
|
|
252
|
+
if not confirmed:
|
|
253
|
+
info("Cancelled.")
|
|
254
|
+
raise typer.Exit()
|
|
255
|
+
|
|
256
|
+
admin = get_admin_client()
|
|
257
|
+
admin.properties().acknowledgeUserDataCollection(
|
|
258
|
+
property=f"properties/{effective_property}",
|
|
259
|
+
body={"acknowledgement": _UDC_ACKNOWLEDGEMENT},
|
|
260
|
+
).execute()
|
|
261
|
+
success(f"User data collection acknowledged for property {effective_property}.")
|
|
262
|
+
except typer.Exit:
|
|
263
|
+
raise
|
|
264
|
+
except Exception as e:
|
|
265
|
+
handle_error(e)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _format_quota_category(key: str) -> str:
|
|
269
|
+
"""Convert API field name to human-readable label (e.g. 'tokensPerDay' -> 'Tokens Per Day')."""
|
|
270
|
+
import re
|
|
271
|
+
|
|
272
|
+
# Split on camelCase boundaries
|
|
273
|
+
words = re.sub(r"([a-z])([A-Z])", r"\1 \2", key)
|
|
274
|
+
return words.title()
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
@properties_app.command("quotas")
|
|
278
|
+
def quotas_cmd(
|
|
279
|
+
property_id: Optional[str] = typer.Option(
|
|
280
|
+
None, "--property-id", "-p", help="Property ID (numeric)"
|
|
281
|
+
),
|
|
282
|
+
output_format: Optional[str] = typer.Option(
|
|
283
|
+
None, "--output", "-o", help="Output format (json, table, compact)"
|
|
284
|
+
),
|
|
285
|
+
):
|
|
286
|
+
"""Show API quota usage for a property."""
|
|
287
|
+
try:
|
|
288
|
+
effective_property = get_effective_value(property_id, "default_property_id")
|
|
289
|
+
require_options({"property_id": effective_property}, ["property_id"])
|
|
290
|
+
effective_format = resolve_output_format(output_format)
|
|
291
|
+
|
|
292
|
+
data_alpha = get_data_alpha_client()
|
|
293
|
+
result = (
|
|
294
|
+
data_alpha.properties()
|
|
295
|
+
.getPropertyQuotasSnapshot(
|
|
296
|
+
name=f"properties/{effective_property}/propertyQuotasSnapshot"
|
|
297
|
+
)
|
|
298
|
+
.execute()
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
if effective_format != "table":
|
|
302
|
+
output(result, effective_format)
|
|
303
|
+
return
|
|
304
|
+
|
|
305
|
+
rows = []
|
|
306
|
+
for category_key, category_value in result.items():
|
|
307
|
+
if not isinstance(category_value, dict):
|
|
308
|
+
continue
|
|
309
|
+
category_label = _format_quota_category(category_key)
|
|
310
|
+
for metric_key, metric_value in category_value.items():
|
|
311
|
+
if isinstance(metric_value, dict) and "remaining" in metric_value:
|
|
312
|
+
rows.append({
|
|
313
|
+
"category": category_label,
|
|
314
|
+
"metric": _format_quota_category(metric_key),
|
|
315
|
+
"consumed": str(metric_value.get("consumed", 0)),
|
|
316
|
+
"remaining": str(metric_value.get("remaining", 0)),
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
if not rows:
|
|
320
|
+
info("No quota data available.")
|
|
321
|
+
else:
|
|
322
|
+
output(
|
|
323
|
+
rows,
|
|
324
|
+
effective_format,
|
|
325
|
+
columns=["category", "metric", "consumed", "remaining"],
|
|
326
|
+
headers=["Quota Category", "Metric", "Consumed", "Remaining"],
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
except Exception as e:
|
|
330
|
+
handle_error(e)
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""Property settings commands (attribution, Google Signals, enhanced measurement)."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from ..api.client import get_admin_alpha_client
|
|
8
|
+
from ..config.store import get_effective_value
|
|
9
|
+
from ..utils import handle_error, output, require_options, resolve_output_format, success
|
|
10
|
+
|
|
11
|
+
property_settings_app = typer.Typer(
|
|
12
|
+
name="property-settings",
|
|
13
|
+
help="Manage property-level settings",
|
|
14
|
+
no_args_is_help=True,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# --- Attribution Settings ---
|
|
18
|
+
|
|
19
|
+
_ACQUISITION_LOOKBACK_CHOICES = [
|
|
20
|
+
"ACQUISITION_CONVERSION_EVENT_LOOKBACK_WINDOW_7_DAYS",
|
|
21
|
+
"ACQUISITION_CONVERSION_EVENT_LOOKBACK_WINDOW_30_DAYS",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
_OTHER_LOOKBACK_CHOICES = [
|
|
25
|
+
"OTHER_CONVERSION_EVENT_LOOKBACK_WINDOW_30_DAYS",
|
|
26
|
+
"OTHER_CONVERSION_EVENT_LOOKBACK_WINDOW_60_DAYS",
|
|
27
|
+
"OTHER_CONVERSION_EVENT_LOOKBACK_WINDOW_90_DAYS",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
_ATTRIBUTION_MODEL_CHOICES = [
|
|
31
|
+
"PAID_AND_ORGANIC_CHANNELS_DATA_DRIVEN",
|
|
32
|
+
"PAID_AND_ORGANIC_CHANNELS_LAST_CLICK",
|
|
33
|
+
"GOOGLE_PAID_CHANNELS_LAST_CLICK",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
_ADS_EXPORT_SCOPE_CHOICES = [
|
|
37
|
+
"NOT_SELECTED_YET",
|
|
38
|
+
"PAID_AND_ORGANIC_CHANNELS",
|
|
39
|
+
"GOOGLE_PAID_CHANNELS",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _validate_enum(value: Optional[str], choices: list[str], flag_name: str) -> None:
|
|
44
|
+
"""Validate an enum value against allowed choices."""
|
|
45
|
+
if value is not None and value not in choices:
|
|
46
|
+
raise typer.BadParameter(
|
|
47
|
+
f"Invalid value '{value}' for {flag_name}. "
|
|
48
|
+
f"Must be one of: {', '.join(choices)}"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@property_settings_app.command("attribution")
|
|
53
|
+
def attribution_cmd(
|
|
54
|
+
property_id: Optional[str] = typer.Option(
|
|
55
|
+
None, "--property-id", "-p", help="Property ID (numeric)"
|
|
56
|
+
),
|
|
57
|
+
acquisition_lookback: Optional[str] = typer.Option(
|
|
58
|
+
None,
|
|
59
|
+
"--acquisition-lookback",
|
|
60
|
+
help=f"Acquisition conversion lookback window ({', '.join(_ACQUISITION_LOOKBACK_CHOICES)})",
|
|
61
|
+
),
|
|
62
|
+
other_lookback: Optional[str] = typer.Option(
|
|
63
|
+
None,
|
|
64
|
+
"--other-lookback",
|
|
65
|
+
help=f"Other conversion lookback window ({', '.join(_OTHER_LOOKBACK_CHOICES)})",
|
|
66
|
+
),
|
|
67
|
+
attribution_model: Optional[str] = typer.Option(
|
|
68
|
+
None,
|
|
69
|
+
"--attribution-model",
|
|
70
|
+
help=f"Reporting attribution model ({', '.join(_ATTRIBUTION_MODEL_CHOICES)})",
|
|
71
|
+
),
|
|
72
|
+
ads_export_scope: Optional[str] = typer.Option(
|
|
73
|
+
None,
|
|
74
|
+
"--ads-export-scope",
|
|
75
|
+
help=f"Ads web conversion data export scope ({', '.join(_ADS_EXPORT_SCOPE_CHOICES)})",
|
|
76
|
+
),
|
|
77
|
+
output_format: Optional[str] = typer.Option(
|
|
78
|
+
None, "--output", "-o", help="Output format (json, table, compact)"
|
|
79
|
+
),
|
|
80
|
+
):
|
|
81
|
+
"""Get or update attribution settings. With no update flags, displays current settings."""
|
|
82
|
+
try:
|
|
83
|
+
effective_property = get_effective_value(property_id, "default_property_id")
|
|
84
|
+
require_options({"property_id": effective_property}, ["property_id"])
|
|
85
|
+
effective_format = resolve_output_format(output_format)
|
|
86
|
+
|
|
87
|
+
resource_name = f"properties/{effective_property}/attributionSettings"
|
|
88
|
+
admin = get_admin_alpha_client()
|
|
89
|
+
|
|
90
|
+
field_map = {
|
|
91
|
+
"acquisitionConversionEventLookbackWindow": acquisition_lookback,
|
|
92
|
+
"otherConversionEventLookbackWindow": other_lookback,
|
|
93
|
+
"reportingAttributionModel": attribution_model,
|
|
94
|
+
"adsWebConversionDataExportScope": ads_export_scope,
|
|
95
|
+
}
|
|
96
|
+
body = {k: v for k, v in field_map.items() if v is not None}
|
|
97
|
+
|
|
98
|
+
if body:
|
|
99
|
+
# Validate enums
|
|
100
|
+
_validate_enum(
|
|
101
|
+
acquisition_lookback,
|
|
102
|
+
_ACQUISITION_LOOKBACK_CHOICES,
|
|
103
|
+
"--acquisition-lookback",
|
|
104
|
+
)
|
|
105
|
+
_validate_enum(other_lookback, _OTHER_LOOKBACK_CHOICES, "--other-lookback")
|
|
106
|
+
_validate_enum(attribution_model, _ATTRIBUTION_MODEL_CHOICES, "--attribution-model")
|
|
107
|
+
_validate_enum(ads_export_scope, _ADS_EXPORT_SCOPE_CHOICES, "--ads-export-scope")
|
|
108
|
+
|
|
109
|
+
update_mask = ",".join(body.keys())
|
|
110
|
+
settings = (
|
|
111
|
+
admin.properties()
|
|
112
|
+
.updateAttributionSettings(
|
|
113
|
+
name=resource_name, updateMask=update_mask, body=body
|
|
114
|
+
)
|
|
115
|
+
.execute()
|
|
116
|
+
)
|
|
117
|
+
success("Attribution settings updated.")
|
|
118
|
+
else:
|
|
119
|
+
settings = (
|
|
120
|
+
admin.properties()
|
|
121
|
+
.getAttributionSettings(name=resource_name)
|
|
122
|
+
.execute()
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
output(settings, effective_format)
|
|
126
|
+
except typer.BadParameter:
|
|
127
|
+
raise
|
|
128
|
+
except Exception as e:
|
|
129
|
+
handle_error(e)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# --- Google Signals Settings ---
|
|
133
|
+
|
|
134
|
+
_SIGNALS_STATE_CHOICES = [
|
|
135
|
+
"GOOGLE_SIGNALS_ENABLED",
|
|
136
|
+
"GOOGLE_SIGNALS_DISABLED",
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@property_settings_app.command("google-signals")
|
|
141
|
+
def google_signals_cmd(
|
|
142
|
+
property_id: Optional[str] = typer.Option(
|
|
143
|
+
None, "--property-id", "-p", help="Property ID (numeric)"
|
|
144
|
+
),
|
|
145
|
+
state: Optional[str] = typer.Option(
|
|
146
|
+
None,
|
|
147
|
+
"--state",
|
|
148
|
+
help=f"Google Signals state ({', '.join(_SIGNALS_STATE_CHOICES)})",
|
|
149
|
+
),
|
|
150
|
+
output_format: Optional[str] = typer.Option(
|
|
151
|
+
None, "--output", "-o", help="Output format (json, table, compact)"
|
|
152
|
+
),
|
|
153
|
+
):
|
|
154
|
+
"""Get or update Google Signals settings. With no update flags, displays current settings."""
|
|
155
|
+
try:
|
|
156
|
+
effective_property = get_effective_value(property_id, "default_property_id")
|
|
157
|
+
require_options({"property_id": effective_property}, ["property_id"])
|
|
158
|
+
effective_format = resolve_output_format(output_format)
|
|
159
|
+
|
|
160
|
+
resource_name = f"properties/{effective_property}/googleSignalsSettings"
|
|
161
|
+
admin = get_admin_alpha_client()
|
|
162
|
+
|
|
163
|
+
if state is not None:
|
|
164
|
+
_validate_enum(state, _SIGNALS_STATE_CHOICES, "--state")
|
|
165
|
+
body = {"state": state}
|
|
166
|
+
settings = (
|
|
167
|
+
admin.properties()
|
|
168
|
+
.updateGoogleSignalsSettings(
|
|
169
|
+
name=resource_name, updateMask="state", body=body
|
|
170
|
+
)
|
|
171
|
+
.execute()
|
|
172
|
+
)
|
|
173
|
+
success("Google Signals settings updated.")
|
|
174
|
+
else:
|
|
175
|
+
settings = (
|
|
176
|
+
admin.properties()
|
|
177
|
+
.getGoogleSignalsSettings(name=resource_name)
|
|
178
|
+
.execute()
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
output(settings, effective_format)
|
|
182
|
+
except typer.BadParameter:
|
|
183
|
+
raise
|
|
184
|
+
except Exception as e:
|
|
185
|
+
handle_error(e)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# --- Enhanced Measurement Settings ---
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@property_settings_app.command("enhanced-measurement")
|
|
192
|
+
def enhanced_measurement_cmd(
|
|
193
|
+
property_id: Optional[str] = typer.Option(
|
|
194
|
+
None, "--property-id", "-p", help="Property ID (numeric)"
|
|
195
|
+
),
|
|
196
|
+
stream_id: str = typer.Option(
|
|
197
|
+
..., "--stream-id", "-s", help="Data stream ID"
|
|
198
|
+
),
|
|
199
|
+
stream_enabled: Optional[bool] = typer.Option(
|
|
200
|
+
None,
|
|
201
|
+
"--stream-enabled/--no-stream-enabled",
|
|
202
|
+
help="Enable/disable enhanced measurement for this stream",
|
|
203
|
+
),
|
|
204
|
+
scrolls: Optional[bool] = typer.Option(
|
|
205
|
+
None, "--scrolls/--no-scrolls", help="Capture scroll events"
|
|
206
|
+
),
|
|
207
|
+
outbound_clicks: Optional[bool] = typer.Option(
|
|
208
|
+
None, "--outbound-clicks/--no-outbound-clicks", help="Capture outbound click events"
|
|
209
|
+
),
|
|
210
|
+
site_search: Optional[bool] = typer.Option(
|
|
211
|
+
None, "--site-search/--no-site-search", help="Capture site search events"
|
|
212
|
+
),
|
|
213
|
+
video_engagement: Optional[bool] = typer.Option(
|
|
214
|
+
None, "--video-engagement/--no-video-engagement", help="Capture video engagement events"
|
|
215
|
+
),
|
|
216
|
+
file_downloads: Optional[bool] = typer.Option(
|
|
217
|
+
None, "--file-downloads/--no-file-downloads", help="Capture file download events"
|
|
218
|
+
),
|
|
219
|
+
page_changes: Optional[bool] = typer.Option(
|
|
220
|
+
None, "--page-changes/--no-page-changes", help="Capture page change (history) events"
|
|
221
|
+
),
|
|
222
|
+
form_interactions: Optional[bool] = typer.Option(
|
|
223
|
+
None, "--form-interactions/--no-form-interactions", help="Capture form interaction events"
|
|
224
|
+
),
|
|
225
|
+
search_query_parameter: Optional[str] = typer.Option(
|
|
226
|
+
None, "--search-query-parameter", help="URL query parameters for site search"
|
|
227
|
+
),
|
|
228
|
+
uri_query_parameter: Optional[str] = typer.Option(
|
|
229
|
+
None, "--uri-query-parameter", help="Additional URL query parameters"
|
|
230
|
+
),
|
|
231
|
+
output_format: Optional[str] = typer.Option(
|
|
232
|
+
None, "--output", "-o", help="Output format (json, table, compact)"
|
|
233
|
+
),
|
|
234
|
+
):
|
|
235
|
+
"""Get or update enhanced measurement settings.
|
|
236
|
+
|
|
237
|
+
With no update flags, displays current settings.
|
|
238
|
+
"""
|
|
239
|
+
try:
|
|
240
|
+
effective_property = get_effective_value(property_id, "default_property_id")
|
|
241
|
+
require_options({"property_id": effective_property}, ["property_id"])
|
|
242
|
+
effective_format = resolve_output_format(output_format)
|
|
243
|
+
|
|
244
|
+
resource_name = (
|
|
245
|
+
f"properties/{effective_property}/dataStreams/{stream_id}"
|
|
246
|
+
"/enhancedMeasurementSettings"
|
|
247
|
+
)
|
|
248
|
+
admin = get_admin_alpha_client()
|
|
249
|
+
|
|
250
|
+
field_map = {
|
|
251
|
+
"streamEnabled": stream_enabled,
|
|
252
|
+
"scrollsEnabled": scrolls,
|
|
253
|
+
"outboundClicksEnabled": outbound_clicks,
|
|
254
|
+
"siteSearchEnabled": site_search,
|
|
255
|
+
"videoEngagementEnabled": video_engagement,
|
|
256
|
+
"fileDownloadsEnabled": file_downloads,
|
|
257
|
+
"pageChangesEnabled": page_changes,
|
|
258
|
+
"formInteractionsEnabled": form_interactions,
|
|
259
|
+
"searchQueryParameter": search_query_parameter,
|
|
260
|
+
"uriQueryParameter": uri_query_parameter,
|
|
261
|
+
}
|
|
262
|
+
body = {k: v for k, v in field_map.items() if v is not None}
|
|
263
|
+
|
|
264
|
+
if body:
|
|
265
|
+
update_mask = ",".join(body.keys())
|
|
266
|
+
settings = (
|
|
267
|
+
admin.properties()
|
|
268
|
+
.dataStreams()
|
|
269
|
+
.updateEnhancedMeasurementSettings(
|
|
270
|
+
name=resource_name, updateMask=update_mask, body=body
|
|
271
|
+
)
|
|
272
|
+
.execute()
|
|
273
|
+
)
|
|
274
|
+
success("Enhanced measurement settings updated.")
|
|
275
|
+
else:
|
|
276
|
+
settings = (
|
|
277
|
+
admin.properties()
|
|
278
|
+
.dataStreams()
|
|
279
|
+
.getEnhancedMeasurementSettings(name=resource_name)
|
|
280
|
+
.execute()
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
output(settings, effective_format)
|
|
284
|
+
except typer.BadParameter:
|
|
285
|
+
raise
|
|
286
|
+
except Exception as e:
|
|
287
|
+
handle_error(e)
|