Skip to content

Load gtag.js only after the visitor accepts - #218

Merged
korya merged 1 commit into
masterfrom
korya-fix-consent-before-tag
Sep 19, 2026
Merged

korya merged 1 commit into
masterfrom
korya-fix-consent-before-tag

Conversation

@korya

@korya korya commented Sep 18, 2026

Copy link
Copy Markdown
Member

Problem

The demo now reports visitors to Google who pressed Decline. Since #216 made the GA4 tag work, it runs in Google's advanced consent mode: loaded for everyone with storage denied, sending cookieless pings that still carry page URL, referrer, device and IP — for people who said no.

The privacy policy promises the opposite, in as many words:

"Until you accept, no analytics cookies are set and no measurement data is collected."
"Google Analytics receives demo-site usage data, but only from visitors who accepted analytics. Decline and it receives nothing."

The contradiction was dormant while the tag was inert. It went live when #216 merged, which is why this follows immediately behind it.

Solution

Load gtag.js only once the visitor accepts, so nothing Google-bound is requested before that — not the collector, and not the script itself.

Before / after, for a visitor who declines:

before:  page load → gtag.js fetched → cookieless page_view to Google  (gcs=G100)
after:   page load → nothing

The accepting path is unchanged in substance: the same batch is queued — denial default → grant → js → config — and then the tag is fetched, measuring identically at gcs=G101, with ad storage still denied for someone who only agreed to analytics. I verified that ordering end to end in a live browser before writing the change. This also matches qr-code-gen.frontsail.app, whose implementation already worked this way.

What we give up is Google's behavioural modelling of non-consenting visitors, which needs 1,000 analytics_storage='denied' events per day for 7 days before it engages. The demo is orders of magnitude below that, so those pings bought nothing and cost a promise.

bootCommands and loadTag lose their stored parameter — both are now reached only after consent — so the grant is unconditional and grantConsent is gone. Two unit tests went with it: "an unanswered visitor is never silently granted" and "a returning refuser stays denied with no update" asserted branches that no longer exist. The guarantee they approximated is now checked where it is observable: e2e asserts that a decliner, and a visitor who ignores the banner, produce no Google-bound request at all — on that visit or the next.

Verified by breaking the code it guards: reverting the gate in main.ts fails exactly those two silence tests and nothing else.

No visual change — the banner, its copy, and the stored-choice behaviour are untouched.

Other Changes

  • DEMO-19 now states both halves of the contract: that measurement happens for an accepting visitor, and that silence is real for everyone else. It also records why advanced consent mode was rejected, so the next person doesn't "upgrade" back into the contradiction.

Related:

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

🚀 Preview deployed for e36ac33: https://aspicio-demo-m3evgisx9-dmitri-k-s-projects.vercel.app (Vercel login required)

@github-actions

github-actions Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

🛠️ API preview deployed for e36ac33: https://aspicio-c4admxmg2-dmitri-k-s-projects.vercel.app (Vercel login required)

@korya
korya marked this pull request as ready for review September 18, 2026 19:16
@korya
korya force-pushed the korya-fix-consent-before-tag branch from c746bdf to 820d434 Compare September 18, 2026 19:22
The privacy policy says a visitor who declines is not reported to Google: "Until you
accept, no analytics cookies are set and no measurement data is collected." Advanced
consent mode contradicts that. It loads the tag for everyone with storage denied, and
those cookieless pings still carry page URL, referrer, device and IP to Google — for
people who said no. The contradiction was dormant only because the tag never worked at
all; the previous commit would have made it live.

Previously the tag loaded on every visit to the production host and `grantConsent()`
lifted the denial on accept. Now nothing Google-bound is requested until the visitor
accepts, and accepting loads the tag with the full batch — denial, grant, `js`, `config`
— which measures identically: `gcs=G101`, ad storage still denied. This matches
qr-code-gen.frontsail.app, whose implementation already worked this way.

What we give up is Google's behavioural modelling of non-consenting visitors, which needs
1,000 denied events per day for 7 days before it engages. The demo is orders of magnitude
below that, so those pings bought nothing and cost a promise.

`bootCommands` and `loadTag` lose their `stored` parameter: both are now reached only
after consent, so the grant is unconditional and `grantConsent` is gone. Two unit tests
went with it — "an unanswered visitor is never silently granted" and "a returning refuser
stays denied with no update" asserted branches that no longer exist. The guarantee they
approximated is now checked where it is observable, in e2e: a visitor who declines or
ignores the banner produces no request to Google at all, on that visit or the next.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@korya
korya force-pushed the korya-fix-consent-before-tag branch from 820d434 to 3da50cd Compare September 18, 2026 19:32
@korya
korya merged commit 084efff into master Sep 19, 2026
4 checks passed
@korya
korya deleted the korya-fix-consent-before-tag branch September 19, 2026 15:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant