# Worked incident: signups fall to a third of backend after a frontend refactor

Everything here is fictional. Acme Cloud is a made-up B2B SaaS on `www.example.com` with web container `GTM-XXXXXXX`, GA4 property 123456789 with web stream measurement ID `G-XXXXXXXXXX`, no server container, daily BigQuery export and a BI dashboard built on a warehouse model. The GA4 property timezone and the backend both use UTC, so no timezone conversion is needed between them. The Google Ads account, customer `123-456-7890`, is set to America/New_York and imports the GA4 `sign_up` key event as a primary conversion for Smart Bidding. Test identities follow the pattern `qa-signup-NN@example.com` with a backend test flag, one fresh identity per run, excluded from both counts. The GA4 developer traffic filter is Active.

## The complaint

On 2026-09-14 the growth lead writes: "GA4 signups have crashed this week but sales say trials are normal. Is GA4 broken? Should we switch Google Ads to optimise on backend signups instead?"

Two proposals are already on the table:

- Change the GTM trigger to a regex that "catches every signup event name".
- Import backend signups into Google Ads and stop using the GA4 key event.

Both are downstream changes. Neither is made until the first failing layer is proven.

The sizing runs on 2026-09-15, so the incident window ends on 2026-09-12, the last day that is complete and more than 48 hours old.

## Step 1: the question

```yaml
question:
  complaint_as_stated: "GA4 signups have crashed this week but trials are normal"
  report: "GA4 Reports, Engagement, Events, event name sign_up"
  metric: "event count, sign_up (marked as a key event)"
  scope: "event"
  date_range: "2026-09-09 to 2026-09-12, completed days outside the processing window on 2026-09-15"
  timezone: "UTC for GA4 and for the backend"
  filters: "none"
  independent_source: "backend accounts table, one row per verified signup, created_at in UTC, test flag excluded"
  expected_ratio: "81% to 88% (2026-08-10 to 2026-09-06, 28 comparable days)"
  observed_ratio: "33% to 36%"
  first_change_date: "2026-09-08"
```

## Step 2: size, date and contain

Daily averages. Acme's signup volume does not vary by weekday, so all days share one band. 2026-09-07, a US public holiday, is left out of the baseline.

- Baseline, 2026-08-10 to 2026-09-06: backend 120, GA4 `sign_up` 102, ratio 85%.
- Incident, 2026-09-09 to 2026-09-12: backend 118, GA4 `sign_up` 40, ratio 34%.
- Direction: under. Expected GA4 at the baseline ratio: 118 x 0.85 = 100. Absolute gap: about 60 per day.

The ratio left the band on 2026-09-08 and every completed day since stayed out, so the 2-day persistence rule is met from 2026-09-09. Rechecked on 2026-09-17: 2026-09-13 and 2026-09-14, the next days past the 48-hour mark, sat in the same range (33% to 35%). The change log for 2026-09-08 has one entry: app release "Rebuild signup form", deployed 14:00 UTC. No GTM publish (Versions page), no CMP change, no ads change, no entries in GA4 property change history, no gateway or CDN change, and nothing in the GTM release notes for that date.

Guards:

- Mix: email was 70% of backend signups in both periods, so the drop is not a mix shift.
- Definition: the backend signup query and its filters did not change in the release.
- Volume: well above 30 outcomes a day, so a daily band is usable.
- Offsetting errors: `sign_up` carries `account_id`. Events per ID stayed at 1.00 in both periods, and unique-ID coverage fell from 85% to 34% with the ratio. No duplicates are hiding a larger loss.

Splits:

- By signup method (GA4 `method` parameter, backend `signup_method`):
  - Google sign-in: baseline 31 of 36 (86%), incident 30 of 35 (86%). Unaffected.
  - Email: baseline 71 of 84 (85%), incident 10 of 83 (12%). This is where the gap sits.
- GA4 email signups by page: baseline `/signup` 61 and `/pricing` (signup modal) 10; incident `/signup` 0 and `/pricing` 10.
- By browser: Chrome, Safari and Firefox fall by the same share. Not a Safari policy.
- By session default channel group: sessions per channel within their usual range. Traffic still arrives and is attributed.

