# Sizing, routing, containment and proof

Depth for steps 2 and 5 of the procedure: how to size a break in either direction, the guards that stop a ratio from lying, where to start walking, how to contain the damage while the proof runs, and what proof looks like with or without an independent count. Facts were checked on 2026-10-17, and the review fixes on 2026-10-20; re-check anything marked "verify live".

## The ratio and its direction

- Ratio per completed day: analytics outcome divided by the independent outcome (backend, billing, CRM), same days, same timezone.
- Baseline band: the ratio's range over the previous 28 comparable days. Comparable means the same weekday type (weekdays with weekdays, weekend days with weekend days), with public holidays and known outage days left out. Consent, blockers and identity keep the band below 100% for most events.
- First-change date: the first of at least 2 consecutive completed days outside the band, or the first of 3 days out of 5. A single day outside is recorded as `watch`, re-checked the next completed day, and opens no incident on its own.
- Direction:
  - Under-count: the ratio sits below the band.
  - Over-count: the ratio sits above the band, or above 100% for an event that should fire once per outcome (`sign_up`, `purchase`, `generate_lead`).
- Gap per day, always positive:
  - Under-count: independent count x baseline ratio, minus observed.
  - Over-count: observed, minus independent count x baseline ratio.
- Record the direction and the absolute gap in the break-map record (`sizing.direction`, `sizing.gap`).

## Offsetting errors

A ratio can hold while two breaks cancel. Take a baseline of 85 tracked events per 100 outcomes. A release loses 10 more outcomes and sends 10 others twice: still 85 events, so the ratio holds while coverage fell from 85% to 75% and events per ID rose from 1.00 to 1.13. Segment ratios can hold too, so the mix-shift guard alone would record "no break".

For any outcome event that carries an ID (`sign_up`, `generate_lead`, `purchase`), compute two more numbers per completed day, for the baseline and the incident:

- `unique_id_coverage`: distinct analytics IDs found in the backend set, divided by the backend count.
- `events_per_id`: analytics events for those IDs, divided by the distinct IDs found.

Rule: if `events_per_id` is above its baseline, or `unique_id_coverage` falls below its band while the raw ratio holds, record "offsetting errors" and walk both the under-count route and the over-count route. Record both numbers in `sizing`.

```sql
WITH ga AS (
  SELECT
    COALESCE(
      (SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'account_id'),
      (SELECT CAST(value.int_value AS STRING) FROM UNNEST(event_params) WHERE key = 'account_id'),
      (SELECT CAST(value.double_value AS STRING) FROM UNNEST(event_params) WHERE key = 'account_id')
    ) AS outcome_id
  FROM `your-project.analytics_PROPERTY_ID.events_*`
  WHERE _TABLE_SUFFIX BETWEEN '20260831' AND '20260915'
    AND event_name = 'sign_up'
),
ga_ids AS (
  SELECT outcome_id, COUNT(*) AS events
  FROM ga
  WHERE outcome_id IS NOT NULL
  GROUP BY outcome_id
),
backend AS (
  SELECT DATE(created_at) AS d, CAST(account_id AS STRING) AS outcome_id
  FROM `your-project.app.accounts`
  WHERE DATE(created_at) BETWEEN '2026-09-01' AND '2026-09-14'
    AND NOT is_test_account
)
SELECT
  b.d,
  COUNT(*) AS backend_outcomes,
  COUNTIF(g.outcome_id IS NOT NULL) AS ids_found,
  ROUND(COUNTIF(g.outcome_id IS NOT NULL) / COUNT(*), 3) AS unique_id_coverage,
  ROUND(SUM(g.events) / NULLIF(COUNTIF(g.outcome_id IS NOT NULL), 0), 2) AS events_per_id
FROM backend AS b
LEFT JOIN ga_ids AS g USING (outcome_id)
GROUP BY b.d
ORDER BY b.d;
```

- Replace the parameter key, the backend table, its ID column and its test flag. The GA4 window runs one day wider on each side so outcomes near midnight still match.
- For `purchase`, read `ecommerce.transaction_id` instead of an event parameter.
- The export fills one typed field per parameter: a string goes to `string_value`, an integer to `int_value`, a double to `double_value`. The `COALESCE` reads all three, so a numeric ID is not read as missing.
- If the backend is not in BigQuery, export the backend IDs for the window into a scratch table the investigation owns. Keep only the aggregates in the record, never the IDs.

## Guards before trusting a ratio change

