8. Debugging GTM Web
How Preview mode actually works, the debugging workflow, reading collect requests in the network tab, GA4 DebugView and platform test tools, and the common failure modes table.
Tracking bugs hide in one of three layers: the model (did GTM see the event and fire the tag?), the wire (did the right request leave the browser?), or the platform (did the vendor accept and attribute it?). Debugging is walking those layers in order — and knowing which tool shows which layer:
layer question tool
───── ─────────────────────────────── ─────────────────────────────
MODEL did GTM fire what I expected, GTM Preview (Tag Assistant)
with the right values?
WIRE did the request actually leave, DevTools → Network
with the right payload?
PLATFORM did the vendor receive, match, GA4 DebugView · Meta Test
and attribute it? Events · Ads diagnosticsGreen at one layer and red at the next is information — it localizes the bug.
Preview mode: what it actually does
Hit Preview and Tag Assistant (tagassistant.google.com) opens your site
with a debug session attached. Mechanically, the part worth knowing: your
browser gets a debug build of your current workspace draft — not the
published version — served only to you (the session rides on a short-lived
cookie/storage handshake; every other visitor still gets the published
gtm.js). Two consequences:
- You can preview unpublished changes safely. Preview-then-publish is the whole workflow; nothing you preview affects real visitors.
- Preview success ≠ production success. The classic Friday bug — "it worked in preview!" — and the version was never published. Preview tests the draft; production runs the published version. Check the version banner when sanity escapes you.
The debug UI is a message-by-message replay of the evaluation model from Chapter 6, which is why it's organized the way it is:
- Left rail — every data layer message, in order: the lifecycle events
(Consent Init → Init → Container Loaded → DOM Ready → Window Loaded),
GTM's auto-events (
gtm.click,gtm.historyChange…), and your custom pushes. This is the queue, replayed. - Click a message → tabs show the world as of that message:
- Tags — fired vs not fired, and for each, which trigger conditions passed or failed, one by one. The single most useful screen in GTM.
- Variables — every variable's resolved value on that event (fire-time resolution, made visible).
- Data Layer — the model state after this message's merge — the queue-vs-model distinction from Chapter 5, on screen.
- API Calls / consent state — what consent was at each moment, which explains consent-blocked tags.
The workflow
Nearly every "tag didn't fire" resolves in five questions, asked in order against the left rail:
- Was the message pushed at all? No
purchasemessage in the rail = application bug, not GTM. Stop debugging the container. - Does the event name match exactly?
purchasevsPurchase— case-sensitive, the eternal classic. - Did the trigger's conditions pass? Open the tag → trigger conditions are listed pass/fail per condition.
- Did an exception match, or consent block it? Blocked tags say so.
- Did variables resolve to what the tag needed? Fired-with-
undefinedis "fired" in the UI but garbage on the wire.
If all five are green, the model layer is clean — move to the wire.
The wire: network tab literacy
Chapter 2 introduced reading collect requests; here's the practiced version. DevTools → Network, filter per platform:
| Platform | Filter for | Endpoint |
|---|---|---|
| GA4 | collect |
google-analytics.com/g/collect (or region1.… for EU routing) |
| Google Ads | conversion |
googleadservices.com/pagead/conversion |
| Meta | /tr |
facebook.com/tr |
| TikTok | tiktok |
analytics.tiktok.com |
linkedin |
px.ads.linkedin.com |
What to actually check in a payload: the event name, the money
(value/currency), the dedup key (transaction_id/event_id), and the
identifiers (cid, fbp/fbc) — present and non-empty. Three wire-reading
rules that save hours:
- HTTP status is nearly meaningless. Tracking endpoints return
200/204for almost anything, including payloads they'll silently discard. Acceptance is verified at the platform layer, not by status code. - Blocked ≠ failed. An ad-blocked request doesn't show as an error
response — it shows as
(blocked:other)/ERR_BLOCKED_BY_CLIENT, or never appears at all. Test with your own blocker off, then remember (Chapter 3) that a third of your visitors have it on. - Beacons hide.
sendBeaconrequests appear with typeping/beaconand are easy to filter out by accident. "Fetch/XHR" filter alone will miss GA4 traffic.
The platform layer
- GA4 DebugView (Admin → DebugView) — a live per-device event stream. Preview mode automatically flags your traffic as debug, so your session appears here within seconds, parameters inspectable per event. This — not tomorrow's reports — is where GA4 implementation is verified; standard reports lag 24h+ and Realtime aggregates too coarsely.
- Meta Test Events (Events Manager → Test Events) — enter the test code on your device, watch events arrive with their parameters and match keys. Also the tool for CAPI dedup verification later (Chapter 13).
- Google Ads — Goals → Conversions → status column ("Recording conversions" vs "No recent conversions") plus the per-action diagnostics tab. Slow feedback (hours), so treat it as confirmation, not as a debug loop.
- TikTok / LinkedIn — same idea, smaller tools: TikTok Events Manager test events; LinkedIn's Insight Tag status page.
Failure modes: the table
Symptom-first, because that's how bugs arrive:
| Symptom | Usual cause | Where to look |
|---|---|---|
| Works in preview, dead in production | Version never published; or consent granted in your session but denied by default | Versions list; consent defaults |
| Tag not firing | Event never pushed / name case mismatch / failed condition / exception / consent | Preview rail, questions 1–5 |
Variable undefined |
Built-in not enabled / wrong DLV path / data pushed after event | Preview → Variables tab |
| Double pageviews | gtag.js + GTM both installed / SPA initial double-fire | Network tab: two /g/collect per page |
| Purchases duplicate | Thank-you page refresh without transaction_id dedup |
Payload: dedup key present? |
| GA4 has conversions, Ads doesn't | Conversion Linker missing / click IDs stripped by a redirect | _gcl_* cookies exist? gclid in landing URL? |
| Meta events arrive, match quality poor | No advanced matching, _fbp/_fbc missing |
Payload: fbp/fbc params present? |
| Everything missing, one user/network | Ad blocker, DNS filter, corporate proxy | ERR_BLOCKED_BY_CLIENT; expected loss (Ch. 3) |
| Safari-only gaps, returning users "new" | ITP 7-day cookie cap | Nothing to fix client-side — that's Part 3 |
| Numbers differ between GA4 and platform X | Different counting: attribution windows, modeled data, timezone, dedup rules | Both platforms' methodology docs — before assuming a bug |
That last row deserves its own sentence: platforms are not supposed to match. GA4 counts key events with its attribution model; Meta counts within its click/view windows including modeled conversions; Ads dedupes differently again. Identical numbers would be the suspicious outcome. The realistic target is explainable deltas — typically within 10–20% — not equality.
When the browser is green and the data is still wrong
Sometimes all three layers check out and reality still disagrees — backend says 100 orders, GA4 says 71, Meta says 80. You've hit the structural ceiling from Chapter 3: blocked requests you never see, consent decliners, ITP-shortened identities, bounced-before-GTM visitors. No amount of client-side debugging recovers data the browser never sent.
That ceiling is the entire reason Part 3 exists. Time to move the machinery off the browser: Chapter 9 — Why Server-Side Tagging Exists.