Analytics Measurement Audit
An analytics setup can look complete in code and deliver zero events for months. Before trusting any number — or shipping any experiment that depends on one — prove the pipeline end-to-end. Every conclusion drawn from an unverified pipeline is potentially meaningless and must be re-examined once measurement is fixed.
Verify end-to-end, in production
Analytics scripts are client-injected: curl proves nothing. Drive a real browser at the production site:
- Confirm the tag loads (network request for the gtag/analytics script, correct measurement ID).
- Perform a real tracked interaction (click a CTA, switch a pricing tab).
- Confirm the event appears in
dataLayerAND as an outgoing collect/beacon network request with the right event name and parameters. - Confirm it lands in the backend (GA4 Realtime or DebugView).
A pipeline is verified only when all four hold. Repeat after every fix.
Where dead tracking hides
Hunt these in order — each one fails silently:
- Shadowed modules. Two files with the same basename and different extensions (
analytics.jsbesideanalytics.ts): the resolver may pick the stale one for every importer. Build warnings about duplicate/ambiguous modules are the tell — chase them to zero. - Env-var gates. Guards like
if (ID !== 'G-XXXXXXXXXX')reading a variable that is set nowhere silently discard every event. Grep each guard's variable against what's actually configured in each deploy environment. - Duplicate helpers. Multiple
trackEventimplementations accumulated over time — some wired to the live tag, some not. Consolidate to one typed module and delete the rest. - Never-shipped components. Tracking code in components no page imports.
Instrumentation architecture
- Instrument the listener, not the dispatch sites. If many CTAs dispatch a shared custom event that one listener handles, put the tracking call in the listener — one line covers every current and future dispatch site, instead of N copies that drift.
- Standard parameters on every event: page path plus a
placementidentifying which CTA/surface fired it. - Distinct event names per funnel stage (open vs. submit), so drop-off is measurable.
- Demand-test or experimental CTAs get their own event name so their signal is separable.
The admin layer (code can't fix this)
GA4 conversions ("key events") are an admin setting, not code. After events verifiably flow:
- Star the events that represent real conversions (allow ~24h of data before they're selectable).
- Un-star stale key events showing "No stream data" — leftovers from retired features make conversion reports lie.
- Confirm the property's data stream points at the measurement ID the site actually loads.
Deliverable
Report: what was dead and why, what was fixed (with commits), the production verification evidence for each event, remaining admin steps only the account owner can do, and which historical conclusions are invalidated by the outage window.
Worked example
Input: a marketing site with ~70 trackEvent calls across its components — and a GA4 property that had never received a single custom event.
Root cause: a stale analytics.js sitting beside analytics.ts. The module resolver served the old .js file to every importer, and its guard read a measurement-ID env var that was set in no environment — so every event was silently discarded before reaching gtag. The build had been printing 17 warnings about duplicate modules; nobody had chased them.
Fix and proof: deleted the shadow file and its one dependent dead component (build warnings 17 → 0); moved demo-CTA tracking into the single shared event listener instead of its 13 dispatch sites; then drove a real browser at production — clicking a pricing tab produced pricing_tab and the demo CTA produced demo_request_open, both visible in dataLayer and as outgoing collect beacons, then in GA4 Realtime.
Report flagged: every GA4-based conclusion from the outage window as invalid, and listed the remaining admin-only steps — starring the three real conversion events and un-starring five stale key events showing "No stream data."