Turning Complex Problems Into Confident Technology
Twenty events a page, and GA4 got one
Integrating other people’s systems · Production · Inside the Workshop
The problem
My site sends its own events to Google Analytics as well as page views: which audience route a visitor chooses, how far they scroll, whether they click to call or email, and each step of the contact form, the CV request and the booking link. On 16 October 2026 an audit of my own analytics found that none of those events had reached GA4 since late August. Until then they had been arriving every day. From 17 September there were none at all.
Nothing had reported an error. Page views were still arriving, so the reports looked alive. What had gone was everything the site measured on purpose: the contact funnel, the CV request, booking, audience routes, and call and email clicks.
Finding it
A probe in a real browser against production, with analytics consent given, showed the shape of it. The page queued about twenty gtag events: session context, call-to-action impressions, scroll depth, engaged time and so on. GA4 received one of them, the page_view that Google Tag Manager sends itself.
The cause was a change made earlier for sound reasons. The site used to load GA4 with its own gtag('config', ...) call. That call was retired in favour of the Google tag deployed through Tag Manager. The Google tag only delivers an event that names the GA4 stream it is meant for. An event with no address stays in the dataLayer. It is never sent, and nothing anywhere records that it was dropped.
Sending one event four ways
The quickest useful test was to send the same event several ways and see which one landed:
- with
send_toset to the stream's G- id, it arrived; - with no
send_to, it did not; - with
send_toset to the Google tag's own GT- id, it did not; - after a site-side config call with its page view switched off, it did not.
That took minutes, and it answered the question directly instead of leaving me to reason about consent, ad blockers or a broken container.
What I chose, and why
Every GA4 event call in the site now names the stream. That covers the core site script, the enhanced analytics module, the contact page, the CV request, the 404 page and the home page.
// Before: queued in the dataLayer, never delivered
gtag('event', 'scroll_depth', params);
// After: the event names the GA4 stream, so the Google tag sends it
gtag('event', 'scroll_depth', Object.assign({ send_to: GA4_STREAM_ID }, params));
I considered two shorter routes and rejected both.
- Wrap
window.gtagonce so every call has the address added for it. Fewer lines change, but the routing then lives in a global monkey-patch. Anyone reading a call site would see an event with no destination and no way of knowing where it goes. - Bring back a site-side config call. The probe had already shown that it does not deliver these events either, and it would bring back a second page_view, so every page would be counted twice.
Naming the stream has a side benefit. These events can no longer drift into the Google Ads tag, which matters once leads are de-duplicated between the two.
Making it hard to break again
The site already runs a tracking lint on every build. It now fails on any gtag event without a send_to, or with one that names a different G- stream. Pointed at the unfixed source on the dev branch, it reported all 14 unaddressed calls. Google Ads conversion calls already name their own Ads destination, so the check reads it there without needing an exemption.
A browser test pins the queued commands. On 16 October, with the real Tag Manager container loaded on a local build, the batched GA4 requests carried first_interaction, cta_impression, section_dwell, scroll_depth and engaged_time. That run blocked the requests before they left the browser, so no test data reached GA4. Since 23 October the site loads Tag Manager only on the live site and nowhere else, so that local run is a record of the day rather than something a local build can repeat.
A second gap behind the first
Later the same day I found a second reason some visitors were never measured. Page code decided whether to send GA4 events by reading an old combined "marketing" consent key. That key was set to granted only when a visitor allowed both Google Analytics and Google Ads. A visitor who allowed analytics alone was never measured by the enhanced analytics module or the 404 page, although that was exactly what they had agreed to.
The module that owns the consent record now exposes two read-only checks, one for Google Analytics and one for Google Ads, and page code asks those instead of reading storage keys itself. GA4 events need GA4 consent. The contact page's Google Ads conversion keeps exactly its old gate, which needs both, because loosening an advertising gate is a decision of its own and did not belong in this fix.
Tests pinned the old behaviour first. The two analytics-only cases failed against the unchanged build and pass after the change.
What to take from this
If you have moved from your own gtag.js snippet to a Google tag deployed through Tag Manager, check your custom events specifically. Page views carry on arriving, so the reports keep looking healthy while everything else has stopped.
- A dropped event leaves no error. Watch for absence: a custom event that falls to zero is a signal on its own.
- When something does not arrive, send it more than one way. Comparing four variants of one event took minutes and removed the guesswork.
- Put the rule where the build can enforce it. A note in a README would not have caught the 14 calls. A lint did.
- Read your consent gates as carefully as your delivery. A combined key quietly turned "allowed analytics" into "not measured".
Related
If this is your problem too
Measurement that quietly stops is an integration failure like any other, and finding where a pipeline drops data without saying so is what an integration review is for. Another set of figures that looked right and were not is in a dashboard is only as honest as its labels.
Got a problem like this?
A free 20-minute call: you’ll know whether I can help, what is probably wrong, and what the next step involves. No obligation.