# Check catalogue

Seventy-one checks in ten domains. Each check names the evidence that settles it, what PASS, WARN and FAIL look like, how to detect it, how to confirm it and where to hand it off. The worksheet [../assets/audit-checklist.csv](../assets/audit-checklist.csv) carries the same IDs, severities, gates and levels, and [scoring.md](scoring.md) explains how they are weighted.

## How to read a check

- The first line gives severity (critical, high, medium or low), default gate (S1 to S4) and the access level that can run it: L1 public evidence, L2 read access, L3 approved test transactions.
- Evidence: what settles the check. Detect: the script section or platform read that finds it. Confirm: the reproduction that turns a lead into a finding.
- "Not used as FAIL" means the check rates PASS or WARN only, and the FAIL belongs to the check it names.
- Every FAIL and every S1 or S2 WARN needs one reproduction in a fresh context before it is reported as confirmed.

## Statuses

- PASS: evidence seen and correct.
- WARN: a plausible defect that needs a decision or a confirmation, or a minor deviation.
- FAIL: evidence seen and wrong.
- NOT_CHECKED: applicable but not verified (no access, no approval, no evidence). Needs a reason. Lowers coverage, never health.
- NA: not applicable to this site, for example no server routes. Needs a reason.
- One status per check: the worst result across every capture, run, region, landing route and platform read that tested it. INFO lines are evidence, not statuses.
- A PASS needs every required run: 3 of 3 for consent, SPA and cross-domain cases.
- A FAIL that does not reproduce becomes a WARN with "unconfirmed" in notes. A finding from a platform read is confirmed by a second read or the owner's UI screenshot, not by a browser reproduction.

## Beside the other audits

