Skip to content
Development

A one-line token swap that found a broken button and a GDPR gap

By Victor Da Luz
securityanalyticsdev-logsite

This looked like the smallest possible ticket: Cloudflare Web Analytics was auto-injecting its tracking beacon at the edge, bypassing this site’s own consent gate. Turn off the auto-inject in the dashboard, swap a placeholder token for a real one in analytics.ts, done. One dashboard click, one string replacement.

Instead it turned into the most productive bug hunt I’ve had on this site in weeks, and neither bug it found was the one I went looking for.

The dashboard had moved

I went in from memory - Analytics & Logs, a toggle, done. The actual dashboard matched none of it. Cloudflare had reorganized the Web Analytics page since I last looked at it. Rather than guess again, I pulled the actual current docs and got the real path: Manage RUM Settings → “Enable with JS Snippet installation.” Small thing, but it’s the difference between confidently wrong and just confirming what’s true before acting on it.

The token swap itself was fine. Verifying it wasn’t.

The code change really was one line: placeholder string out, real token in, plus a type: "module" attribute to match Cloudflare’s current snippet format (they’d switched from defer at some point too). astro check ran clean. That’s normally where I’d call it done.

But this is a security-relevant change - a script either fires without consent or it doesn’t - so I ran a full browser-verify pass instead of trusting the type checker. First attempt, against npm run dev: a Content-Security-Policy violation, completely unrelated to anything I’d touched. Second attempt, against a real production build (npm run build && npm run preview): a different CSP violation. Same category of error, two different causes, neither one mine.

The dev-server one turned out to be a red herring - Astro bundles inline scripts differently in dev mode than in a production build, so a CSP hash computed against dev output will never match what actually ships. Lesson bookmarked: never trust a CSP check against astro dev. Always build and preview first.

The one that wasn’t a red herring

The production-build violation didn’t go away. I traced it to GamesSection.astro’s inline click handler - the script that reveals the Planetary Pathways game iframe when you hit “Launch.” Its hash is hardcoded into public/_headers, set once back when the security headers first went in. An unrelated i18n pass on that same component, two days earlier, had changed the script’s compiled output just enough to invalidate that hash. Nothing caught it - not astro check, not the build, not any test. The only symptom was a console-only CSP error and a button that did nothing when clicked.

I stashed my analytics change and rebuilt to make sure I wasn’t imagining a connection. Same violation, unmodified main. Confirmed: the game launch button had been silently dead in production for two days, and the only way anyone would ever notice is by opening devtools on the right page at the right time. Filed as its own bug rather than folding a fix into an unrelated ticket - the two problems have nothing to do with each other, they just happened to share a browser tab.

A question worth asking twice

Partway through, I stopped to ask a question I’d been putting off: does the consent library handle anything EU-specific? My first instinct was to check for geo-detection logic - there isn’t any, by design. The library’s answer to “EU-specific” is “there’s no EU-specific anything, because everyone gets opt-in by default,” which is a stronger bar than most geo-gated consent banners clear.

But that wasn’t actually the gap. GDPR’s informed-consent standard expects a visitor to be able to see what’s collected and why, typically via a linked privacy policy right there in the consent prompt. The library’s prompt config has a message, an accept button, a decline button - no field for a link at all. And imperfectsystems.com doesn’t have a site-wide privacy policy to link to even if it did. vdaluz.com has one, but its own consent config doesn’t link to it either, so the gap isn’t really about this site, it’s in the shared library both sites depend on. Filed as its own follow-up instead of trying to design and ship a fix inline.

What I’d keep doing

None of this - the dashboard correction, the broken button, the consent gap - would have surfaced from a green typecheck. Every one of them needed an actual browser, actual network requests, and a real production build instead of the dev server. The one-line ticket took the better part of a session, but the alternative was shipping a “done” that quietly left a two-day-old broken feature in place and never asked whether the privacy prompt was legally sufficient. Slower, but the kind of slower that’s worth it.

Related reading

Development

The bug behind the bug

A broken GitHub widget traced to a paused upstream demo - and, found along the way, a hash-pinned CSP silently invalidated by a one-line accessibility fix.

Read