Decision rule from the procedure: an under-count concentrated in one page and one method, so this is collection. Start at tag manager, but record evidence for link and consent first.

Containment, drafted on 2026-09-15 before any layer is proven. The trigger is met: five completed days outside the band, and `sign_up` feeds Smart Bidding.

- Google Ads data exclusion, for the ads owner, on the campaigns bidding on the imported `sign_up` action:
  - The break started 2026-09-08 14:00 UTC, which is 10:00 in the account's America/New_York time zone.
  - The conversion lag query (`segments.conversion_lag_bucket`, last 90 days) shows 91% of `sign_up` conversions within 2 days of the click, so the exclusion starts at 2026-09-06 00:00 account time to cover the clicks behind the affected conversions.
  - Fixed end: 2026-09-16 23:59 account time, the earliest likely release day. If the fix slips, a further exclusion is drafted for the extra days; an applied exclusion is not removed.
- "Provisional" annotations proposed on the GA4 property and the signup dashboard from 2026-09-08.
- One automated rule moves budget toward campaigns with the lowest GA4 `sign_up` cost per conversion; proposed to its owner for pausing until the retest passes.
- The ads owner applies the exclusion on 2026-09-15 at 16:00 UTC. Nothing in collection changes.

## Step 3: the path

Email signup on `/signup`: ad or organic click, landing page, consent banner, signup form, GTM web container, GA4 request, GA4 processing, BigQuery daily export, dashboard model, dashboard tile. The journey stays on `www.example.com`, so there is no domain handoff. Server container and gateway: not applicable.

Comparison journeys: email signup in the `/pricing` modal, and Google sign-in on `/signup`. Both still track.

Every test journey below uses a fresh test identity (`qa-signup-01` onward; the record lists which run used which), GTM Preview and `gclid=TeSter-123`. Google Ads conversion hosts are blocked in the test browser with DevTools network request blocking, and that is noted in the evidence. With the developer traffic filter Active, the test events show in DebugView and stay out of reports and BigQuery.

## Step 4: walk the layers

1. Link and redirect: pass.
   - Evidence: channel session counts inside baseline range; a hop trace of a search ad's final URL, built with the account's tracking template, keeps `gclid=TeSter-123` and all UTMs through two redirects to `/signup`; in Chrome the `dl` of the first GA4 request carries the same page location. The loss happens after landing, at the form step.
2. Consent: pass.
   - Evidence: CMP unchanged since June per the change log. Tag Assistant matrix (accept, reject, no choice, change later) shows the default on Consent Initialization and the update after the choice. The Google sign-in path on the same page, under the same consent states, tracks at baseline.
3. Tag manager: fail.
   - The Versions page shows live version 41, published 2026-08-27, before the break. `workspaces.list` returns two workspaces; `getStatus` on each shows no changed entities. The published container did not change, so the cause is outside it; the app diff is first in line.
   - Preview, email signup on `/signup`: the Data Layer tab shows `signUpComplete` with `{signupMethod: "email", accountId: ...}`. The tag "GA4 Event - sign_up" is under Tags Not Fired. Its trigger "CE - signup_complete" requires Custom Event equals `signup_complete`.
   - Preview, comparison journeys: the `/pricing` modal and the Google sign-in callback both push `signup_complete` with `{method: ..., account_id: ...}` and the tag fires once.
   - The release diff, in the rebuilt form's success handler:

     ```diff
     - window.dataLayer.push({ event: 'signup_complete', method: 'email', account_id: accountId });
     + window.dataLayer.push({ event: 'signUpComplete', signupMethod: 'email', accountId: accountId });
     ```

4. Browser request: not checked as a separate cause. On the failing journey no `sign_up` event appears in any GA4 request, which follows from layer 3.
5. Server container: not applicable.
6. to 9. Platform processing, export, warehouse model, report: not checked as causes. They sit downstream of the first failing layer. The dashboard and BigQuery show the same drop as the GA4 interface, which is what a collection break predicts.

## Step 5: proof

