---
name: ga4-audit
description: Runs a read-only audit of one Google Analytics 4 property through the Admin and Data APIs, the official GA4 MCP server or interface exports, and scores it. Covers streams, enhanced measurement, retention, internal, developer and hostname filters, unwanted referrals and cross-domain, key events with counting and values, Google Ads and BigQuery links, custom definitions, channel groups, attribution, consent signals, PII, thresholding, sampling and reporting identity. Returns a scored report, analyst caveats and a change list; never edits the property. Use when asked for a GA4 audit, GA4 health check, GA4 setup review or Google Analytics review, when inheriting a property, before importing key events to Google Ads, when GA4 conversions or the key event rate look too high, purchases lack transaction IDs, staff or test traffic shows in reports, PayPal or your own domain appears as a referral, the BigQuery export nears 1 million events a day, or reports show thresholding or sampling.
license: CC-BY-4.0
compatibility: Scripts need Python 3.9 or later, standard library only. The snapshot script also needs the gcloud CLI to mint a read-only access token. Works without either by reading through the official GA4 MCP server or interface exports.
metadata:
  tier: "free"
  category: "tracking-troubleshooting"
  version: "0.1.0"
  last_reviewed: "2026-11-06"
  status: reviewed
---

# GA4 audit

Reads one Google Analytics 4 property and reports what makes its numbers wrong, collected without permission, lost or unreadable. Each of 26 rules (G01 to G26) is rated PASS, WARN, FAIL, NOT CHECKED or N/A with its evidence, and the report adds a score with coverage, a trust label per report family, analyst caveats and a change list. The audit changes nothing. A finding is a lead until data or an approved test confirms it.

## When to use

Use this skill when:

- Someone asks for a GA4 audit, health check or Google Analytics review, or a property is inherited or handed over.
- Key events are about to be imported into Google Ads, or a warehouse is about to depend on the BigQuery export.
- The key event rate looks implausible, or purchases lack transaction IDs or revenue.
- Staff, test or staging traffic shows in reports, or a payment provider or your own domain appears as a referral.
- The daily export nears 1 million events, or reports show thresholding or sampling.

Use something else when:

- Nobody knows which layer fails (click, consent, tag, server, platform, warehouse): tracking-break-map.
- Unassigned, (not set) or Direct needs a cause and a fix: ga4-unassigned-not-set-diagnosis. This audit only measures the shares.
- The GTM container behind GA4 is suspect (duplicate Google tags, debug_mode parameters, event tags before the Google tag): gtm-container-audit.
- Consent must be proven at runtime across choices and regions: consent-debugging.
- GA4 purchases disagree with the order system order by order: purchase-reconciliation.
- A site-wide audit of tags, pixels, consent behaviour and conversions across vendors from browser captures: tracking-audit.