- Mix shift. Compute the ratio within each shared segment (method, page or form, platform, region, campaign) for the baseline and the incident. If every segment ratio holds and only the share of outcomes per segment moved, there is no break. Examples: signups moving to an app that is not tracked on the web stream, or a new campaign in a region whose consent default is denied. Record "no break, mix shift" with the segment shares, after the offsetting check above.
- Definition change on the independent side. Confirm the backend query, its filters (test accounts, verification status, deleted records) and its timezone did not change in the same release. A ratio can move because the denominator changed.
- Low volume. If the independent count is below about 30 per daily bucket, a daily band is noise. Use weekly buckets, or repeated controlled journeys (see proof without an independent count).
- Recent days in GA4. If only the last 1 to 3 days are low, re-check after 48 hours before walking. GA4 processing takes 24 to 48 hours, daily BigQuery tables are updated for 2 more calendar days, and many models only rebuild recent partitions.
- Recent days in Google Ads. The default conversion columns date a conversion by its ad interaction, so the last N days keep rising as late conversions arrive. A B2B account with a 7 to 14 day lag looks broken every day under the GA4 rule. Either compare on Conversions (by conv. time) (`metrics.conversions_by_conversion_date` in GAQL), or drop the last N days, where N is the lag within which about 90% of the action's conversions arrive. Read the lag through the Google Ads MCP server's `search` tool (verify the field definitions live):

  ```sql
  SELECT segments.conversion_lag_bucket, metrics.conversions
  FROM campaign
  WHERE segments.date BETWEEN '2026-06-01' AND '2026-08-31'
    AND segments.conversion_action_name = 'Sign up (GA4)'
  ```

## Where to start

Wherever you start, the layers above it still need pass evidence before the verdict stands.

- Over-count (ratio above the band or above 100%) or offsetting errors: start at tag manager and browser request.
  - Perform one action under the test-traffic rules and count the events it produces across every request to the collection host (one request can carry several events) and, server side, per outcome ID.
  - Inventory every sender of the event: hardcoded gtag, CMS or platform plugin, GTM web container, GA4 enhanced measurement (`form_submit`, page views on browser history changes), server container route, Measurement Protocol calls from the backend, Google tag gateway. Then the non-Google senders: Meta Pixel and Conversions API, TikTok and LinkedIn pixels and their server routes, Shopify customer event pixels, and CDP destinations such as Segment. For each browser-plus-server pair, check that both sends carry one shared event ID (layer 5).
  - Check whether the success event fires on click or form submit attempt instead of on the confirmed outcome.
  - Check wildcard custom event triggers for matches on `gtag.config` (GTM release note of 8 October 2026).
  - For a container loaded through `/gtag/js` or `/gtag/destination` with a `GTM-` ID, check whether custom HTML or third-party tags began firing on 9 July 2026 (layer 3).
  - Key events auto-marked. With Ads Personalization on for a linked Google Ads account, Google marks `add_to_cart`, `add_to_wishlist`, `begin_checkout`, `session_start`, `view_item`, `view_item_list` and `view_search_results` as key events. A "key events" total that jumps on the link date is this, not duplicate collection. Read the key event list and, in Property change history, when each was marked.
  - Stale ecommerce data. Google's ecommerce samples push `{ ecommerce: null }` before each ecommerce event to clear the previous object. Without it, items or value from an earlier event can carry into the purchase. In Preview, check that the purchase tag's ecommerce variables at the purchase event hold only the current order.
  - In BigQuery, count events per outcome ID on the daily tables. For `purchase`, use the dedicated column:

    ```sql
    SELECT ecommerce.transaction_id AS outcome_id, COUNT(*) AS events
    FROM `your-project.analytics_PROPERTY_ID.events_*`
    WHERE _TABLE_SUFFIX BETWEEN '20260901' AND '20260914'
      AND event_name = 'purchase'
    GROUP BY outcome_id
    HAVING events > 1
    ORDER BY events DESC
    LIMIT 50;
    ```

    For other events, read the ID parameter across all value types:

    ```sql
    SELECT
      COALESCE(
        (SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'lead_id'),
        (SELECT CAST(value.int_value AS STRING) FROM UNNEST(event_params) WHERE key = 'lead_id'),
        (SELECT CAST(value.double_value AS STRING) FROM UNNEST(event_params) WHERE key = 'lead_id')
      ) AS outcome_id,
      COUNT(*) AS events
    FROM `your-project.analytics_PROPERTY_ID.events_*`
    WHERE _TABLE_SUFFIX BETWEEN '20260901' AND '20260914'
      AND event_name = 'generate_lead'
    GROUP BY outcome_id
    HAVING events > 1
    ORDER BY events DESC
    LIMIT 50;
    ```

    A null or `(not set)` ID across all value types, with a large count, means the ID is not sent, which is its own finding.
