# Worked example: Acme Store (fictional)

## Inputs

- [../assets/example-acme-stack.md](../assets/example-acme-stack.md): the `stack.md` the agent reads first. Property 123456789 (standard, America/New_York, USD); stream "Acme web" (1234567890, G-XXXXXXXXXX); cart and checkout on shop.example.net with the same measurement ID; PayPal as a payment option; Google Ads 123-456-7890 active and 000-000-0000 retired; BigQuery project acme-analytics.
- [../assets/example-acme-audit-config.json](../assets/example-acme-audit-config.json): the config built from it. Needs 14 months of history, a 45-day sales cycle, GA4 key events imported to Google Ads, revenue in the warehouse, and the order system's aggregate count for the window (9,350 orders).
- [../assets/example-acme-snapshot.json](../assets/example-acme-snapshot.json): what route B collected on 2026-11-01 (trimmed). A second stream, "Acme web (2023 rebuild)" (2345678901, G-XXXXXXXX), is still fed by a hard-coded snippet in the theme.
- [../assets/example-acme-ui-settings.csv](../assets/example-acme-ui-settings.csv): the intake with cropped screenshot references.
- [../assets/example-acme-bq-results.json](../assets/example-acme-bq-results.json) and [../assets/example-acme-ads-conversions.json](../assets/example-acme-ads-conversions.json): the read-only BigQuery and GAQL results.

## Command

From the skill folder (a real audit runs from its own working folder):

```bash
python3 scripts/audit_ga4.py --config assets/example-acme-audit-config.json \
  --snapshot assets/example-acme-snapshot.json --ui assets/example-acme-ui-settings.csv \
  --bq assets/example-acme-bq-results.json --ads assets/example-acme-ads-conversions.json \
  --as-of 2026-11-01
```

Exit code 1 (FAILs present). The last line:

```text
Score 25/100 (not decision-grade), coverage 26/26 rules. 15 FAIL, 8 WARN, 3 PASS. Nothing in GA4 was changed.
```

A few output lines, shortened:

```text
> Data loss risk: G18 22 days above the 1,000,000 standard daily export limit (peak 1,187,440 on 2026-10-27 in BigQuery, consent-denied pings included); ... at risk: daily tables still complete
FAIL Developer traffic filter in Testing matches 61.0% of events: debug_mode is on production events ...
FAIL session_start is a key event and it fires in nearly every session (marked the day the Google Ads link was created)
FAIL purchase is counted Once per session, so repeat purchases in a session are dropped ...
WARN GA4 records 9,800 purchases against 9,350 orders in the order system for the window (104.8%) ...
FAIL PURCHASE: GA4-imported "Acme Store - GA4 (web) purchase" and native or uploaded "Website purchase" are both primary ...
WARN sampling horizon 8 days at the default exploration limit: explorations over more than 8 days sample (average 1,218,734 events a day)
```

The full hand-off report, with the agent's confirmation notes added: [../assets/example-acme-report.md](../assets/example-acme-report.md).

## What the agent proposes for each finding

Each proposal is a change list item for the role named in it; the audit changes nothing in GA4, GTM or the site.

FAIL:

