The GA4 dashboard had been flat for four days. Not declining. Flat. Zero sessions, zero page views, zero events. Two fixes had already merged and deployed: one addressing a content-type misconfiguration that was preventing iOS Safari from rendering the page at all, one adding an explicit Consent Mode v2 signal. Both diagnosed real faults. Neither moved the graph.
That was 2026-06-29.
Two real problems
captainrandom.co.uk is a statically-exported Next.js 15 build, deployed via GitHub Actions to a cPanel host over FTP. The analytics issue surfaced on 2026-06-25. Two faults emerged in sequence.
The first: LiteSpeed was serving Next.js RSC .txt payloads without the correct content type. Direct browser requests to those URLs, iOS Safari hitting /index.txt, received a raw 16 KB RSC JSON payload and downloaded it as a file rather than rendering the homepage. That isn’t a degraded analytics experience. That’s no page rendered at all. PR #177 fixed it: serve RSC .txt payloads as text/x-component, redirect browser hits to the HTML document.
After PR #177 landed, Playwright confirmed gtag.js was loading and dataLayer was populating. The scripting was present and executing. GA4 was still receiving zero events.
The second fault: no explicit Consent Mode v2 signal. GA4 doesn’t assume consent where none is declared; without a default, events are withheld. PR #178 added the explicit signal.
Both diagnoses were correct. Both PRs merged 2026-06-25.
Four days
GA4 stayed flat.
The first interpretation: the deploy hadn’t taken, or a regression had crept in alongside one of the fixes. I ruled that out on 2026-06-29. I traced the deployment chain, confirmed the build was current, saw nothing had regressed. The infrastructure was fine.
That shifted the question to the code, to what PR #178 had actually shipped.
The ordering rule
Consent Mode v2 has a constraint Google’s documentation doesn’t surface prominently: the consent default must be set in the <head> before gtag.js loads. Not alongside it. Not in a subsequent render pass. Before.
When gtag.js initialises, it reads the consent state present at that moment. If no default has been set, it makes its own assumptions, and those are locked in for the session. A consent signal that arrives after gtag.js has already run is, from GA4’s perspective, beside the point. The decision was already made.
PR #178 added the consent default in the wrong position. gtag.js ran first, found no default, and suppressed every event that followed. Four days of zero data.
dataLayer populated correctly. gtag.js loaded. Browser-side execution was fine; the ordering was wrong.
PR #215
PR #215 made two changes: it hoisted the Consent Mode v2 default into the <head> before gtag.js loads, and it ensured gtag.js loads for every visitor, not only those who had previously interacted with a consent surface.
Merged 21:53 GMT on 2026-06-29. Deployed via GitHub Actions run #28405164050; FTP completed 21:54:57 GMT. Fresh incognito tab, DevTools open on reload. Events flowing.
What I’d tell past-me
Both early fixes were worth shipping. PR #177 unblocked iOS Safari users who couldn’t render the page at all. PR #178 correctly identified that an explicit consent signal was missing. The mistake wasn’t the diagnosis of either problem. It was assuming a correct diagnosis implied a correct implementation, and not questioning that for four days.
Playwright confirming gtag.js and dataLayer is not the same as confirming events reach GA4. Playwright surfaces browser-side execution state. The consent suppression happens between the browser and GA4’s collection endpoint, a layer Playwright doesn’t surface. GA4’s own realtime view would have shown the problem on day one. When the symptom involves analytics delivery rather than analytics execution, verify at the delivery endpoint, not the execution context.
When a fix has merged and the symptom hasn’t moved, look at the fix itself sooner. Four days passed before attention turned from the deployment infrastructure to the code PR #178 had shipped. The deploy chain was fine; it almost certainly was going to be fine. The instinct to confirm infrastructure before questioning the code extended the outage unnecessarily.
Consent Mode v2 ordering is binary. The default either lands in the <head> before gtag.js initialises, or it doesn’t count. In a framework where components hydrate after the initial HTML is served, the consent block must be in the server-rendered <head>: synchronous, early, and present before any analytics script runs. There’s no equivalent of “close enough.”
Why four days
Three things ran together.
There were two real problems, each with its own fix. When both fixes merge and the symptom persists, the natural inference is that one of them didn’t apply cleanly, not that one applied in the wrong position. Getting from “the fix didn’t land” to “the fix landed but in the wrong order” required explicitly ruling out the infrastructure explanation first.
The verification tooling confirmed the wrong layer. gtag.js loading and dataLayer populating reads as a pass, and it was genuine browser-side behaviour, correct as far as it went. But the gap between browser-side execution and GA4 receiving events isn’t visible in Playwright.
GA4 standard reports have variable lag. A flat line on day one is ambiguous; data lag is a credible explanation. On day four it isn’t. The early ambiguity gives a legitimate reason to wait, and it also delays when the investigation should start.
Out of scope: whether the four days of missing data is recoverable, and the specifics of GA4’s consent state persistence across sessions. The reporting gap is permanent. The fix is correct.


