fix(website): Consent Mode v2 must fire before gtag.js writes cookies#191
Merged
Conversation
PR #190 shipped a Consent Mode v2 default-deny gate, but the script ordering in <head> was wrong: @docusaurus/plugin-google-gtag injects its scripts BEFORE user-config headTags, so the rendered order was <script async src=gtag.js> (kicks off, will run later) <script>gtag("config",...)</script> (plugin-injected, sync) <script>gtag("consent","default","denied")</script> (mine — too late) dataLayer therefore got [..., config, consent default]. When gtag.js boots it processes dataLayer top-down, so it applied `config` first, initialised the tracker, wrote `_ga` and `_ga_M8NW21WY2M` cookies, and only then saw the denied consent default. Reported by inspecting Application > Cookies in an incognito session: both cookies present with 2-year expiry, before any banner click. Fix: stop using the preset's `gtag` shortcut and replicate its behaviour manually in headTags, with consent default as the very first tag. New order: 1. <script>gtag("consent","default","denied")</script> 2. <link rel=preconnect googletagmanager> 3. <link rel=preconnect google-analytics> 4. <script async src=gtag.js> 5. <script>gtag("js",...);gtag("config",..., anonymize_ip)</script> Now dataLayer is seeded with the denied consent default before any config call, so gtag.js applies the gate first and refuses to write cookies / send hits until the user clicks Accept. The preset's plugin-google-gtag also handled SPA pageview tracking via onRouteUpdate; ported that into the cookie-consent client module so route changes still fire `gtag('event', 'page_view', ...)` — gated by the current consent state, naturally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6e4d649 to
aec6104
Compare
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Hotfix for #190. The Consent Mode v2 default-deny gate was set up in
headTags, but the @docusaurus/plugin-google-gtag from the preset injects its scripts BEFORE user-configheadTags— so the rendered<head>order was:When
gtag.jsfinished loading and processeddataLayer, it sawconfigbefore the consent default → initialised the tracker → wrote_gaand_ga_M8NW21WY2Mcookies with 2-year expiry before any banner interaction.The user reported this by opening an incognito tab → DevTools → Application → Cookies and finding both already set:
Fix
Stopped using the preset's
gtagshortcut. Replicated the plugin's behaviour by hand inheadTags, in the correct order:dataLayeris now seeded with the denied consent default before anyconfigcall. Whengtag.jsboots, the gate is already applied — no cookies, no hits, until the user clicks Accept.SPA pageview tracking
The preset's plugin-google-gtag also handled SPA route changes via
onRouteUpdate(firinggtag('event','page_view',...)on navigation). Ported that into the existing cookie-consent client module so route changes still report — gated by the current consent state, of course.Test plan
Local build was verified by extracting the rendered
<head>and confirming the new order. After deploy, in a fresh incognito tab:_ga*cookies should NOT exist.collect→ no requests to*.google-analytics.com/g/collect._gaand_ga_M8NW21WY2Mcookies appear,collectrequests start, GA4 Realtime shows the visit.collectrequests never fire.collectrequests withpage_pathmatching each route (SPA pageview tracking).🤖 Generated with Claude Code