- Consent: this file owns runtime behaviour (CON-01 to CON-07). gtm-container-audit R01, R02 and R16 own the container configuration, and ga4-audit G23 reads what GA4 received. A PASS in one proves nothing for the others, and only a FAIL or a clean CON run says what visitors got.
- GA4 key events: ga4-audit G11 and G12 own the list and counting. CNV-05 rates only whether a non-outcome reaches bidding, so a key event can be a G11 FAIL and a CNV-05 WARN. gtm-container-audit R19 owns exact-name matches in tags.
- Purchases: CNV-03 judges the requests in your captures, ga4-audit G13 the property's totals against orders, gtm-container-audit R12 and R29 the tag setup, tracking-plan-audit D07 and D12 the rows against the plan, purchase-reconciliation the order-level gaps.
- A check here that one of those skills has already answered cites its report as evidence instead of repeating the work. Status words and scores: [scoring.md](scoring.md#beside-the-other-audits).

## Gates

- S1: Permission and privacy: data sent or stored beyond the visitor's choice, personal data to a vendor, hashed match data without consent or decision, server routes ignoring consent.
- S2: Bidding and revenue integrity: conversions counted twice, counted for attempts, wrong value or currency, non-outcomes used for bidding, missing dedup keys on dual routes.
- S3: Data loss: missing events or conversions, lost click IDs or UTMs, broken cross-domain sessions, wrong SPA counts, CRM capture gaps.
- S4: Configuration risk and hygiene: unexpected or retired IDs without privacy impact, settings drift, unregistered definitions, ownership and monitoring gaps.
- A finding may raise its gate with a written reason, for example an unexpected vendor that the CMP does not disclose becomes S1. Only the privacy owner lowers an S1.

Vendor facts in this file were checked between 2026-11-01 and 2026-11-06. GA4 collect, Meta pixel, Google Ads, UET `asc` and OpenAI Ads pixel request parameter names, and gcs and gcd decoding, are community knowledge rather than documented contracts: confirm them in Tag Assistant, DebugView or the vendor's helper before quoting them.

## INV: Tag and pixel inventory

Which tags and pixels run, who loads them, and are they the expected ones? Domain weight 8.

Without gtm-container-audit: INV-08 light path. Read the published gtm.js, list tag functions and destination IDs, and mark paused tags, exception triggers and workspaces as not checked.

### INV-01 Only expected destinations receive data

- high, gate S4, L1
- Evidence: har-audit inventory across every capture; published gtm.js read for IDs in tags that did not fire.
- PASS: Every ID that received a request is in stack.md or confirmed by its owner.
- WARN: An unexpected loader or configuration ID with no event requests, or an ID only in the published container.
- FAIL: An unexpected ID receives event requests.
- Detect: har-audit --expect-ids (inventory marks unexpected IDs) and the unclassified-host list.
- Confirm: Ask the owner; use the loader (INV-04) to find who added it.
- Hand-off: gtm-container-audit R11 for IDs inside the container.
- Note: Raise the gate to S1 when the vendor is not disclosed in the CMP.

### INV-02 Retired IDs receive nothing

- high, gate S4, L1
- Evidence: har-audit with --retired-ids; published gtm.js read.
- PASS: No retired ID appears in any request.
- WARN: A retired ID only in the published container or a paused tag.
- FAIL: A retired ID receives requests.
- Detect: har-audit --retired-ids.

### INV-03 One loader and one base event per destination

- high, gate S2, L1
- Evidence: har-audit duplicate section per HAR page.
- PASS: Each container and destination ID loads once per page and sends one base page event per page load.
- WARN: A second loader that sends nothing.
- FAIL: Two GTM containers for one site role, a hard-coded Google tag plus a GTM Google tag for one ID, two base pixel inits for one pixel, or two page_view requests for one ID on one page load.
- Detect: har-audit duplicate loaders and base events per page.
- Confirm: One page load in a fresh context, count requests per ID.

### INV-04 Every vendor's loader is known

- medium, gate S4, L1
- Evidence: Chrome HAR initiator chains (_initiator), page source, published container.
- PASS: Every vendor has a known loader (GTM container, page source, CMS or app plugin, tag gateway, server container) and an owner.
- WARN: A vendor loaded outside GTM with no documented owner, or no initiator data (Firefox, Safari or Playwright HAR).
- FAIL: Not used as FAIL; feeds INV-03 and CON findings.
- Detect: har-audit loaded-by column.

### INV-05 Expected tags present on every page type

- high, gate S3, L1
- Evidence: har-audit inventory per HAR page for landing, product or pricing, form, checkout, confirmation and app entry.
- PASS: The destinations the plan expects are present on each page type in scope.
- WARN: not used.
- FAIL: An expected destination is absent on a page type where its events are planned, for example no GA4 on the checkout return page.
- Detect: har-audit inventory per page.

### INV-06 Other collecting vendors are inventoried

- medium, gate S4, L1
- Evidence: har-audit vendor categories (replay, chat, heatmap, testing, embedded media) and the unclassified third-party hosts it lists with request counts, methods and loaders.
- PASS: Each collecting vendor beyond ads and analytics, each own-host collector and each unclassified host that receives data is listed with purpose, consent category and owner.
- WARN: A collecting vendor, own-host collector candidate or unclassified host with no category or owner (har-audit INV-06 WARN in accept and journey runs).
- FAIL: Not used as FAIL here; collection before consent fails under CON-02 or CON-03.
- Detect: har-audit categories and the unclassified-host list. Pass the scope sheet's first-party collection endpoints with --collector-hosts so they join the inventory. An owned-host request on a known collector path (Adobe /b/ss/, Snowplow, Segment, PostHog, a /tr request with id and ev) raises a candidate WARN, and other own-host requests with parameters or a body are listed under not checked.

### INV-07 Container loads with its GTM- ID

- medium, gate S4, L1
- Evidence: Loader requests in the HAR; Tag Assistant.
- PASS: Each GTM container loads through gtm.js with its GTM- ID, or a G- or AW- load is intentional and only Google tags are expected.
- WARN: not used.
- FAIL: A container is loaded with a G- or AW- ID through /gtag/js or /gtag/destination while non-Google tags in it are needed.
- Detect: har-audit loader list; manual review.
- Source: GTM release notes, 9 July 2026: containers loaded with product-specific IDs permit only Google-provided tags and variables.

### INV-08 Container rules reviewed

- medium, gate S4, L2
- Evidence: gtm-container-audit report on the live version.
- PASS: No open FAIL in consent settings, duplicate Google tags, retired tags or conversion triggers, or each FAIL is mapped to a check here.
- WARN: Only the light path ran (published gtm.js read).
- FAIL: Not used as FAIL; map container FAILs to the matching check in this catalogue.
- Detect: gtm-container-audit.
- Hand-off: gtm-container-audit.
- Note: Light path: list tag functions and destination IDs in gtm.js; paused tags, exception triggers and unpublished workspaces are invisible there.

## CON: Consent behaviour

Does the visitor's choice control what is sent and stored? Domain weight 15.

Without consent-debugging: run the three scenarios here in each region you can reach by IP egress and mark the rest of CON-08 not checked. Without server-side-tracking-debug (planned): server container Preview and host logs for one reject run and one accept run (CON-07).

### CON-01 Consent default set early, completely, by one owner

- critical, gate S1, L1
- Evidence: Tag Assistant Consent tab on the first event; consent parameters of the first Google request; gtm-container-audit R02.
- PASS: One owner sets ad_storage, analytics_storage, ad_user_data and ad_personalization before any tag reads storage, per region, in 3 of 3 runs including one throttled.
- WARN: not used.
- FAIL: No default, a missing signal, a default after the first tag, or two owners writing different defaults.
- Detect: Tag Assistant; har-audit prints the first Google request's gcd (decoding is community knowledge).
- Note: The agent never signs in to Tag Assistant; a person runs it in a test profile and shares the Consent tab. Without it, judge from the gcd of the first Google request in each run and `google_tag_data.ics` read by consent-debugging's consent-snapshot.js (undocumented, a supplement only), and say so in notes. Two owners can include the Google tag's "Set consent mode override" setting.
- Hand-off: consent-debugging.

### CON-02 No choice sends nothing optional

- critical, gate S1, L1
- Evidence: har-audit --scenario no-choice per page type; cookie names (never values) against the scope sheet's essential list.
- PASS: In opt-in regions, zero non-essential vendor event requests and no non-essential cookies while the banner is ignored, across one internal navigation.
- WARN: A vendor library file loads with no event request, or an unclassified third-party host or own-host collector candidate receives parameters or a body (classify it, then rate it by hand).
- FAIL: Any non-essential vendor event request or cookie before a choice.
- Detect: har-audit CON-02 and the unclassified-host list; pass --essential-cookies so unknown cookie names are judged, and --collector-hosts so first-party collection endpoints are judged like any vendor.
- Confirm: Three runs, one throttled.
- Note: In a region whose documented model grants signals by default (opt-out), pass --granted-defaults; requests those grants allow before a choice print as INFO for review. After reject nothing is allowed.
- Note: Microsoft UET is judged against the documented pattern: basic loads UET only after a grant; advanced fires it before a choice with ad_storage denied, as Microsoft recommends (UET consent mode page, checked 2026-11-02). With `--uet-mode advanced`, denied-state beacons (`asc=D`) pass and `asc=G` or no `asc` fails; `asc` is read from bat.js, not documented, so confirm it in UET Tag Helper.
- Note: The OpenAI Ads pixel collects by default unless `oaiq("consent", false)` runs before init (OpenAI measurement pixel docs).

### CON-03 Reject sends nothing optional and the journey still works

- critical, gate S1, L1
- Evidence: har-audit --scenario reject; essential journey run (L3 for submissions).
- PASS: After reject all, zero non-essential vendor event requests or cookies across two pages, and the essential journey (form, checkout, login) completes.
- WARN: An unclassified third-party host or own-host collector candidate receives parameters or a body after reject (classify it, then rate it by hand).
- FAIL: Any such request after reject, or the essential journey breaks after reject.
- Detect: har-audit CON-03 and the unclassified-host list, with --collector-hosts as under CON-02.
- Note: A person runs any login step, or the agent uses only a test account created under the approved signup. Never ask for a password.
- Note: In opt-out regions, the reject case is the CMP's opt-out of sale or sharing, run once more with Global Privacy Control on ([capture-protocol.md](capture-protocol.md#scenarios)). Where the documented model shows no banner prompt, test the opt-out mechanism the decision names (link or GPC) as the reject case, or mark CON-03 NOT_CHECKED with the reason; run no-choice with --granted-defaults set to the documented defaults.

### CON-04 Google tags follow the documented mode

- high, gate S1, L1
- Evidence: har-audit consent parameters on Google requests per scenario; cookie names; the `url_passthrough` and `ads_data_redaction` states read from the published container or Tag Assistant (consent-debugging's evidence-and-signals has the proofs), checked against the scope sheet; the UET consent pattern where Microsoft Ads runs.
- PASS: Basic: no Google measurement requests before a grant. Advanced: only denied-state requests (gcs G100) and no _ga, _ga_*, _gcl_* or FPID cookies before a grant. URL passthrough and ads data redaction match the scope sheet.
- WARN: Advanced documented but no denied-state requests seen (modelling inputs lost; not a privacy fault), or URL passthrough set differently from the scope sheet.
- FAIL: Requests or cookies beyond the documented mode (granted state before a choice, or requests while basic is documented), or ads data redaction documented on while Ads or Floodlight requests carry the click ID under denied ad_storage.
- Detect: har-audit gcs decoding (G1xy: x ad_storage, y analytics_storage; community knowledge, confirm in Tag Assistant).
- Source: Google consent guide (url_passthrough carries ad click information in URL parameters while ad_storage is denied; ads_data_redaction redacts ad click identifiers on Google Ads and Floodlight requests only while ad_storage is denied, checked 2026-11-02); Simo Ahava consent mode v2 guide.

### CON-05 Accept fires each consented destination once

- high, gate S3, L1
- Evidence: har-audit --scenario accept, base events per page per destination.
- PASS: After accept, each consented destination sends its base page event exactly once on the same page, and later pages carry granted state.
- WARN: A second base event on the accept page (blocked tag plus update trigger; the FAIL belongs to INV-03), Google requests after accept that keep a denied gcs, or an expected ID silent on a page where its events may not be planned.
- FAIL: A consented destination never fires on the accept page (first page lost).
- Detect: har-audit --scenario accept with --expect-ids (zero base events FAIL; a second base event shows as CON-05 WARN plus INV-03 FAIL; Google requests after accept that still carry a gcs other than G111 WARN, because the consent update never reached the Google tags).
- Note: Mark CON-05 NA with the decision reference when the documented model has no accept control.

### CON-06 Consent interaction never triggers business events

- high, gate S2, L1
- Evidence: har-audit conversion events inside no-choice, reject and accept captures of non-conversion pages.
- PASS: No purchase, lead or signup request in any consent capture.
- WARN: not used.
- FAIL: A conversion request follows a banner interaction.
- Detect: har-audit CON-06.
- Source: gtm-container-audit R06 (purchase pixel on the bare consent update event).

### CON-07 Server routes honour consent

- critical, gate S1, L2
- Evidence: Server container Preview or host logs for one reject run and one accept run.
- PASS: The server container receives the consent state and sends no vendor request (or only the documented cookieless form) for denied visitors.
- WARN: not used.
- FAIL: Outgoing vendor requests, for example Conversions API, for a visitor who rejected.
- Detect: Manual, server Preview or logs.
- Hand-off: server-side-tracking-debug (planned); fallback server Preview and host logs.
- Note: No server access means NOT_CHECKED with the reason, never PASS.

### CON-08 Full consent matrix run or scheduled

- medium, gate S4, L2
- Evidence: consent-debugging write-up.
- PASS: consent-debugging ran its minimum set (regions, partial choices, returning visitor, withdrawal) with a recorded verdict.
- WARN: Only this skill's three scenarios ran.
- FAIL: Not used as FAIL.
- Detect: consent-debugging.
- Note: NOT_CHECKED when deferred, with owner and date.

## EVT: Events against the plan

Do events match their contracts in name, parameters, types and limits? Domain weight 12.

Without tracking-plan-builder: derive the expected events from the current setup, label them derived, not approved, and cap EVT checks at WARN. Without tracking-plan-audit: check plan conformance in Mixpanel, Amplitude, PostHog or Segment by hand from the inventory and their event lists, and mark the deeper reconciliation not covered. Without mobile-attribution-fundamentals: native app SDK events stay out of this audit, so list them under Not checked.

### EVT-01 Planned events fire with exact names

- high, gate S3, L1
- Evidence: Journey captures; GA4 DebugView; platform test tools.
- PASS: Every planned event on the tested journeys reaches each planned destination with the exact planned name.
- WARN: not used.
- FAIL: A planned event is missing or renamed (for example signUpComplete instead of sign_up).
- Detect: har-audit --scenario journey with --plan-events (tier 1 missing FAIL, other tiers WARN). It checks GA4 names only and skips plan rows with status proposed; check other destinations' event names from the inventory, or hand off to tracking-plan-audit.

### EVT-02 Required parameters present with planned types

- high, gate S3, L1
- Evidence: har-audit --plan-properties on journey captures.
- PASS: Required parameters present with planned types and allowed values: value numeric, currency three upper-case ISO 4217 letters, IDs non-empty, enums inside the list.
- WARN: A violation on a non-conversion event.
- FAIL: A violation on a tier-1 conversion event (raise gate to S2).
- Detect: har-audit EVT-02 with --plan-events and --plan-properties (tier 1 FAIL, other tiers WARN, from risk_tier).

### EVT-03 GA4 collection limits respected

- medium, gate S4, L1
- Evidence: har-audit GA4 event parameters.
- PASS: Event and parameter names at most 40 characters, at most 25 parameters per event (100 on Analytics 360), values at most 100 characters (page_title 300, page_referrer 420, page_location 1,000).
- WARN: Values over the length limit; confirm in DebugView what arrived.
- FAIL: More than the limit of parameters or a name over 40 characters on a planned event.
- Detect: har-audit EVT-03 (pass --ga4-param-limit 100 for an Analytics 360 property).
- Source: GA4 help 9267744, checked 2026-11-01.

### EVT-04 Ecommerce data cleared and current

- medium, gate S2, L1
- Evidence: Data layer snapshot (capture-protocol snippet) and the purchase payload.
- PASS: Each ecommerce push is preceded by ecommerce null, and items match the action.
- WARN: No clear push, but payloads correct in the tests.
- FAIL: A purchase carries items or values from an earlier push.
- Detect: Manual data layer snapshot; har-audit items per event.

### EVT-05 Page context current on every event

- medium, gate S4, L1
- Evidence: har-audit stale-context check.
- PASS: Every event after a route change carries the new page location and title.
- WARN: Events carry the previous route's location or title.
- FAIL: Not used as FAIL.
- Detect: har-audit EVT-05.

### EVT-06 No unplanned or legacy duplicate events

- low, gate S4, L2
- Evidence: GA4 run_report eventName with eventCount for the last 28 complete days.
- PASS: Every event name maps to the plan or to automatically collected and enhanced measurement events.
- WARN: Case variants (Purchase and purchase), legacy names or events from removed features.
- FAIL: Not used as FAIL.
- Detect: GA4 MCP read.

### EVT-07 Reporting definitions registered

- low, gate S4, L2
- Evidence: get_custom_dimensions_and_metrics against the parameters the plan reports on.
- PASS: Parameters used in reports are registered.
- WARN: A reported parameter is unregistered, or high-cardinality IDs are registered and create (other) rows.
- FAIL: Not used as FAIL.
- Detect: GA4 MCP read.

## CNV: Conversions and values

Is each conversion a real outcome, counted once, with the right value, and used correctly for bidding? Domain weight 20.

Without purchase-reconciliation: the CNV-10 order-ID sample with its thresholds; mark causes unexplained. Without tracking-break-map: compare CRM-accepted leads with analytics per completed day (CNV-11). Without platform-number-reconciliation (planned): align conversion action, date basis, time zone and window before comparing, and never call a by-design gap a defect.

### CNV-01 Conversions fire on the outcome, never the attempt

- critical, gate S2, L3
- Evidence: Approved negative tests (validation error, rejected payment, abandoned payment, double click) with HAR and platform test tools. For an embedded scheduler or form, the outcome is the vendor's documented success message to the parent page (for example a postMessage event), not the click that opens the embed.
- PASS: Only the confirmed outcome sends conversions; negative cases send none.
- WARN: not used.
- FAIL: A conversion is sent for an attempt: submit click, form start, checkout view, failed payment or the click that opens an embed.
- Detect: har-audit conversion requests per journey step and its embedded-frame list; gtm-container-audit R06 for triggers.
- Note: No approval for test submissions means NOT_CHECKED.

### CNV-02 One conversion per outcome

- critical, gate S2, L3
- Evidence: Reload, back, double-click and revisit tests; har-audit duplicates.
- PASS: No second conversion reaches any destination, and conversions carry the outcome ID.
- WARN: A second GA4 purchase with the same transaction_id (GA4 deduplicates web purchases with the same ID; other destinations may not).
- FAIL: A second conversion that the destination cannot deduplicate (no or different ID, Meta browser-only duplicates, Google Ads without order ID).
- Detect: har-audit CNV-02.

### CNV-03 GA4 purchase carries the order ID

- critical, gate S2, L1
- Evidence: har-audit purchase payloads; BigQuery query in platform-reads.
- PASS: Every GA4 purchase carries a non-empty transaction_id equal to the order ID, stable across reload and any server copy.
- WARN: not used.
- FAIL: Missing or empty transaction_id, or a cart or placeholder ID instead of the order ID.
- Detect: har-audit CNV-03.
- Source: GA4 help 12313109: GA4 deduplicates all purchases with transaction_id="" together, and transaction ID deduplication works for web streams only. A purchase with no transaction_id cannot be deduplicated or joined to an order.

### CNV-04 Value and currency match the order

- high, gate S2, L3
- Evidence: Test order record against each destination's payload; har-audit --order-value and --order-currency.
- PASS: Each destination's value and currency match the test order under the documented value rule. Google's GA4 ecommerce guide computes value as the sum of price times quantity and sends tax and shipping as their own parameters (checked 2026-11-01).
- WARN: not used.
- FAIL: Tax, shipping or another currency inside a value the rule excludes, minor units sent as major, or value missing.
- Detect: har-audit CNV-04 with --order-value and --order-currency (GA4 judged; other destinations printed for comparison).

### CNV-05 Key events and primary actions are outcomes

- high, gate S2, L2
- Evidence: GA4 key events (Admin, or keyEvents by eventName); GAQL conversion_action list.
- PASS: GA4 key events and Google Ads primary actions match the plan's outcomes.
- WARN: A non-outcome is a GA4 key event (Google can mark some events automatically when a linked Ads account has personalization on). ga4-audit G11 rates the same key event from the property's side and can FAIL it; this check stays WARN unless bidding uses it.
- FAIL: A non-outcome (page_view, session_start, add_to_cart, scroll) is primary for bidding.
- Detect: GA4 MCP and Google Ads MCP reads.

### CNV-06 One primary conversion action per outcome

- critical, gate S2, L2
- Evidence: GAQL: conversion_action.primary_for_goal, type, category, origin, status.
- PASS: Each outcome has exactly one primary action across website tag, GA4 import, offline import and integrations.
- WARN: not used.
- FAIL: Two primary actions count the same outcome, for example WEBPAGE and GOOGLE_ANALYTICS_4_PURCHASE both primary in category PURCHASE.
- Detect: Google Ads MCP read.

### CNV-07 Counting matches the outcome

- medium, gate S2, L2
- Evidence: GAQL conversion_action.counting_type.
- PASS: Lead actions count ONE_PER_CLICK; purchase actions count MANY_PER_CLICK.
- WARN: A lead action counts every conversion.
- FAIL: Not used as FAIL.
- Detect: Google Ads MCP read.

### CNV-08 Revenue actions use transaction values

- high, gate S2, L2
- Evidence: GAQL value_settings.always_use_default_value and default_value; har-audit values across test orders.
- PASS: Revenue actions use the transaction value; values differ across test orders.
- WARN: not used.
- FAIL: A revenue action always uses the default value, or the payload sends a constant.
- Detect: Google Ads MCP read; har-audit.

### CNV-09 Action status and upload diagnostics healthy

- high, gate S3, L2
- Evidence: GAQL conversion_action.status, customer.conversion_tracking_setting.conversion_tracking_status, offline_conversion_upload_conversion_action_summary; UI status tooltip.
- PASS: Every action an active campaign uses is recording within 7 days or, for an action that normally records under one conversion a week, its last conversion falls within its usual interval and matches CRM outcomes; upload summaries show no alerts.
- WARN: Seen, but no conversions in the expected window, pending a CRM comparison.
- FAIL: An action used for bidding shows the never-seen or stopped state, or upload alerts persist.
- Detect: Google Ads MCP read.
- Note: Status labels are changing in the UI; read the tooltip and judge by meaning: never seen, stopped, or seen with no recent conversions. Take the usual interval from 90 days of conversions by conversion date.

### CNV-10 Purchase order-ID sample inside thresholds

- high, gate S3, L2
- Evidence: Order export against GA4 transaction IDs for 7 to 28 complete days.
- PASS: Unexplained gap at most 2 percent of eligible orders, duplicate flags at most 0.5 percent, value mismatches at most 1 percent.
- WARN: Gap above 2 and up to 5 percent.
- FAIL: Gap above 5 percent, duplicates above 0.5 percent, or value mismatches above 1 percent (raise gate to S2 for duplicates and values).
- Detect: Manual join or purchase-reconciliation worksheet.
- Hand-off: purchase-reconciliation.
- Source: purchase-reconciliation defaults.
- Note: Under 200 eligible orders, mark provisional or extend the window.

### CNV-11 Lead outcomes counted once

- high, gate S3, L2
- Evidence: CRM-accepted leads against GA4 generate_lead and ad lead conversions on the same completed days, or 5 approved test leads.
- PASS: Events per lead at most 1.0 per destination and coverage inside its 28-day band, or 5 of 5 test leads produce exactly one event per destination.
- WARN: not used.
- FAIL: Events per lead above 1.0, coverage outside the band for 2 or more completed days, or a test lead with zero or two events.
- Detect: Manual comparison; har-audit for test leads.
- Hand-off: tracking-break-map when outside the band.

### CNV-12 Platform-side rules do not duplicate coded conversions

- medium, gate S2, L2
- Evidence: Meta ads_pixel_event_read and ads_get_customconversions; Google Ads WEBPAGE_CODELESS actions; URL-rule conversions on LinkedIn and Microsoft; GA4 created or modified events.
- PASS: No platform-side rule duplicates a coded conversion or fires on an attempt.
- WARN: not used.
- FAIL: A click- or URL-based rule creates a conversion the code also sends, or fires on an attempt.
- Detect: Meta Ads MCP and Google Ads MCP reads; UI for the rest.

## XDS: Cross-domain and SPA

Do identity and sessions survive domain changes, and do SPA page views match screens shown? Domain weight 8.

Without cross-domain-and-spa-qa (planned): the XDS checks here with the route fixture and three fresh-click runs.

### XDS-01 Client ID survives every domain boundary

- high, gate S3, L1
- Evidence: har-audit cross-domain section with --own-domains; three fresh-click runs.
- PASS: GA4 client ID identical before and after every boundary in 3 of 3 runs.
- WARN: not used.
- FAIL: The client ID changes at a boundary.
- Detect: har-audit XDS-01.
- Note: Linker parameters expire after two minutes; test with fresh clicks.

### XDS-02 Session continues across boundaries

- high, gate S3, L1
- Evidence: har-audit session ID and session-start flags at boundaries.
- PASS: Same session ID, no session start and no own-domain referrer after each boundary inside the session timeout.
- WARN: not used.
- FAIL: A new session or self-referral at a boundary.
- Detect: har-audit XDS-02.

### XDS-03 Third-party returns keep the session

- high, gate S3, L1
- Evidence: har-audit referrer and session flags on return pages; GA4 sessionSource report for processor hosts.
- PASS: Returns from payment, booking or sign-in hosts keep the session; untaggable processors are listed as unwanted referrals.
- WARN: not used.
- FAIL: The return starts a session with the third party as referrer, or the processor appears as a session source.
- Detect: har-audit XDS-03 judges every return from a host that sent no GA4, whether a third party or an owned host that cannot be tagged (a hosted checkout on a subdomain). A first arrival from outside is an entry, not a return. GA4 MCP read for session sources.
- Source: GA4 help 10327750 (unwanted referrals for third-party payment processors).

### XDS-04 Linker on every outbound path

- medium, gate S4, L1
- Evidence: Navigation requests between owned domains in the HAR.
- PASS: Every path to another owned domain carries _gl (links, scripted buttons, forms needing decorate_forms) and redirects keep it.
- WARN: _gl present on the first request and removed by a redirect.
- FAIL: Not used as FAIL (a lost client ID fails XDS-01).
- Detect: har-audit XDS-04.

### XDS-05 SPA page views match the route fixture

- high, gate S3, L1
- Evidence: Route fixture with the expected count written before running; har-audit page view sequence.
- PASS: Observed page views equal the expected count and one owner sends them.
- WARN: Two page views for one location within 2 seconds outside the fixture.
- FAIL: The count differs, or two owners send page views (enhanced measurement history plus a GTM history trigger or app event).
- Detect: har-audit XDS-05 with --route-fixture host/path=N on the fixture capture.
- Source: GA4 page views guide.

### XDS-06 Route edge cases send the right page views

- medium, gate S4, L1
- Evidence: Fixture steps for failed, canceled and auth-redirected routes.
- PASS: No page view for a screen not shown; virtual page views carry correct location, title and referrer.
- WARN: A stale title or referrer, or a page view for a screen not shown.
- FAIL: Not used as FAIL.
- Detect: har-audit sequence plus manual fixture notes.

### XDS-07 Configured domains match the journey

- low, gate S4, L2
- Evidence: GA4 Admin configured domains (UI read).
- PASS: Configured domains match the journey's owned hosts; staging and preview hosts follow a documented policy.
- WARN: A broad match catches unintended hosts, or a journey host is missing.
- FAIL: Not used as FAIL.
- Detect: UI read.

## SST: Server-side routes and deduplication

Does each event reach each destination once across browser and server routes, with the visitor's context? Domain weight 10.

Without server-side-tracking-debug (planned): server container Preview and host logs for one reject run and one accept run (SST-06).

### SST-01 One owner route per event and destination

- high, gate S2, L2
- Evidence: stack.md, ads_get_dataset_details, platform integration lists, server container configuration.
- PASS: Every event and destination has one owner route, or a documented dual route with a dedup key.
- WARN: not used.
- FAIL: Two routes send the same event to one destination without a shared key (for example theme pixel plus app plus server container).
- Detect: Manual inventory; har-audit browser side.

### SST-02 Meta browser and server copies deduplicate

- critical, gate S2, L1
- Evidence: har-audit with --server-routes meta; Events Manager Test Events (reported evidence); ads_get_dataset_stats by event source.
- PASS: Dual-route events carry eventID equal to the server event_id with the same event name and pixel; Test Events marks the pair deduplicated; no browser-only duplicates.
- WARN: not used.
- FAIL: A dual-route event without eventID, mismatched IDs, or two browser copies of one action.
- Detect: har-audit SST-02 with --server-routes meta or meta:Purchase.
- Source: Meta dedup doc, checked 2026-11-01 (48-hour window; the fbp and external_id method only works when the browser event arrives first).

### SST-03 Other dual-route platforms carry their keys

- high, gate S2, L1
- Evidence: har-audit with --server-routes tiktok,linkedin,openai; platform test views.
- PASS: TikTok carries event_id on pixel and Events API; LinkedIn uses the same event id on Insight Tag and Conversions API with one conversion rule per source; OpenAI Ads sends the pixel `event_id` equal to the Conversions API `id`, with the same pixel ID and event name; others per their docs.
- WARN: not used.
- FAIL: A dual-route platform without its key.
- Detect: har-audit SST-03 with --server-routes tiktok, linkedin or openai (LinkedIn keys are printed for review; for OpenAI the keys seen are printed so the parameter name can be confirmed).
- Source: TikTok event deduplication help; LinkedIn Conversions API use case; OpenAI Ads Conversions API (the first event received for a matching pixel ID, event name and ID is kept, later duplicates ignored; checked 2026-11-02).

### SST-04 GA4 receives each event once

- high, gate S2, L1
- Evidence: har-audit route per measurement ID.
- PASS: Each GA4 event reaches the property by one route.
- WARN: One ID uses two routes for different events (mixed routing; document it).
- FAIL: The same event to the same measurement ID both directly and through a server container or gateway, or browser plus Measurement Protocol copies under one name.
- Detect: har-audit SST-04.

### SST-05 Server endpoint is first-party and HTTPS

- medium, gate S4, L1
- Evidence: Hosts of GA4 and vendor requests that are not the vendor's own (Google collection and ad hosts, doubleclick.net included, count as Google-direct); the server address har-audit prints for each route next to the main document's; a DNS lookup of the route host (`dig +short sgtm.example.com`).
- PASS: Server container or gateway on a subdomain or path of the site over HTTPS.
- WARN: A hosting provider default domain (*.stape.io, *.run.app, *.appspot.com).
- FAIL: Plain http.
- Detect: har-audit SST-05.
- Note: Safari caps cookies set in responses from a subdomain that resolves through a third-party CNAME, or from third-party IP address cloaking, to 7 days (WebKit tracking prevention, checked 2026-11-02). That removes the cookie-lifetime benefit many teams buy a server container for. Record it under Expected limits; only a path on the site's own host, such as a tag gateway or a reverse proxy, avoids it.

### SST-06 Server events carry the visitor's context

- medium, gate S4, L2
- Evidence: Server container Preview outgoing request; platform test event payload view.
- PASS: Client IP and user agent are the visitor's, event time is the real occurrence, action_source matches where it happened, and a Measurement Protocol event carries the tagged browser's client_id, captured with its consent state when the action happened.
- WARN: not used.
- FAIL: The server or webhook IP is sent as the visitor's IP, delayed events carry send time, or the server invents a client_id or puts the customer ID in its place (one customer can have several browsers, and an invented ID joins nothing).
- Detect: Manual.

### SST-07 Match keys survive delivery

- medium, gate S3, L2
- Evidence: ads_get_dataset_quality per-key coverage against the share of events where the business holds each key.
- PASS: Each key's coverage is at least the share of events where the business holds it and consent allows it.
- WARN: Coverage below that share, or double hashing suspected.
- FAIL: Not used as FAIL.
- Detect: Meta Ads MCP read.
- Note: Report Event Match Quality as a trend; do not grade on third-party score bands.

## CID: Click IDs, UTMs and CRM

Do click IDs and UTMs survive from the landing request to the CRM outcome and the offline import? Domain weight 12.

UTM violations go to utm-convention-and-registry. Without offline-conversion-import (planned): read upload summaries and connector run history for CID-08.

### CID-01 Click IDs and UTMs survive redirects

- high, gate S3, L1
- Evidence: har-audit marker trace from landing URLs built from the ad final URL and tracking template.
- PASS: Every marker reaches the final landing document unchanged on every landing route tested.
- WARN: not used.
- FAIL: A redirect drops or alters a marker (trailing slash, locale, http to https, shortener, consent reload).
- Detect: har-audit CID-01.
- Source: Google Ads auto-tagging help: the GCLID must reach the final landing page.

### CID-02 Auto-tagging is on

- high, gate S3, L2
- Evidence: GAQL customer.auto_tagging_enabled; Microsoft Ads setting where used.
- PASS: Auto-tagging on for Google Ads, and msclkid auto-tagging on where Microsoft Ads runs.
- WARN: not used.
- FAIL: Auto-tagging off while Google Ads runs.
- Detect: Google Ads MCP read.

### CID-03 Click IDs stored first-party as designed

- medium, gate S4, L1
- Evidence: Browser cookie list (names only) or a sensitive HAR from a fresh test profile with approval.
- PASS: Click ID storage appears as designed after a grant (_gcl_aw, _fbc, _uetmsclkid, li_fat_id, _ttp, and the OpenAI Ads `__oppref` and `__obref`) and not before a choice or after reject.
- WARN: Storage present under denied state against the design.
- FAIL: Not used as FAIL here (non-Google storage before consent fails CON-02).
- Detect: har-audit CID-03 when cookies are visible; manual otherwise.
- Note: Sanitized HARs drop Cookie and Set-Cookie headers; cookies written by JavaScript never appear as Set-Cookie.

### CID-04 Form submissions carry exact values

- high, gate S3, L3
- Evidence: har-audit marker trace on form POST bodies (form endpoints, --form-hosts and own-domain posts); for an embedded scheduler or form, the markers forwarded into the embed URL or the vendor's hidden fields.
- PASS: The submission carries every marker exactly (same case, full length) in the intended fields.
- WARN: An embedded frame whose URL carries none of the markers, until its hidden fields are checked.
- FAIL: A marker is missing, lowercased, truncated or in the wrong field.
- Detect: har-audit CID-04 (form submissions and embedded frames).

### CID-05 CRM record holds exact values

- high, gate S3, L3
- Evidence: The test record's attribution fields read in the CRM (record ID and those fields only). In HubSpot read both the hidden-field custom property (for example gclid) and HubSpot's own Google ad click ID and Facebook click ID, which HubSpot fills itself, and note which one the offline import reads.
- PASS: Intended properties hold the exact values after creation; a returning-contact submission follows the documented first or latest policy; returning-visitor prepopulation does not overwrite hidden fields.
- WARN: not used.
- FAIL: Values altered, missing or overwritten against the policy.
- Detect: Manual CRM read.
- Source: HubSpot hidden fields help: prepopulation for returning visitors may overwrite hidden field values. HubSpot default contact properties: Google ad click ID and Facebook click ID are set automatically, not from form submissions (checked 2026-11-02).

### CID-06 CRM export capture inside thresholds

- high, gate S3, L2
- Evidence: crm-attribution-coverage output, one run per click ID column (in HubSpot the hidden-field property and HubSpot's own Google ad click ID); the column the offline import reads decides the status.
- PASS: Control segment coverage at least 95 percent, malformed or placeholder values at most 1 percent, lowercase-only gclids at most 1 percent.
- WARN: Coverage 80 to 95 percent.
- FAIL: Coverage below 80 percent, malformed above 1 percent, or lowercase-only gclids above 1 percent.
- Detect: crm-attribution-coverage CID-06.
- Note: Where the consent decision forbids click ID capture without consent, compute coverage over consented records only (`--consent <column> --consented <values>`) and report the rest under Expected limits. Report Safari and iOS separately where the data allows (`--split`); their losses are limits, not defects.

### CID-07 Conversion and association mapping keep the click ID

- medium, gate S3, L3
- Evidence: Lead conversion test (Salesforce) or deal association read (HubSpot).
- PASS: The outcome record the offline import reads holds the click ID selected for that outcome.
- WARN: not used.
- FAIL: The outcome record has no click ID while its lead had one.
- Detect: Manual.
- Source: Salesforce lead conversion considerations: mapped fields fill empty fields and do not overwrite existing contact values.

### CID-08 Offline conversion route healthy

- medium, gate S3, L2
- Evidence: Upload summaries, connector run history, outcome timestamps; the upload route (native CRM integration, Data Manager connection, Data Manager API, or a Google Ads API uploader) and, for a Google Ads API uploader, whether its developer token is allowlisted for legacy access.
- PASS: CRM outcomes upload recently with accepted rows, no alerts, real outcome times, inside 90 days of the click (63 for enhanced conversions for leads), through a route that still works after 15 June 2026.
- WARN: Rejected rows, gaps, send time used as outcome time, or a Google Ads API uploader with no dated migration owner.
- FAIL: Uploads to an action used for bidding have stopped.
- Detect: Google Ads MCP read; connector UI.
- Source: Google Ads help 2998031: since 15 June 2026 offline conversion import and enhanced conversions for leads uploads go through the Data Manager API and are blocked in the Google Ads API; developer tokens that sent no request between January and June 2026 are not allowlisted for legacy access (checked 2026-11-02).
- Hand-off: offline-conversion-import (planned).

### CID-09 UTM values follow the naming rules

- low, gate S4, L2
- Evidence: crm-attribution-coverage UTM section; GA4 session source and medium.
- PASS: No placeholders, no case variants of one source, medium in the allowed list.
- WARN: Violations above 1 percent of tagged records.
- FAIL: Not used as FAIL.
- Detect: crm-attribution-coverage CID-09.
- Hand-off: utm-convention-and-registry.

## PII: Personal data and test traffic

Is personal data kept out of vendor requests and URLs, and is test traffic separated? Domain weight 7.

No sibling skill. If real personal data appears in a vendor request, stop, restrict the evidence and tell the user the same day, with a draft note for the privacy owner. Never send it yourself; deletion requests are the owner's decision.

### PII-01 No personal data in vendor requests

- critical, gate S1, L1
- Evidence: har-audit --pii with the test values; generic email pattern scan; GA4's data redaction state for email and query parameters (Admin API read, see [platform-reads.md](platform-reads.md)).
- PASS: No raw, URL-encoded, lower-cased or base64 test email or phone, and no email pattern, in any tag request, --collector-hosts request or request to a non-owned host other than form endpoints.
- WARN: A request whose body the script cannot read (compressed or binary, for example session replay), until the vendor's own tool shows no test value; or a test value in an own-host collector candidate, until it is classified.
- FAIL: Any occurrence, including in page location, referrer, title or custom parameters.
- Detect: har-audit PII-01 and the unclassified-host list. Test values are searched raw, URL-encoded, lower-cased and inside base64 values (query values, form and JSON fields, whole bodies); a phone is also searched as its last 10 digits. Any other email address is searched in the request decoded up to twice, because a page URL carrying an email arrives double-encoded inside dl, url or pu, and is printed masked. A body the script cannot read raises the WARN in place of a PASS and joins the not-checked list. Own-host requests are searched only through --collector-hosts or as collector candidates. Pass a form or scheduler endpoint the visitor submits to with --form-hosts so it is judged as a form, not a collector.
- Confirm: For real-user occurrences, stop, restrict the evidence and tell the user the same day with a draft note for the privacy owner; never send it yourself. GA4 redaction does not protect the other vendors that receive the same URL.

### PII-02 No personal data in URLs

- critical, gate S1, L3
- Evidence: Navigation document URLs in journey captures after valid and invalid submissions.
- PASS: Forms submit with POST; validation and thank-you pages carry no personal data in the URL.
- WARN: not used.
- FAIL: The test email or phone, or any other email address, appears in any page URL.
- Detect: har-audit PII-02 (test values and any other email address in document URLs, base64 query values included).

### PII-03 Hashed match data only with consent and a decision

- high, gate S1, L3
- Evidence: har-audit hashed-value search per scenario; decisions.md.
- PASS: Hashed email or phone appears only in approved matching fields after a grant.
- WARN: not used.
- FAIL: Hashed values before a choice, after reject, or to a destination without an approved decision.
- Detect: har-audit PII-03 with --pii and --approved-matching (destination keys such as google-ads, meta, tiktok and openai are listed in the script header; an unknown key is refused). Tag requests, --collector-hosts requests and unclassified third-party hosts are searched.

### PII-04 Test and internal traffic handled

- medium, gate S4, L2
- Evidence: GA4 data filter states (UI read), test identity flags.
- PASS: Filter states recorded (an active filter's effect is permanent) and test identities flagged and excluded from comparisons.
- WARN: States unknown, or test records counted in reports.
- FAIL: Not used as FAIL.
- Detect: UI read.
- Source: GA4 help 10108813.

### PII-05 Debug and test settings off in production

- medium, gate S4, L1
- Evidence: har-audit debug flags; server payload views.
- PASS: No GA4 debug mode on normal production traffic, no test event codes in production server payloads, no staging IDs on production.
- WARN: Debug flags or test codes on production traffic outside the auditor's session.
- FAIL: Not used as FAIL.
- Detect: har-audit PII-05 (pass --debug-expected when the auditor's own session uses debug mode).

## CFG: Platform settings

Do property and account settings that change counts match the decisions? Domain weight 5.

With ga4-audit: cite its rules (G01 for CFG-01, G04 for CFG-02, G03 for CFG-04, G22 for CFG-05) instead of reading the property twice. Without it: the CFG checks here plus CNV-05, EVT-06 and EVT-07. Without ga4-unassigned-not-set-diagnosis: the CFG-05 band check only.

### CFG-01 Time zone and currency match the business

- low, gate S4, L2
- Evidence: get_property_details against metrics.md.
- PASS: Reporting time zone and currency match the definitions.
- WARN: A mismatch with the business definition.
- FAIL: Not used as FAIL.
- Detect: GA4 MCP read.
- Hand-off: ga4-audit for full property settings.

### CFG-02 Data retention matches the decision

- low, gate S4, L2
- Evidence: Data retention through the Admin API with analytics.readonly (`properties.getDataRetentionSettings`, v1beta) or an Admin UI read.
- PASS: Retention matches the decision (standard properties offer 2 or 14 months for user-level and key event data).
- WARN: 2 months while year-on-year exploration is needed.
- FAIL: Not used as FAIL.
- Detect: Admin API read or UI read (the GA4 MCP server has no tool for it).
- Source: GA4 help 7667196, checked 2026-11-01.

### CFG-03 Links and export state known

- low, gate S4, L2
- Evidence: list_google_ads_links; BigQuery export settings.
- PASS: Expected Google Ads links exist and the BigQuery export state is recorded.
- WARN: A missing expected link.
- FAIL: Not used as FAIL.
- Detect: GA4 MCP read.

### CFG-04 Enhanced measurement consistent with page view owners

- medium, gate S4, L2
- Evidence: Stream enhanced measurement settings through the Admin API with analytics.readonly (`properties.dataStreams.getEnhancedMeasurementSettings`, v1alpha) or a UI read, against the plan.
- PASS: History page views are off when another owner sends page views; form interactions are not used as lead conversions.
- WARN: not used.
- FAIL: History page views stay on while another owner sends page views, or form_submit is used as a lead conversion.
- Detect: Admin API read or UI read (the GA4 MCP server has no tool for it).

### CFG-05 Unassigned and (not set) inside the band

- medium, gate S4, L2
- Evidence: GA4 run_report session default channel group with key events, last 8 completed weeks.
- PASS: Share inside its 8-week band.
- WARN: Share outside the band.
- FAIL: Share at 5 percent or more of key events (default threshold, the same as ga4-audit G22).
- Detect: GA4 MCP read.
- Hand-off: ga4-unassigned-not-set-diagnosis.

## GOV: Ownership and monitoring

Is there a plan, an owner and a check that will catch the next break? Domain weight 3.

Without tracking-monitoring-and-incidents (planned): rerun this audit after each release that touches tags, consent, checkout or forms, and record that trigger in the report.

### GOV-01 Plan exists for tier-1 conversions

- medium, gate S4, L1
- Evidence: tracking-plan/ contracts or sheet tabs.
- PASS: Each tier-1 conversion has a contract with trigger, dedup key, value rule and owner.
- WARN: Only a derived plan exists.
- FAIL: No contract for any tier-1 conversion.
- Detect: Manual.
- Hand-off: tracking-plan-builder.

### GOV-02 Owners and publish rights

- low, gate S4, L2
- Evidence: stack.md owners; gtm-container-audit user permissions check (outside the export).
- PASS: Each destination and route has a named owner; at most three GTM publishers; no former agency with edit rights.
- WARN: Missing owners or excess publishers.
- FAIL: Not used as FAIL.
- Detect: Manual.

### GOV-03 Monitoring catches the next break

- low, gate S4, L2
- Evidence: Alert or scheduled check configuration.
- PASS: An alert or scheduled check on tier-1 conversion volume or coverage with an owner who acts.
- WARN: not used.
- FAIL: No monitoring on any tier-1 conversion.
- Detect: Manual.
- Hand-off: tracking-monitoring-and-incidents (planned).