If a named skill is not installed, use the matching pages in `sources.yaml`. More near misses: [references/reading-sources.md](references/reading-sources.md#other-near-misses).

## Inputs

Read these first if they exist ([assets/example-acme-stack.md](assets/example-acme-stack.md) shows the fields):

- `martech-context/stack.md`: property ID, streams and measurement IDs, every hostname and registrable domain in the journey, payment and sign-in providers, Google Ads customer IDs (active, retired, third party), BigQuery project and dataset, CMP, server senders.
- `martech-context/decisions.md`: history needed in explorations, consent mode per region, reporting identity, attribution and channel group choices, accepted exceptions with dates.
- `martech-context/tracking-plan/`: intended key events with counting and value rules, which events tags already send, must-not-fire conditions.
- `martech-context/metrics.md` and `data-dictionary.md`: which KPIs read which key events (to rank findings), what each custom dimension means.

Then gather:

- The property ID and the read route: the official GA4 MCP server, the snapshot script with the user's own read-only credentials, or interface exports.
- Whether Google Ads imports GA4 key events or uses GA4 audiences, and whether a warehouse depends on the export.
- Sales cycle length in days, whether demographic reports are used, and the order system's order count and revenue for the window (aggregates only).
- Screenshots or transcriptions of the settings no API shows ([assets/ui-settings-intake.csv](assets/ui-settings-intake.csv)).
- Who may run tests that create GA4 events, where (staging or a test property) and with whose approval.

Work in a folder in the user's project, for example `ga4-audit-2026-11-01/`: copy [assets/audit-config-template.json](assets/audit-config-template.json) to `audit-config.json` and `assets/ui-settings-intake.csv` to `ui-settings.csv` there and fill them. Run both scripts from that folder by full path (`python3 <skill folder>/scripts/...`); never write user files into the skill folder. Ask the user to delete the R08 and R15 exports after the audit.

Never ask for access tokens, Measurement Protocol API secrets, service account keys, IP address lists or user emails. An export or a cropped screenshot needs no credentials.

## Procedure

### 1. Fix the baseline and the question

- Record property name and ID, service level, property type, time zone, currency, read route, scopes granted and read time in UTC.
- Data window for reports: 28 complete days ending two days before the run (processing takes 24 to 48 hours), plus the previous 28 days for trend rules.
- Record which decisions the property feeds (bidding, board reporting, warehouse) in `decisions_fed`, and rank findings by those.
- If the property you can read is not the one named, or is a subproperty or roll-up, stop and say so.

### 2. Read the configuration without write access

- Choose route B when the user can mint read-only credentials, route A when the GA4 MCP server is already connected, route C otherwise. Scopes, calls and the snapshot shape: [references/reading-sources.md](references/reading-sources.md).
- Route B: the user runs the gcloud sign-in (browser consent; alternatives if Google rejects it are in reading-sources.md). The agent runs `python3 <skill folder>/scripts/ga4_snapshot.py --property 123456789 --as-of 2026-11-01 --quota-project YOUR_CLOUD_PROJECT --dry-run`, shows every planned call, and reruns with `--out ga4-snapshot.json` once the user agrees. Exit 2: the scope gate stopped; ask for read-only credentials, never a pasted token.
- Route A: list the server's tools first. If any create, update or delete tool is present, the server runs with `--allow-write`: stop and ask the user to restart it without the flag. It cannot read most settings (retention, streams, attribution, identity, BigQuery links, channel groups); transcribe them or use route B.
- Decision rule: an endpoint that returns 403 or 404 makes the rules it feeds NOT CHECKED, with the status code. Never fill a value from memory.

### 3. Pull the data checks

- Run reports R01 to R17 ([references/data-checks.md](references/data-checks.md)) through the script, the MCP `run_report` tool or exports. Keep every response's metadata block and rowCount.
- On route A, never request transactionId, pageLocation, pagePath, pageReferrer, customEvent: or customUser: dimensions through `run_report`: take R08, R14 and R15 from route B, from a route C export passed to the script by path unopened, or from their count-only versions; otherwise mark them NOT CHECKED. One exception: R16 filtered to the root path, limit 1, metadata only.
- Decision rule: percentage thresholds need at least 1,000 sessions, 100 key events or 100 purchases of the relevant kind in the window. Below that, report counts and cap the rule at WARN, labelled low volume.
- Never paste URL values, transaction IDs or custom dimension values into the conversation. Work from counts.

### 4. Capture what no API shows

- Send the user the intake CSV. Each row says where the setting lives (verify the path at run time), the value format and a screenshot reference. Formats: [references/ui-only-settings.md](references/ui-only-settings.md).
- Record internal traffic rule names and traffic_type values only, Measurement Protocol secret counts and owners only, change history by role, not by person.
- Screenshots follow the same rule: the user crops or blurs secret values, IP addresses and people's names and emails before sharing one. mp_secrets rows take count and owner as text, with no screenshot unless the Secret value column is cropped.
- Rows left empty become NOT CHECKED with the reason "UI evidence not supplied".

### 5. Run the rules engine

```bash
python3 <skill folder>/scripts/audit_ga4.py --config audit-config.json --snapshot ga4-snapshot.json \
  --ui ui-settings.csv [--exports DIR] [--tag-coverage coverage.csv] \
  [--bq bq-results.json] [--ads ads-conversions.json] --as-of 2026-11-01 > ga4-audit-report.md
```

- Exit 1 means at least one FAIL; 2 means an input error. `--json` writes the same findings as JSON. `--self-test` checks the engine against [assets/regression-snapshot.json](assets/regression-snapshot.json).
- A rule PASSes only with every primary input read; otherwise it is NOT CHECKED.
- If Python is unavailable, apply [references/rules.md](references/rules.md) by hand and label the report "script not run".

### 6. Review every finding with judgement

Per-rule evidence, thresholds and confirmation steps: [references/rules.md](references/rules.md). The decisions that need the agent:

- FAIL means wrong data now, data without permission, PII, or permanent loss in progress or one click away. Downgrade a FAIL only by citing a dated exception from `decisions.md` (the config's `accepted_exceptions`).
- Each sub-finding scores once, in the most specific rule: form_submit as a key event in G11, not G03; PII values in G24, not G19.
- Developer filter: if it matches 5% or more of events in Testing, debug_mode is on production events. A debug_mode parameter set to false still enables debug mode. Never advise activating the filter until the parameter is gone from production tags: gtm-container-audit finds it, a person with Edit access removes it.
- Internal traffic: a rule's traffic_type value must equal the filter's value. A Testing filter that matches 20% or more of events, or purchases near the property's normal rate, is too broad; do not activate it.
- Auto-marked key events: session_start and six ecommerce steps appear when a linked Google Ads account has personalized advertising on. Check `deletable` before proposing removal; if false, document it and read per-event key event metrics.
- Purchase counted Once per session is a FAIL. That is the legacy default for key events migrated from Universal Analytics goals.
- Unwanted referrals are for third-party processors. An owned, tagged domain belongs in Configure your domains; listing it as an unwanted referral hides the label and keeps the identity break.
- Retention: if `decisions.md` names BigQuery as the long-range source and the daily export runs with no table expiration shorter than the history needed, G04 drops from FAIL to WARN.
- Rate consent configuration against `decisions.md`; never choose a mode.
- Names are not evidence. A filter called Internal traffic, a channel called AI or a dimension called email proves nothing until its value or data does.

### 7. Confirm the top findings

- Every FAIL in G06 to G13, G16, G18, G23 and G24 gets one confirmation from data or an approved test, recorded beside the finding; otherwise label it unconfirmed.
- Filters: read filters already in Testing with R11 after 24 to 36 hours. Putting a filter into Testing is a change list item for a property editor with the measurement owner's approval; the audit never changes filter state.
- Key events and purchases: rerun R01, R08 and R09 on days after a fix. Use Realtime or DebugView only on staging or a test property, with approval.
- Google Ads: the GAQL result listing primary actions per category, then each action's tag trigger or GA4 source event. BigQuery: the coverage query in [references/data-checks.md](references/data-checks.md). Consent: the Consent settings page again 48 to 72 hours after a fix, runtime proof through consent-debugging.
- Live-site tests need explicit approval (Boundaries). An Active developer filter drops debug traffic, so confirmation runs that must appear in reports avoid debug mode.

### 8. Score, report and propose

- Report from [assets/audit-report-template.md](assets/audit-report-template.md), which the script output follows, with confirmation notes added. Scoring and banners: [references/scoring.md](references/scoring.md).
- Change list from [assets/change-list-template.md](assets/change-list-template.md), ordered P1 to P4. Each item states current value, change, reversibility, role needed, test and annotation text. GA4 has no versions to roll back to: [references/change-safety.md](references/change-safety.md).
- Propose updates to `stack.md` (streams, links, hostnames found) and `decisions.md` (open decisions). Write nothing until the user approves.

## Common failure modes

- debug_mode false shipped to production: the Developer traffic filter in Testing matches most events; activating it deletes real data. Detect: G07, R11.
- Internal traffic defined but never filtered: the rule sets office, the filter excludes internal, or the filter sits in Testing for months. Detect: G06.
- Ads personalization inflates key events: session_start becomes a key event and the session key event rate nears 100%. Detect: G11, `custom` false, created the day the link was made.
- Purchase still Once per session after the goal migration. Detect: G12.
- Second owned domain shows as a referral: missing from Configure your domains, or wrongly listed as an unwanted referral. Detect: G09 and G10, outbound clicks to it in R07.
- Staging or a copied theme sends to production, or test orders inflate purchases against the order system. Detect: G08, G13.
- Two streams on one site from a leftover hard-coded snippet: events double, sessions split. Detect: G02, R03.
- BigQuery daily export over 1,000,000 events on standard, or tables that still expire after a sandbox upgrade. Detect: G18, BQ2b, BQ3.

All 34 known failure modes with their detection: [references/rules.md](references/rules.md#failure-mode-catalogue).

## Worked example

Acme Store (fictional), property 123456789, two web streams on www.example.com, checkout on shop.example.net. From the skill 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
```

It ends with `Score 25/100 (not decision-grade), coverage 26/26 rules. 15 FAIL, 8 WARN, 3 PASS. Nothing in GA4 was changed.` Among the findings:

- FAIL G07: the Developer traffic filter in Testing matches 61% of events; activating it would delete most production data.
- FAIL G11: session_start and form_submit are key events (the six ecommerce steps marked the day the Google Ads link was made are WARN). FAIL G12: purchase still counts Once per session from the 2023 goal migration.
- FAIL G18: 22 days over the 1,000,000 daily export limit (peak 1,187,440 in BigQuery, consent-denied pings included); tables are still complete, so the report shows a data loss risk banner.
- FAIL G16: a GA4-imported purchase and a website purchase are both primary in Google Ads.

Walkthrough: [references/worked-example.md](references/worked-example.md). The hand-off report: [assets/example-acme-report.md](assets/example-acme-report.md).

## Acceptance checks

- Baseline: property, service level, time zone, currency, route, scopes granted, read time and data window are in the report. Pass: all present.
- Read-only: the call log shows only allowlisted GET calls and Data API reports, the MCP tool list had no write tool, and no token had analytics.edit. Pass: no write call exists.
- Route A privacy: no `run_report` call requested transactionId, pageLocation, pagePath outside the filtered R16, pageReferrer or customEvent: and customUser: dimensions. Pass: the tool history shows none.
- Every rule: G01 to G26 each show PASS, WARN, FAIL, NOT CHECKED or N/A with a reason. Pass: none missing.
- Coverage and grade: a grade appears only at 70% coverage or more with every applicable core rule (G04, G06, G07, G08, G11, G12, G23, G24) checked. Pass: score, grade, coverage and caps printed together.
- Evidence: every FAIL cites a field value, an aggregated report row or an intake row with a screenshot reference; every PASS rests on its primary inputs. Pass: neither is missing.
- Confirmation: every FAIL in G06 to G13, G16, G18, G23 and G24 has a data recheck or approved test, or the label unconfirmed. Pass: no confirmed finding without its record.
- UI-only settings: every intake row is filled with evidence or listed as not checked with the reason. Pass: none silently skipped.
- Privacy: search the report and change list for email addresses, tokens or secret values and IP addresses. Pass: the search finds none.
- Analyst caveats: retention horizon, sampling horizon in days, thresholding, attribution model and lookbacks, reporting identity, modelled data and time zones are stated. Pass: an analyst can read the numbers without asking.
- Change list: each item has entity, current value, change, reversibility, role needed, test, annotation text and owner. Pass: a person with that role could apply it without a question.
- Context updates: proposed changes to `stack.md` and `decisions.md` are listed. Pass: none written without approval.

## Boundaries

- Read-only. Never create, update, delete, archive, link, unlink, acknowledge, reorder or activate anything in GA4, Google Ads, GTM or BigQuery, even when the identity could. Proposed annotations are text for a human.
- No live side effects without approval: no forms, orders or consent banner clicks on a live site; prefer staging or a test property; list every test record created.
- Decision owners: consent, legal basis and account data sharing, the privacy owner; attribution model, key events and channel definitions, the measurement owner; filters that delete data, a property administrator with measurement owner sign-off.
- Never ask for, print or store tokens, keys, Measurement Protocol secrets, IP values or user emails. Access reviews use counts an administrator supplies.
- Hand-offs:
  - Google Analytics MCP server (official): read tools only, analytics.readonly, never `--allow-write`.
  - Admin API v1beta and v1alpha GET methods and Data API reports through `scripts/ga4_snapshot.py`.
  - Google Ads MCP server (official): only `search`, `get_resource_metadata` and `list_accessible_customers`, with `labels: false` and `assets: false` in its tools_config.yaml, because its mutate tools are on by default.
  - BigQuery MCP server: `execute_sql_readonly`, one SELECT per call, with `execute_sql` and `cancel_job` denied in the client and by IAM policy.
  - gtm-container-audit, consent-debugging, ga4-unassigned-not-set-diagnosis, purchase-reconciliation, event-naming-convention.
  - Approved changes: a person with the role named in each change list item.

## Live docs

Platform settings, limits and menu paths change. Before stating any parameter name, limit, quota or UI path:

- Fetch the current page from the vendor docs MCP server or llms.txt listed in `sources.yaml`, or open the canonical URL.
- If the live page disagrees with this skill, follow the live page and tell the user what changed.
- Cite the URL you used.

Google publishes no llms.txt for these docs; any developers.google.com page is available as Markdown by adding `.md.txt` to its URL, and the Developer Knowledge MCP server covers developer pages but not support.google.com. Most used: the Admin API and Data API references, data retention, data filters, Google tag settings, key events and conversion management, BigQuery export limits and table expiration, consent settings, the Google Ads conversion_action fields (v26), the GA4 MCP repository and the release notes. Checked 2026-11-01 to 2026-11-06; re-check the v1alpha methods, which can change without notice.
