adloop 0.21.0__tar.gz → 0.22.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.
Files changed (69) hide show
  1. {adloop-0.21.0 → adloop-0.22.0}/PKG-INFO +19 -13
  2. {adloop-0.21.0 → adloop-0.22.0}/README.md +18 -12
  3. {adloop-0.21.0 → adloop-0.22.0}/pyproject.toml +1 -1
  4. {adloop-0.21.0 → adloop-0.22.0}/pyproject.toml.orig +1 -1
  5. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/ads/conversion_actions.py +32 -10
  6. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/ads/gaql.py +6 -0
  7. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/ads/read.py +432 -3
  8. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/ads/write.py +19 -6
  9. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/crossref.py +24 -8
  10. adloop-0.22.0/src/adloop/ga4/reports.py +513 -0
  11. adloop-0.22.0/src/adloop/ga4/tracking.py +89 -0
  12. adloop-0.22.0/src/adloop/ga4/write.py +239 -0
  13. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/gsc/reports.py +89 -8
  14. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/gtm/read.py +424 -111
  15. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/gtm/write.py +213 -9
  16. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/pagespeed.py +56 -8
  17. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/reddit/read.py +59 -2
  18. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/reddit/write.py +147 -1
  19. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/rules/adloop.md +49 -24
  20. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/server.py +459 -51
  21. adloop-0.21.0/src/adloop/ga4/reports.py +0 -162
  22. adloop-0.21.0/src/adloop/ga4/tracking.py +0 -36
  23. adloop-0.21.0/src/adloop/ga4/write.py +0 -100
  24. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/__init__.py +0 -0
  25. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/__main__.py +0 -0
  26. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/_mcp_patches.py +0 -0
  27. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/ads/__init__.py +0 -0
  28. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/ads/ai_max.py +0 -0
  29. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/ads/brands.py +0 -0
  30. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/ads/client.py +0 -0
  31. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/ads/currency.py +0 -0
  32. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/ads/custom_conversion_goals.py +0 -0
  33. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/ads/enums.py +0 -0
  34. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/ads/forecast.py +0 -0
  35. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/ads/image_fetch.py +0 -0
  36. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/ads/pmax.py +0 -0
  37. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/ads/validate_only.py +0 -0
  38. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/auth.py +0 -0
  39. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/cli.py +0 -0
  40. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/config.py +0 -0
  41. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/diagnostics.py +0 -0
  42. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/ga4/__init__.py +0 -0
  43. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/ga4/client.py +0 -0
  44. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/gsc/__init__.py +0 -0
  45. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/gsc/client.py +0 -0
  46. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/gtm/__init__.py +0 -0
  47. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/gtm/client.py +0 -0
  48. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/gtm/entities.py +0 -0
  49. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/merchant/__init__.py +0 -0
  50. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/merchant/client.py +0 -0
  51. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/merchant/read.py +0 -0
  52. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/reddit/__init__.py +0 -0
  53. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/reddit/auth.py +0 -0
  54. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/reddit/client.py +0 -0
  55. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/reddit/schedule.py +0 -0
  56. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/rules/__init__.py +0 -0
  57. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/rules/commands/analyze-performance.md +0 -0
  58. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/rules/commands/budget-plan.md +0 -0
  59. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/rules/commands/create-ad.md +0 -0
  60. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/rules/commands/create-campaign.md +0 -0
  61. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/rules/commands/diagnose-tracking.md +0 -0
  62. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/rules/commands/optimize-campaign.md +0 -0
  63. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/rules_install.py +0 -0
  64. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/runtime.py +0 -0
  65. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/safety/__init__.py +0 -0
  66. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/safety/audit.py +0 -0
  67. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/safety/guards.py +0 -0
  68. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/safety/preview.py +0 -0
  69. {adloop-0.21.0 → adloop-0.22.0}/src/adloop/tracking.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: adloop
3
- Version: 0.21.0
3
+ Version: 0.22.0
4
4
  Summary: The AI command center for Google Ads, Reddit Ads, GA4, and tracking code.
5
5
  Keywords: mcp,google-ads,reddit-ads,google-analytics,ga4,cursor,marketing
6
6
  Author: Daniel Klose