- Layers above tag manager pass, with evidence for the same journey.
- The failing journey fails at tag manager and both comparison journeys pass there.
- Share of the gap explained: at the baseline ratio and page mix, GA4 should record about 83 x 0.85 x (61 / 71) = 61 email signups per day from `/signup`; it records 0. The total absolute gap is about 60 per day, so this one journey explains effectively all of it, well above the 80% bar.

First failing layer: tag manager. Mechanism: the app's dataLayer contract changed (event name and two parameter keys) and the trigger, correctly, no longer matches.

## Step 6: fix, retest, gap

Why not the regex trigger: it would match `signUpComplete` but the tag would still read `method` and `account_id`, which no longer exist, so every email signup would arrive with no method and no ID, and the offsetting check would go blind. It would also accept the next rename silently.

Why not switching Google Ads to backend imports now: it may be a good decision later, but it does not repair GA4, funnels or audiences, and it is a separate change with its own owner. It goes to the ads owner as a proposal.

The fix, as a draft for the app owner:

- Restore `window.dataLayer.push({ event: 'signup_complete', method: 'email', account_id: accountId })` in the success handler, after the backend confirms the account, not on button click.
- Add an end-to-end test that completes an email signup with a test identity and asserts exactly one `signup_complete` push with `method: 'email'` and an `account_id`.
- Link the test to the `user_registered` contract in `martech-context/tracking-plan/`.
- GTM stays untouched.

The app owner approves; the release ships 2026-09-16 at 10:00 UTC (06:00 in the Google Ads account's time zone), inside the exclusion's end.

Retest:

- Same journey in Preview with a fresh test identity: one `signup_complete` push, tag fires once, `method` is `email`, `account_id` present.
- Network panel: `sign_up` appears once across the GA4 requests, status 2xx.
- DebugView shows the test event.
- Completed days after the processing window, checked on 2026-09-21: 2026-09-17 ratio 84%, 2026-09-18 ratio 86%, both inside the 81% to 88% band. Events per ID 1.00.
- The paused budget rule goes back to its owner to resume.

Gap statement:

- Untracked window: 2026-09-08 14:00 UTC to 2026-09-16 10:00 UTC (2026-09-08 10:00 to 2026-09-16 06:00 in the Google Ads account's time zone).
- Estimated missing GA4 `sign_up` events: about 25 on 2026-09-08, 420 for the seven full days and 25 on 2026-09-16, about 470 in total. These are missing analytics events, not missing customers; the backend has every account.
- Affected: GA4 funnels and the signup audience for those dates; the Google Ads campaigns bidding on the imported `sign_up` key event, which received about 40% of their usual GA4 `sign_up` volume for eight days. The data exclusion, applied on 2026-09-15 for clicks from 2026-09-06 00:00 to 2026-09-16 23:59 account time, limits what bidding learns from the gap.
- Backfill: none for GA4. By the fix date most of the gap (2026-09-08 to 2026-09-13 10:00 UTC) was older than 72 hours; under the default `RELAXED` validation those events would be accepted with timestamps re-stamped to 72 hours ago, on the wrong dates. The rest would still arrive without their sessions and risk duplicates.
- Google Ads: Google advises against backfilling conversions. If the ads owner still approves a reporting backfill from stored click IDs through a separate offline conversion action, it waits until at least 2026-09-20, 5 days after the exclusion was applied, and each click must sit inside that action's click-through conversion window and Google's upload limit, read live first.
- Annotations proposed on the GA4 property and the signup dashboard for the window, replacing the "provisional" ones.
- Proposed for `martech-context/`, pending approval: a dated entry in `decisions.md` for the incident, the containment and the declined downstream fixes, and the regression-test link on the `user_registered` contract.

## What the record shows a reviewer

- The ratio, not the count, found the break and dated it; the guards ruled out a mix shift, and the per-ID check ruled out duplicates hiding a larger loss.
- Containment went to the ads owner on the day of sizing, before the proof, and changed nothing in collection.
- One split (method, then page) put the whole gap in one journey.
- The unchanged live version moved the cause outside the container, and the app diff showed the contract change.
- The fix went to the layer that changed, the downstream patches were declined with reasons, and the retest repeated the same journey before the completed-day ratio was accepted.