- G02: www.example.com splits 85% and 15% between the two streams. Ask the site owner to remove the hard-coded snippet; the 2023 rebuild stream is not in `stack.md`. Confirm with one fresh page load: one collect request per page, to G-XXXXXXXXXX only.
- G04: event retention is two months against a 14-month need, and `decisions.md` does not name BigQuery as the long-range source. Raising retention is reversible and urgent; every month waited is lost from explorations.
- G06: the rule "Office" sets office, the Active filter excludes internal. Staff traffic is in every report. Fix the rule value, not the filter, so the Testing evidence stays comparable.
- G07: the Developer filter in Testing matches 61% of events. Hand to gtm-container-audit to find the debug_mode parameter; never activate the filter first (CR-2).
- G08: staging.example.com sends 2.1% of events and 380 key events to production. Remove the production tag from staging or give staging its own property.
- G09: paypal.com holds 3.2% of key event credit and is not an unwanted referral; shop.example.net is a referral with 3.4% of sessions.
- G10: example.net is missing from Configure your domains; 12% of click events are outbound clicks to it. Add it; do not add it to unwanted referrals. Confirm with a fresh click across the boundary: `_gl` on arrival, same client ID.
- G11: session_start and form_submit are deletable key events (FAIL); the six ecommerce steps marked the day the Google Ads link was created are WARN. Check GAQL before unmarking (CR-3).
- G12: purchase is Once per session since the 2023 goal migration (CR-4). generate_lead runs at 1.33 events per session, which goes to G14's create rule.
- G13: 6.2% of purchases lack a transaction ID and 2.4% of IDs repeat. GA4 counts 104.8% of the 9,350 orders the order system reports, and gross item revenue sits 3.0% under gross purchase revenue, so the purchase value likely includes shipping. Hand order-level checks to purchase-reconciliation.
- G14: a create rule makes generate_lead from the thank-you page while GTM already sends it. Remove the rule after confirming the GTM event fires only on server acceptance.
- G16: a GA4-imported purchase and a website purchase are both primary in Google Ads, and both use a 30-day click-through window against a 45-day sales cycle. The Google Ads owner keeps one primary; the export scope also needs choosing.
- G18: exported events passed 1,000,000 on 22 of 28 days; the peak, 1,187,440 on 2026-10-27, is the BigQuery count, the conservative figure: cookieless pings are exported (answer 9358801), and Google does not say whether the limit counts them. Daily tables are still complete, so the report shows a risk banner, not data loss. Options for the owner: exclude low-value events from the export (G17 then rechecks key events), streaming export, or 360.
- G23: Acme web shows ad_personalization not received with 38% EEA traffic while Google Ads personalization is on. Runtime proof through consent-debugging; recheck the Consent settings page 48 to 72 hours after the fix.
- G24: 312 page views carry an email in the newsletter confirmation URL, and the user-scoped dimension user_email holds email-like values. Privacy owner first (CR-1); the dimension is archived only after the privacy owner and the data owner agree. Account data sharing with Google products & services is on with no recorded decision, a question for the privacy owner.

WARN:

- G03: form interactions on while GTM sends generate_lead. Turn the option off after G11's form_submit decision.
- G15: a link to the retired account 000-000-0000; 18% of google / cpc sessions lack a customer ID (check auto-tagging on the active account).
- G19: event-scoped dimensions at 41 of 50, two of them IDs (session_id, client_id).
- G20: experiment_variant is only (not set) for 28 days; session_id, client_id and user_email each exceed 500 values a day.
- G21: the other key events lookback is 30 days against a 45-day sales cycle, changed on 2026-07-09 without an annotation (proposed annotation in the report).
- G22: Unassigned holds 7.8% of sessions in both the default group and the primary group "Acme channels"; in the primary group, AI Assistants sits after Referral, so assistant visits that arrive with medium referral land in Referral. Causes go to ga4-unassigned-not-set-diagnosis.
- G25: the sampling horizon is 8 days. State it in every analysis that uses explorations.
- G26: the aggregate identifier diagnostic has been open since 2026-08-02 with no owner.

PASS: G01 (time zone and currency match `stack.md` and Google Ads), G05 (Blended identity documented on 2024-02-20, Blended reports include estimated user data, User-ID not expected), G17 (daily export of the production stream, nothing excluded).

## Score

Areas: A 50, B 50, C 0, D 0, E 25, F 50, G 50, H 50, I 0, J 50. Weighted sum 24.5, rounded half up to 25. Not decision-grade; G23 and G24 would cap it at material issues anyway. The calculation is in [scoring.md](scoring.md#worked-calculation-acme-store).

## Regression fixture

[../assets/regression-snapshot.json](../assets/regression-snapshot.json) holds 45 cases with expected statuses: this worked example (read from the asset files), a healthy property where all 26 rules PASS, an edge case (MCP snake_case keys, a 360 property, low volume, route C exports, data loss in progress, a partial audit), an app-only property (13 rules N/A, one area out of the score), and 41 small cases, most built from these by patching a few inputs: primary evidence missing, each Manage default consent branch, an empty EEA share, dataset and per-table expiration, every `earliest_expiry` shape, BigQuery standing in for retention, BigQuery counts over the limit with complete or missing tables, counts as strings, BQ3 without BQ1, streaming-only export, no link, a route C export alone, the order system and item checks, Google Ads counting and windows, lead actions with and without a mapped outcome, modelling and data sharing, the primary channel group, a truncated R11, the route A count-only shapes, the MCP metadata shape, internal traffic with no rule, an address or a spelling variant in the PII probe, a share under the G13 line, count gaps either side of the BigQuery update window, and a rule that errors. After changing the script, run from the skill folder:

```bash
python3 scripts/audit_ga4.py --self-test
```

Expected: `45 of 45 cases match`, exit 0. The rule error case prints one `rule G23 error` line on stderr on purpose. The edge, Acme and PII probe cases also check that no probe value (a token, a name, a transaction ID, an email) appears in any finding, reason or partial note.
