From 1a7fbcb22310eae4d0da004ea4421b671e6f6abe Mon Sep 17 00:00:00 2001 From: Vishal Rana Date: Fri, 4 Sep 2026 15:16:53 -0700 Subject: [PATCH 1/6] feat(site): ask for analytics consent instead of measuring nothing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The site loaded gtag on every page and then held it in Consent Mode's denied state. That is the worst of both halves: Google's tag shipped to every reader, and no `_ga` cookie to make its numbers mean anything. Every pageview arrived as a new user in a new session, so users, sessions, bounce rate and returning visitors were all noise, and no amount of reading the reports would have said so. Nothing is loaded now until measurement is allowed, and when it loads the consent it was denied is granted, so the identifier exists and the sessions are real. Whether permission must be asked for is a question about the reader's location rather than about this site. The EEA, the UK and the Crown Dependencies require it; elsewhere measurement may run with a way to turn it off. Cloudflare already knows which applies and says so at `/cdn-cgi/trace`, so most readers are measured properly and never see a banner, and the readers who are owed the question get it. A failed lookup asks — the safe direction, and what a build outside Cloudflare gets. Do Not Track and Global Privacy Control sit above all of it, including above the site's own controls: a reader who has answered this in their browser is not asked again and is not measured either way. Withdrawal is the half that is easy to leave out, so `/privacy` carries a standing control that states what is currently happening before offering the buttons, and declining actually revokes: it sets the `ga-disable-` flag, pushes consent back to denied, and deletes the cookies acceptance set. Permission that cannot be withdrawn as readily as it was given was never permission. Two things are cut down before they leave the browser. Query strings are dropped from the recorded address, because a link can put anything after the `?` and the path is the whole of what this site wants to know. The referrer is reduced to its origin, so the site a reader came from is recorded and their search terms are not. The preference lives in localStorage rather than a cookie, so declining stores nothing that travels with a request, and it is mirrored across tabs. The banner renders on every page rather than only where the site footer does: a reader arriving from a search result lands on a documentation page, and a prompt confined to the landing page asks the question of the wrong half of the audience. `privacy` joins ORDER in both llms.txt generators, which otherwise fail the build rather than let a section go silently unindexed. Claude-Session: https://claude.ai/code/session_0188fRgp2DyMXzUcJgdMZSyu --- site/src/components/Analytics.astro | 390 ++++++++++++++---- site/src/components/AnalyticsConsent.astro | 142 +++++++ site/src/components/AnalyticsPreference.astro | 153 +++++++ site/src/components/PageFrame.astro | 2 + site/src/components/SiteFooter.astro | 22 +- site/src/content/docs/privacy.mdx | 95 +++++ site/src/pages/llms-full.txt.ts | 2 +- site/src/pages/llms.txt.ts | 3 +- 8 files changed, 722 insertions(+), 87 deletions(-) create mode 100644 site/src/components/AnalyticsConsent.astro create mode 100644 site/src/components/AnalyticsPreference.astro create mode 100644 site/src/content/docs/privacy.mdx diff --git a/site/src/components/Analytics.astro b/site/src/components/Analytics.astro index 71f6cbdd..227dfbea 100644 --- a/site/src/components/Analytics.astro +++ b/site/src/components/Analytics.astro @@ -1,12 +1,35 @@ --- -/* Privacy-respecting Google Analytics 4. +/* Consent-gated Google Analytics 4. * * The component is absent unless PUBLIC_GA_MEASUREMENT_ID is configured for a - * production build. It denies analytics/ad storage before loading gtag, turns - * off advertising signals, honors DNT and GPC before making a request, and - * drops query strings from recorded locations. That keeps local builds and - * forks silent and avoids asking for consent to store identifiers Fanout does - * not need. + * production build, so local builds and forks stay silent. + * + * Nothing is loaded until the visitor is allowed to be measured. Earlier this + * file loaded gtag on every page and then held it in Consent Mode's denied + * state, which sent a cookieless ping for every view: no `_ga` cookie, no + * stable client id, so every pageview arrived as a new user and a new session. + * That is a report in which sessions, bounce rate and returning visitors are + * all meaningless, bought at the price of shipping Google's tag to everybody. + * The tag now does not exist until it may set the cookie that makes its numbers + * mean something. + * + * Whether consent must be asked for is a question about the reader's location, + * not about this site. The EEA, the UK and the Crown Dependencies require + * permission before the cookie is set; elsewhere measurement may run with a way + * to turn it off. Cloudflare already knows which one applies and says so at + * /cdn-cgi/trace, so most readers are measured properly and never see a banner, + * and the readers who are owed the question get it. A failed or unavailable + * lookup is treated as "ask" — the safe direction, and what local development + * gets, where the whole component is compiled out anyway. + * + * Do Not Track and Global Privacy Control are honoured above all of it. A + * reader who has already answered this question in their browser is not asked + * it again, and is not measured either way. + * + * The preference lives in localStorage rather than a cookie, so declining + * stores nothing that travels with a request, and is mirrored across tabs. The + * control that changes it later is on /privacy; consent that cannot be + * withdrawn as easily as it was given is not consent. */ const measurementId = import.meta.env.PUBLIC_GA_MEASUREMENT_ID?.trim().toUpperCase(); @@ -25,92 +48,305 @@ const enabled = Boolean(measurementId) && import.meta.env.PROD; enabled && ( ) diff --git a/site/src/components/AnalyticsConsent.astro b/site/src/components/AnalyticsConsent.astro new file mode 100644 index 00000000..83798eed --- /dev/null +++ b/site/src/components/AnalyticsConsent.astro @@ -0,0 +1,142 @@ +--- +/* The consent prompt, for readers whose location requires one. + * + * Markup only. Analytics.astro decides whether this is ever shown and handles + * the clicks: the question of whether to ask is answered in the head, before + * anything renders, and putting the logic in two places is how the banner ends + * up shown to someone who already declined. + * + * It renders on every page rather than only where the site footer does, because + * a reader who arrives from a search result lands on a documentation page, and + * a consent prompt that only appears on the landing page asks the question of + * the wrong half of the audience. + * + * `hidden` in the source, not `display: none` in a stylesheet: the prompt is + * absent for the reader whose location does not require it and for the reader + * who has already answered, and neither should see it flash while a stylesheet + * loads. A build without a measurement id never renders it at all. + */ +const enabled = + Boolean(import.meta.env.PUBLIC_GA_MEASUREMENT_ID?.trim()) && import.meta.env.PROD; +--- + +{ + enabled && ( + + ) +} + + diff --git a/site/src/components/AnalyticsPreference.astro b/site/src/components/AnalyticsPreference.astro new file mode 100644 index 00000000..efed9e77 --- /dev/null +++ b/site/src/components/AnalyticsPreference.astro @@ -0,0 +1,153 @@ +--- +/* The standing analytics control, for the privacy page. + * + * The banner asks once. This is where the answer is changed afterwards, which + * is the half of consent that is easy to leave out: permission that cannot be + * withdrawn as readily as it was given was never permission. + * + * It states the current situation before offering the buttons, because "allowed + * or not" is the question a reader arrives with, and in the regions that do not + * require a prompt the honest answer is that measurement is already running. + * + * The status line is scripted rather than rendered, since the answer lives in + * the reader's browser and this site is static: every reader would otherwise be + * served the same cached sentence about a choice that is theirs alone. + */ +const enabled = + Boolean(import.meta.env.PUBLIC_GA_MEASUREMENT_ID?.trim()) && import.meta.env.PROD; +--- + +{ + enabled ? ( +
+

+ Checking whether analytics are running in this browser. +

+
+ + +
+
+ ) : ( +

+ This build of the site has no analytics configured, so nothing is measured and there is + nothing to turn off. +

+ ) +} + + + + diff --git a/site/src/components/PageFrame.astro b/site/src/components/PageFrame.astro index 255b3110..86a2c566 100644 --- a/site/src/components/PageFrame.astro +++ b/site/src/components/PageFrame.astro @@ -24,6 +24,7 @@ */ import MobileMenuToggle from "@astrojs/starlight/components/MobileMenuToggle.astro"; import SiteFooter from "./SiteFooter.astro"; +import AnalyticsConsent from "./AnalyticsConsent.astro"; const { hasSidebar } = Astro.locals.starlightRoute; const isSplash = Astro.locals.starlightRoute.entry.data.template === "splash"; @@ -45,6 +46,7 @@ const isSplash = Astro.locals.starlightRoute.entry.data.template === "splash"; }
{isSplash && } +