- Drop concentrated in one page, form, route or event: collection. Start at tag manager and browser request.
- Uniform drop across every segment, starting on the date of a CMP or banner change: consent.
- Uniform step change on one date with no CMP change. Check, in order:
  - server container hosting status, plan and tagging-server logs for that date;
  - data filter state changes, and whether `debug_mode` reached production while a developer traffic filter is Active;
  - GA4 property change history for that date (data streams, Google tag settings, enhanced measurement, key events);
  - the GTM and GA4 release notes for that date, including the 2026 changes listed under layer 3 and the gateway dates under layer 4.
- Counts fine but source or campaign wrong, a domain handoff in the journey, or a loss limited to Safari or iOS: link and redirect, including its Domain handoff section.
- Only Google Ads low while GA4 and the backend agree: the Google Ads checks under layer 6, then consent (`ad_storage` and URL passthrough) and link.
- GA4 interface fine, BigQuery or dashboard wrong: export, then warehouse, then report.
- Both sides agree and only a report differs: report.

## Contain

Containment limits what automated systems learn from bad data while the proof runs. It changes no collection, needs no proof of the failing layer, and is not a fix.

- Trigger: the gap is outside the band for at least 2 completed days, and the affected conversion feeds Smart Bidding, audiences or automated budget rules.
- Draft for each owner, and record in the `containment` block:
  - a Google Ads data exclusion, for the ads owner, under the rules below;
  - a "provisional" annotation for each affected report and dashboard, for the report owners;
  - the automated budget rules, scripts and audience-based automations that read the affected conversion, with a proposal to pause or defer each, for their owners.
- Google Ads data exclusion rules (Google's data exclusion guidance):
  - Google recommends exclusions for conversion data outages, including broken conversion tracking, and says to apply them as quickly as possible once the issue is identified.
  - Exclusion dates apply to clicks, not conversions. Start early enough to cover the conversion delay; Google's best practice is to exclude at least 90% of the clicks behind the affected conversions. Read the delay from the conversion lag query above.
  - Start and end times are in the Google Ads account's time zone, not UTC or the GA4 property timezone. Convert the break window first.
  - Give a fixed end. For a break still running, end at the planned fix time; if the fix slips, draft a further exclusion for the extra days. Google says not to remove an exclusion once applied, and that exclusions are not meant for frequent or long use.
  - Google says bids and spend can still dip briefly after an exclusion is applied.
  - Backfilling conversions is generally not recommended. If the owner still approves a reporting backfill for a period with an exclusion, wait at least 5 days after the exclusion was applied.
- Recovery uploads from stored click IDs, if the ads owner approves one (checked 2026-10-20, verify live):
  - Google's offline import guidelines say conversions uploaded more than 90 days after the associated last click are not imported, and 63 days for enhanced conversions for leads.
  - The API rejects a click older than the receiving conversion action's click-through window with `EXPIRED_EVENT`. Read that window (`conversion_action.click_through_lookback_window_days`, or the action's settings in Google Ads) before promising recovery.
- Example arithmetic: a break from 2026-09-08 14:00 UTC, an account in America/New_York (UTC-4 in September) and 90% of the action's conversions within 2 days of the click give a start of 2026-09-06 00:00 account time.

## Proof with an independent count

A layer is the first failing layer only when all three hold:

- Every layer above it has pass evidence for the same journey.
- The failing journey fails at this layer and a comparison journey that should pass (an unaffected path, the previous version, another browser) passes at this layer.
- The failing segment's absolute gap is at least 80% of the total absolute gap. Below 80%, keep walking and record a second break. With offsetting errors, size the under-count and the over-count separately and apply the 80% rule to each.

## Proof without an independent count

When no system commits the outcome independently, or the volume is too low for a band:

- The failing journey fails at this layer in 3 of 3 runs, across at least two browsers.
- The comparison journey passes at this layer in 3 of 3 runs.
- Every layer above has pass evidence for the same runs.
- Record `share_of_gap_explained: not measurable` and keep the case open until a count exists. The fix can still go ahead as a draft; the case closes only when a count confirms the ratio is back in band.