@@ -67,7 +67,7 @@ Both versions run the same tools with the same safety model. The difference is w
67
67
  | **Works with** | claude.ai, ChatGPT, Claude Code, Cursor, Gemini | Claude Code, Cursor, Claude Desktop, any local MCP client |
68
68
  | **Where your data flows** | EU servers (Germany), GDPR-first, DPA included | 100% your machine — nothing leaves it |
69
69
  | **Updates** | Automatic | `pip install -U adloop` |
70
- | **Price** | Free plan, no card; [paid plans](https://getadloop.com/preise) for more accounts and volume | Free forever (MIT) |
70
+ | **Price** | Free plan, no card; [paid plans](https://getadloop.com/pricing) for more accounts and volume | Free forever (MIT) |
71
71
 
72
72
  **Not sure? [Start with Cloud](https://getadloop.com)** — it's the fastest way to see what AdLoop can do, and it's the only way to use AdLoop from claude.ai or ChatGPT. Self-host when you want everything on your own machine or need to modify the code. And if you're here to hack on AdLoop itself: welcome, keep scrolling.
73
73
 
@@ -110,17 +110,19 @@ The best features come from real workflows. If you're using AdLoop and find your
110
110
  | Tool | What It Does |
111
111
  |------|-------------|
112
112
  | `get_account_summaries` | List GA4 accounts and properties |
113
- | `run_ga4_report` | Custom reports — sessions, users, conversions, page performance |
113
+ | `run_ga4_report` | Custom reports: sessions, users, key events, page performance. Optional dimension and metric filters, ordering, paging, and a comparison period |
114
114
  | `run_realtime_report` | Live data — verify tracking fires after deploys |
115
115
  | `get_tracking_events` | All configured events and their volume |
116
+ | `list_key_events` | The property's key events (conversions) with counting method, create time and whether they can be deleted |
117
+ | `list_ga4_dimensions_and_metrics` | Every dimension and metric the property can report on, including custom ones, with search |
116
118
 
117
119
  ### Google Ads Read Tools
118
120
 
119
121
  | Tool | What It Does |
120
122
  |------|-------------|
121
123
  | `list_accounts` | Discover accessible Ads accounts |
122
- | `get_campaign_performance` | Campaign metrics — impressions, clicks, cost, conversions, CPA |
123
- | `get_ad_performance` | Ad copy analysis — headlines, descriptions, CTR |
124
+ | `get_campaign_performance` | Campaign metrics: impressions, clicks, cost, conversions, CPA, Search impression share and share lost to budget/rank |
125
+ | `get_ad_performance` | Ad copy analysis: headlines, descriptions, CTR, policy approval status and topics |
124
126
  | `get_keyword_performance` | Keywords — quality scores, competitive metrics |
125
127
  | `get_search_terms` | What users actually searched before clicking |
126
128
  | `get_negative_keywords` | List direct campaign-level negative keywords |
@@ -132,6 +134,7 @@ The best features come from real workflows. If you're using AdLoop and find your
132
134
  | `get_pmax_assets` | Per-asset details for PMax — field type, serving status, content |
133
135
  | `get_detailed_asset_performance` | Top-performing asset combinations — which headline+description+image combos Google selects most |
134
136
  | `get_audience_performance` | Audience segment performance — remarketing, in-market, affinity, demographics |
137
+ | `get_change_history` | What changed before the drop: who changed what and when, through which client (UI, API, scripts, auto-applied recommendations), last 30 days |
135
138
  | `get_demographic_targeting` | List demographic criteria (age/gender/parental status/income) on an ad group or campaign |
136
139
  | `suggest_brands` | Resolve a brand name to the brands Google recognizes — brand ID, name, state, URLs |
137
140
  | `check_brand_names` | Check a shortlist of brand names against Google's brand knowledge graph (max 25 per call) |
@@ -178,15 +181,16 @@ These tools read the live GTM container and join it with the codebase + GA4 to f
178
181
  | `audit_event_coverage` | **The flagship.** Three-way join: codebase events ↔ GTM tags ↔ GA4 actual fires. For each event name in `expected_events`, returns one of 10 statuses (`ok`, `no_tag_no_fire`, `tag_paused`, `tag_active_but_not_firing`, `gtm_only_firing`, `ga4_only`, etc.) plus auto-generated insights for the gaps. |
179
182
  | `list_gtm_accounts` | Discover accessible GTM accounts |
180
183
  | `list_gtm_containers` | List containers under an account — returns numeric `container_id` (needed by other tools), public `GTM-XXXXXXX` ID, and usage context (web/iOS/Android/server) |
181
- | `list_gtm_tags` | Every tag in the live container with parsed event names and resolved firing/blocking trigger names |
184
+ | `list_gtm_tags` | Every tag in the live container with parsed event names and resolved firing/blocking trigger names. Pass `workspace_id` to read a workspace's unpublished state instead, so a just-drafted tag is visible (also on `get_gtm_tag`, `list_gtm_triggers`, `get_gtm_trigger`, `list_gtm_variables`) |
182
185
  | `get_gtm_tag` | Full raw config for a single tag — every parameter, firing/blocking triggers with filter conditions, priority, pause status, sampling |
183
186
  | `list_gtm_triggers` | Every trigger with filter conditions parsed to readable text (e.g. `{{Page Path}} contains service-promotions`, `{{Form ID}} NOT contains wf-form-...`). Renders the `negate` flag explicitly. |
184
187
  | `get_gtm_trigger` | Full trigger config + reverse lookup of every tag that uses it. Includes parsed `element_visibility` block (selector, on-screen ratio, firing frequency) for elementVisibility triggers and `group_member_trigger_ids` for triggerGroup types |
185
188
  | `list_gtm_variables` | Custom variables (data layer, constants, JS) plus enabled built-in variables |
186
189
  | `list_gtm_workspaces` | List drafts (workspaces) under a container — workspace IDs are needed by `get_gtm_workspace_diff` |
187
190
  | `get_gtm_workspace_diff` | Drafted-but-not-published changes — common cause of "I edited a tag but nothing happened". Returns `is_clean: true` when nothing is pending. |
188
- | `list_gtm_versions` | Publish history with version IDs and entity counts. Use to correlate a metric drop with a recent publish. |
189
- | `get_gtm_version` | Full metadata + tag/trigger names for a single historical container version |
191
+ | `list_gtm_versions` | Version history, newest first, with version IDs and entity counts. The Tag Manager API returns no timestamps or author for versions. |
192
+ | `get_gtm_version` | Name, notes and tag/trigger names for a single historical container version |
193
+ | `get_gtm_version_diff` | What a publish changed: added, removed and changed tags, triggers and variables with the changed fields, plus built-in variables enabled or disabled. Defaults to the live version against the one before it; two or three API calls. |
190
194
 
191
195
  #### GTM write tools (opt-in)
192
196
 
@@ -198,13 +202,14 @@ Off by default. Set `gtm.write_enabled: true` in `~/.adloop/config.yaml` and res
198
202
  | `draft_gtm_trigger` | Create a trigger, or update one by `trigger_id` (type is immutable). `custom_event_name` wires a `customEvent` trigger to a dataLayer event. |
199
203
  | `draft_delete_gtm_entity` | Delete a workspace tag or trigger. Triggers still referenced by a tag are refused up front, with the tags listed. |
200
204
  | `draft_publish_gtm_workspace` | Publish a workspace live. The preview lists every pending change, including edits other people made in the GTM UI. |
205
+ | `draft_rollback_gtm_version` | Republish an older container version live. The preview is the diff from the live version to the target; the plan pins the live version, so apply refuses if someone publishes in between. Workspaces are left as they are. |
201
206
 
202
207
  Safety gates specific to GTM:
203
208
  - **Custom HTML** runs arbitrary JavaScript on your site, so creating or editing an `html` tag — or publishing a workspace that adds or changes one — is refused unless `gtm.allow_custom_html: true` is also set. Pausing or deleting one is always allowed.
204
209
  - **No stale writes.** Updates and deletes pin the entity's fingerprint from the preview, and publish pins the workspace's pending changes; if someone edits the container in between, apply refuses and you re-draft.
205
210
  - Publishing stops on merge conflicts or GTM compiler errors. The dry run runs a Tag Manager **quick preview** first (Tag Manager has no validate-only mode), so compiler errors surface before a version is even created. That preview is a POST and stores a preview version in the container — nothing is published and no live tag changes; the rest of the dry run only reads.
206
- - The publish result names the version it replaced (`previous_live_version_id`), so rolling back is one step: publish that version again in the GTM UI. If the version was created but publishing it failed, the error carries both the created version id and the still-live one.
207
- - Per-operation names for `safety.blocked_operations`: `gtm_create_tag`, `gtm_update_tag`, `gtm_delete_tag`, `gtm_create_trigger`, `gtm_update_trigger`, `gtm_delete_trigger`, `gtm_publish_workspace`.
211
+ - The publish and rollback results name the version they replaced (`previous_live_version_id`), so rolling back is one step: `draft_rollback_gtm_version` with that id. If the version was created but publishing it failed, the error carries both the created version id and the still-live one.
212
+ - Per-operation names for `safety.blocked_operations`: `gtm_create_tag`, `gtm_update_tag`, `gtm_delete_tag`, `gtm_create_trigger`, `gtm_update_trigger`, `gtm_delete_trigger`, `gtm_publish_workspace`, `gtm_rollback_version`.
208
213
 
209
214
  The Google account also needs **Edit** permission on the container (and **Publish** to publish) under Admin → User Management.
210
215
 
@@ -215,13 +220,13 @@ The Google account also needs **Edit** permission on the container (and **Publis
215
220
  | Tool | What It Does |
216
221
  |------|-------------|
217
222
  | `list_gsc_sites` | List Search Console properties the connected account can access |
218
- | `run_gsc_report` | Organic search analytics — clicks, impressions, CTR, position by query/page/country/device/date |
223
+ | `run_gsc_report` | Organic search analytics: clicks, impressions, CTR, position by query/page/country/device/date/searchAppearance/hour. Supports fresh data (`data_state="all"`), `aggregation_type`, and paging past 25,000 rows with `start_row` |
219
224
 
220
225
  ### Web Performance Tools
221
226
 
222
227
  | Tool | What It Does |
223
228
  |------|-------------|
224
- | `analyze_page_speed` | PageSpeed Insights for landing pages — Lighthouse score, Core Web Vitals, real-user CrUX data, top fixes. No OAuth needed (optional API key). |
229
+ | `analyze_page_speed` | PageSpeed Insights for landing pages: Lighthouse score, Core Web Vitals, real-user CrUX data (falls back to origin-wide data, labelled as such, when the page has too little traffic), top fixes. No OAuth needed (optional API key). |
225
230
 
226
231
  ### Merchant Center Tools
227
232
 
@@ -280,6 +285,7 @@ All write operations follow a **draft → preview → confirm** workflow. Nothin
280
285
  | `add_negative_keywords` | Propose negative keywords directly on a campaign |
281
286
  | `add_negative_locations` | Propose negative geo exclusions on a campaign — exclude cities/regions while keeping broader positive targets |
282
287
  | `draft_key_event` | Mark a GA4 event as a key event (conversion) — the fix for "fires but isn't tracked as a conversion" |
288
+ | `draft_delete_key_event` | Remove a GA4 key event. The event keeps firing but no longer counts as a conversion, for future data only |
283
289
  | `draft_demographic_targeting` | Propose demographic criteria (age, gender, parental status, income) — exclusions by default |
284
290
  | `propose_negative_keyword_list` | Draft a shared negative keyword list (SharedSet) and attach it to a campaign — reusable across multiple campaigns |
285
291
  | `propose_brand_list` | Draft a brand list (SharedSet of type BRANDS) from Commercial KG MIDs and optionally attach it to campaigns — `negative=true` (default) excludes the brands, `false` restricts targeting to them |
@@ -614,7 +620,7 @@ MIT — see [LICENSE](LICENSE).
614
620
 
615
621
  ## Privacy
616
622
 
617
- The open-source version runs entirely on your machine. No data is collected, stored, or transmitted to any server. See [PRIVACY.md](PRIVACY.md) for the full privacy policy. AdLoop Cloud has its own [privacy policy](https://getadloop.com/datenschutz) and [DPA](https://getadloop.com/avv).
623
+ The open-source version runs entirely on your machine. No data is collected, stored, or transmitted to any server. See [PRIVACY.md](PRIVACY.md) for the full privacy policy. AdLoop Cloud has its own [privacy policy](https://getadloop.com/privacy) and [DPA](https://getadloop.com/avv).
618
624
 
619
625
  ---
620
626
 
@@ -39,7 +39,7 @@ Both versions run the same tools with the same safety model. The difference is w
39
39
  | **Works with** | claude.ai, ChatGPT, Claude Code, Cursor, Gemini | Claude Code, Cursor, Claude Desktop, any local MCP client |
40
40
  | **Where your data flows** | EU servers (Germany), GDPR-first, DPA included | 100% your machine — nothing leaves it |
41
41
  | **Updates** | Automatic | `pip install -U adloop` |
42
- | **Price** | Free plan, no card; [paid plans](https://getadloop.com/preise) for more accounts and volume | Free forever (MIT) |
42
+ | **Price** | Free plan, no card; [paid plans](https://getadloop.com/pricing) for more accounts and volume | Free forever (MIT) |
43
43
 
44
44
  **Not sure? [Start with Cloud](https://getadloop.com)** — it's the fastest way to see what AdLoop can do, and it's the only way to use AdLoop from claude.ai or ChatGPT. Self-host when you want everything on your own machine or need to modify the code. And if you're here to hack on AdLoop itself: welcome, keep scrolling.
45
45
 
@@ -82,17 +82,19 @@ The best features come from real workflows. If you're using AdLoop and find your
82
82
  | Tool | What It Does |
83
83
  |------|-------------|
84
84
  | `get_account_summaries` | List GA4 accounts and properties |
85
- | `run_ga4_report` | Custom reports — sessions, users, conversions, page performance |
85
+ | `run_ga4_report` | Custom reports: sessions, users, key events, page performance. Optional dimension and metric filters, ordering, paging, and a comparison period |
86
86
  | `run_realtime_report` | Live data — verify tracking fires after deploys |
87
87
  | `get_tracking_events` | All configured events and their volume |
88
+ | `list_key_events` | The property's key events (conversions) with counting method, create time and whether they can be deleted |
89
+ | `list_ga4_dimensions_and_metrics` | Every dimension and metric the property can report on, including custom ones, with search |
88
90
 
89
91
  ### Google Ads Read Tools
90
92
 
91
93
  | Tool | What It Does |
92
94
  |------|-------------|
93
95
  | `list_accounts` | Discover accessible Ads accounts |
94
- | `get_campaign_performance` | Campaign metrics — impressions, clicks, cost, conversions, CPA |
95
- | `get_ad_performance` | Ad copy analysis — headlines, descriptions, CTR |
96
+ | `get_campaign_performance` | Campaign metrics: impressions, clicks, cost, conversions, CPA, Search impression share and share lost to budget/rank |
97
+ | `get_ad_performance` | Ad copy analysis: headlines, descriptions, CTR, policy approval status and topics |
96
98
  | `get_keyword_performance` | Keywords — quality scores, competitive metrics |
97
99
  | `get_search_terms` | What users actually searched before clicking |
98
100
  | `get_negative_keywords` | List direct campaign-level negative keywords |
@@ -104,6 +106,7 @@ The best features come from real workflows. If you're using AdLoop and find your
104
106
  | `get_pmax_assets` | Per-asset details for PMax — field type, serving status, content |
105
107
  | `get_detailed_asset_performance` | Top-performing asset combinations — which headline+description+image combos Google selects most |
106
108
  | `get_audience_performance` | Audience segment performance — remarketing, in-market, affinity, demographics |
109
+ | `get_change_history` | What changed before the drop: who changed what and when, through which client (UI, API, scripts, auto-applied recommendations), last 30 days |
107
110
  | `get_demographic_targeting` | List demographic criteria (age/gender/parental status/income) on an ad group or campaign |
108
111
  | `suggest_brands` | Resolve a brand name to the brands Google recognizes — brand ID, name, state, URLs |
109
112
  | `check_brand_names` | Check a shortlist of brand names against Google's brand knowledge graph (max 25 per call) |
@@ -150,15 +153,16 @@ These tools read the live GTM container and join it with the codebase + GA4 to f
150
153
  | `audit_event_coverage` | **The flagship.** Three-way join: codebase events ↔ GTM tags ↔ GA4 actual fires. For each event name in `expected_events`, returns one of 10 statuses (`ok`, `no_tag_no_fire`, `tag_paused`, `tag_active_but_not_firing`, `gtm_only_firing`, `ga4_only`, etc.) plus auto-generated insights for the gaps. |
151
154
  | `list_gtm_accounts` | Discover accessible GTM accounts |
152
155
  | `list_gtm_containers` | List containers under an account — returns numeric `container_id` (needed by other tools), public `GTM-XXXXXXX` ID, and usage context (web/iOS/Android/server) |
153
- | `list_gtm_tags` | Every tag in the live container with parsed event names and resolved firing/blocking trigger names |
156
+ | `list_gtm_tags` | Every tag in the live container with parsed event names and resolved firing/blocking trigger names. Pass `workspace_id` to read a workspace's unpublished state instead, so a just-drafted tag is visible (also on `get_gtm_tag`, `list_gtm_triggers`, `get_gtm_trigger`, `list_gtm_variables`) |
154
157
  | `get_gtm_tag` | Full raw config for a single tag — every parameter, firing/blocking triggers with filter conditions, priority, pause status, sampling |
155
158
  | `list_gtm_triggers` | Every trigger with filter conditions parsed to readable text (e.g. `{{Page Path}} contains service-promotions`, `{{Form ID}} NOT contains wf-form-...`). Renders the `negate` flag explicitly. |
156
159
  | `get_gtm_trigger` | Full trigger config + reverse lookup of every tag that uses it. Includes parsed `element_visibility` block (selector, on-screen ratio, firing frequency) for elementVisibility triggers and `group_member_trigger_ids` for triggerGroup types |
157
160
  | `list_gtm_variables` | Custom variables (data layer, constants, JS) plus enabled built-in variables |
158
161
  | `list_gtm_workspaces` | List drafts (workspaces) under a container — workspace IDs are needed by `get_gtm_workspace_diff` |
159
162
  | `get_gtm_workspace_diff` | Drafted-but-not-published changes — common cause of "I edited a tag but nothing happened". Returns `is_clean: true` when nothing is pending. |
160
- | `list_gtm_versions` | Publish history with version IDs and entity counts. Use to correlate a metric drop with a recent publish. |
161
- | `get_gtm_version` | Full metadata + tag/trigger names for a single historical container version |
163
+ | `list_gtm_versions` | Version history, newest first, with version IDs and entity counts. The Tag Manager API returns no timestamps or author for versions. |
164
+ | `get_gtm_version` | Name, notes and tag/trigger names for a single historical container version |
165
+ | `get_gtm_version_diff` | What a publish changed: added, removed and changed tags, triggers and variables with the changed fields, plus built-in variables enabled or disabled. Defaults to the live version against the one before it; two or three API calls. |
162
166
 
163
167
  #### GTM write tools (opt-in)
164
168
 
@@ -170,13 +174,14 @@ Off by default. Set `gtm.write_enabled: true` in `~/.adloop/config.yaml` and res
170
174
  | `draft_gtm_trigger` | Create a trigger, or update one by `trigger_id` (type is immutable). `custom_event_name` wires a `customEvent` trigger to a dataLayer event. |
171
175
  | `draft_delete_gtm_entity` | Delete a workspace tag or trigger. Triggers still referenced by a tag are refused up front, with the tags listed. |
172
176
  | `draft_publish_gtm_workspace` | Publish a workspace live. The preview lists every pending change, including edits other people made in the GTM UI. |
177
+ | `draft_rollback_gtm_version` | Republish an older container version live. The preview is the diff from the live version to the target; the plan pins the live version, so apply refuses if someone publishes in between. Workspaces are left as they are. |
173
178
 
174
179
  Safety gates specific to GTM:
175
180
  - **Custom HTML** runs arbitrary JavaScript on your site, so creating or editing an `html` tag — or publishing a workspace that adds or changes one — is refused unless `gtm.allow_custom_html: true` is also set. Pausing or deleting one is always allowed.
176
181
  - **No stale writes.** Updates and deletes pin the entity's fingerprint from the preview, and publish pins the workspace's pending changes; if someone edits the container in between, apply refuses and you re-draft.
177
182
  - Publishing stops on merge conflicts or GTM compiler errors. The dry run runs a Tag Manager **quick preview** first (Tag Manager has no validate-only mode), so compiler errors surface before a version is even created. That preview is a POST and stores a preview version in the container — nothing is published and no live tag changes; the rest of the dry run only reads.
178
- - The publish result names the version it replaced (`previous_live_version_id`), so rolling back is one step: publish that version again in the GTM UI. If the version was created but publishing it failed, the error carries both the created version id and the still-live one.
179
- - Per-operation names for `safety.blocked_operations`: `gtm_create_tag`, `gtm_update_tag`, `gtm_delete_tag`, `gtm_create_trigger`, `gtm_update_trigger`, `gtm_delete_trigger`, `gtm_publish_workspace`.
183
+ - The publish and rollback results name the version they replaced (`previous_live_version_id`), so rolling back is one step: `draft_rollback_gtm_version` with that id. If the version was created but publishing it failed, the error carries both the created version id and the still-live one.
184
+ - Per-operation names for `safety.blocked_operations`: `gtm_create_tag`, `gtm_update_tag`, `gtm_delete_tag`, `gtm_create_trigger`, `gtm_update_trigger`, `gtm_delete_trigger`, `gtm_publish_workspace`, `gtm_rollback_version`.
180
185
 
181
186
  The Google account also needs **Edit** permission on the container (and **Publish** to publish) under Admin → User Management.
182
187
 
@@ -187,13 +192,13 @@ The Google account also needs **Edit** permission on the container (and **Publis
187
192
  | Tool | What It Does |
188
193
  |------|-------------|
189
194
  | `list_gsc_sites` | List Search Console properties the connected account can access |
190
- | `run_gsc_report` | Organic search analytics — clicks, impressions, CTR, position by query/page/country/device/date |
195
+ | `run_gsc_report` | Organic search analytics: clicks, impressions, CTR, position by query/page/country/device/date/searchAppearance/hour. Supports fresh data (`data_state="all"`), `aggregation_type`, and paging past 25,000 rows with `start_row` |
191
196
 
192
197
  ### Web Performance Tools
193
198
 
194
199
  | Tool | What It Does |
195
200
  |------|-------------|
196
- | `analyze_page_speed` | PageSpeed Insights for landing pages — Lighthouse score, Core Web Vitals, real-user CrUX data, top fixes. No OAuth needed (optional API key). |
201
+ | `analyze_page_speed` | PageSpeed Insights for landing pages: Lighthouse score, Core Web Vitals, real-user CrUX data (falls back to origin-wide data, labelled as such, when the page has too little traffic), top fixes. No OAuth needed (optional API key). |
197
202
 
198
203
  ### Merchant Center Tools
199
204
 
@@ -252,6 +257,7 @@ All write operations follow a **draft → preview → confirm** workflow. Nothin
252
257
  | `add_negative_keywords` | Propose negative keywords directly on a campaign |
253
258
  | `add_negative_locations` | Propose negative geo exclusions on a campaign — exclude cities/regions while keeping broader positive targets |
254
259
  | `draft_key_event` | Mark a GA4 event as a key event (conversion) — the fix for "fires but isn't tracked as a conversion" |
260
+ | `draft_delete_key_event` | Remove a GA4 key event. The event keeps firing but no longer counts as a conversion, for future data only |
255
261
  | `draft_demographic_targeting` | Propose demographic criteria (age, gender, parental status, income) — exclusions by default |
256
262
  | `propose_negative_keyword_list` | Draft a shared negative keyword list (SharedSet) and attach it to a campaign — reusable across multiple campaigns |
257
263
  | `propose_brand_list` | Draft a brand list (SharedSet of type BRANDS) from Commercial KG MIDs and optionally attach it to campaigns — `negative=true` (default) excludes the brands, `false` restricts targeting to them |
@@ -586,7 +592,7 @@ MIT — see [LICENSE](LICENSE).
586
592
 
587
593
  ## Privacy
588
594
 
589
- The open-source version runs entirely on your machine. No data is collected, stored, or transmitted to any server. See [PRIVACY.md](PRIVACY.md) for the full privacy policy. AdLoop Cloud has its own [privacy policy](https://getadloop.com/datenschutz) and [DPA](https://getadloop.com/avv).
595
+ The open-source version runs entirely on your machine. No data is collected, stored, or transmitted to any server. See [PRIVACY.md](PRIVACY.md) for the full privacy policy. AdLoop Cloud has its own [privacy policy](https://getadloop.com/privacy) and [DPA](https://getadloop.com/avv).
590
596
 
591
597
  ---
592
598
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "adloop"
3
- version = "0.21.0"
3
+ version = "0.22.0"
4
4
  description = "The AI command center for Google Ads, Reddit Ads, GA4, and tracking code."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "adloop"
3
- version = "0.21.0"
3
+ version = "0.22.0"
4
4
  description = "The AI command center for Google Ads, Reddit Ads, GA4, and tracking code."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -1142,17 +1142,35 @@ def _parse_call_conversion_csv(
1142
1142
 
1143
1143
 
1144
1144
  def _redact_caller_id(caller_id: str) -> str:
1145
- """Mask an E.164 phone for display/logging: keep the leading digits and
1146
- the last 4, star the middle. e.g. '+15555550142' -> '+155***0142'.
1145
+ """Mask an E.164 phone for display/logging: the country code plus the last
1146
+ two digits. e.g. '+14155550142' -> '+1***42'.
1147
+
1148
+ Two trailing digits are enough to sanity-check that the right number was
1149
+ parsed. Everything between them is what would make the hint worth
1150
+ attacking, and this string reaches the preview, the model's context and the
1151
+ audit log.
1147
1152
  """
1148
1153
  s = (caller_id or "").strip()
1149
1154
  if not s:
1150
1155
  return ""
1151
- if len(s) <= 6:
1156
+ if not s.startswith("+") or len(s) < 5:
1152
1157
  return "***"
1153
- head = s[:4]
1154
- tail = s[-4:]
1155
- return f"{head}***{tail}"
1158
+ return f"+{_dialling_country_code(s)}***{s[-2:]}"
1159
+
1160
+
1161
+ def _dialling_country_code(e164: str) -> str:
1162
+ """The dialling country code of an E.164 number, "" when it is unknown.
1163
+
1164
+ libphonenumber knows where the country code ends (`+1` vs `+49` vs `+49`
1165
+ inside `+49151…`); a hand-rolled slice cannot. Failing to resolve it only
1166
+ costs the prefix of a display hint, so this never raises.
1167
+ """
1168
+ try:
1169
+ import phonenumbers
1170
+
1171
+ return str(phonenumbers.parse(e164, None).country_code or "")
1172
+ except Exception: # noqa: BLE001 — display helper, never fatal
1173
+ return ""
1156
1174
 
1157
1175
 
1158
1176
  def draft_upload_call_conversions(
@@ -2417,10 +2435,14 @@ def draft_upload_enhanced_conversions_for_leads(
2417
2435
  ),
2418
2436
  "sample_rows": [
2419
2437
  {
2420
- "email_sha256": (r["email_sha256"][:16] + "...")
2421
- if r["email_sha256"] else "",
2422
- "phone_sha256": (r["phone_sha256"][:16] + "...")
2423
- if r["phone_sha256"] else "",
2438
+ # A marker, not a prefix: the sample only has to show that
2439
+ # an identifier was there. 16 hex characters are 64 bits,
2440
+ # which is enough to confirm a guessed address from a word
2441
+ # list — in a preview that lands in a model's context and
2442
+ # in the audit log. The full hashes stay in
2443
+ # apply_only_payload, where the upload needs them.
2444
+ "email_sha256": "sha256:set" if r["email_sha256"] else "",
2445
+ "phone_sha256": "sha256:set" if r["phone_sha256"] else "",
2424
2446
  "conversion_name": r["conversion_name"],
2425
2447
  "conversion_value": r["conversion_value"],
2426
2448
  "conversion_time": r["conversion_time"],
@@ -160,6 +160,12 @@ def _to_python(obj: object) -> object:
160
160
  if type(obj) is not int and hasattr(obj, "name"):
161
161
  return obj.name
162
162
  return obj
163
+ # FieldMask (change_event.changed_fields): its JSON form is one
164
+ # comma-joined camelCase string, which hides the snake_case paths the
165
+ # rest of the API uses. Return the paths as they are.
166
+ descriptor = getattr(obj, "DESCRIPTOR", None)
167
+ if getattr(descriptor, "full_name", "") == "google.protobuf.FieldMask":
168
+ return list(obj.paths)
163
169
  # Repeated fields (headlines, final_urls, etc.)
164
170
  try:
165
171
  return [_to_python(item) for item in obj]