Skip to content

Let the marketing pages count visits, and nothing else - #13

Draft
jxxyx-bloop wants to merge 1 commit into
mainfrom
claude/analytics-aiobservatory-setup-qahxnu
Draft

Let the marketing pages count visits, and nothing else#13
jxxyx-bloop wants to merge 1 commit into
mainfrom
claude/analytics-aiobservatory-setup-qahxnu

Conversation

@jxxyx-bloop

Copy link
Copy Markdown
Owner

What this does

Adds optional visitor analytics to the marketing pages only — the landing page in its thirteen locales, and /setup/. It is off in this PR, and stays off until a deployment sets ANALYTICS_ID.

Merging this changes nothing about what the site serves. The switch is an environment variable in Cloudflare, not a commit.

Why it needed engineering

The obvious version of this change is one <script> tag in <head>. That was not available here, because three things already existed and all three are deliberate:

  • the README, the landing page in thirteen languages, the PRD and ADR-002 all say "no analytics";
  • check_no_remote.py fails the build on any remote src, and the CSP is default-src 'none' with per-script hashes;
  • a measurement id committed to a public repo follows every fork.

So the change is a boundary rather than an exception: the marketing pages may count visits; the product never phones home.

The boundary, and what holds it

/demo/ and the dashboard render.py writes onto a laptop are never covered. Four independent guards, so it cannot be crossed by forgetting:

Guard Refuses
site/build.py The demo is rendered by the engine's own renderer, which is passed no tag
site/tools/check_no_remote.py --allow never applies to anything under demo/
site/tools/check_headers.py A policy naming a remote host must not be the one that lands on /demo/
site.yml Builds the analytics shape too, and greps the demo for the tag

The privacy copy is switched by the same variable as the tag, in all thirteen locales — a build cannot advertise a promise it is not keeping. With no id set, _headers is byte-for-byte what it was.

The measurement id

Read from ANALYTICS_ID at build time, never committed. Not because it is a secret — it is in the source of any page that runs one — but because an id in this repo would follow every fork, and a stranger's readers would land in this project's property with neither party knowing.

Changes

  • site/build.pyANALYTICS_ID (validated), Consent Mode v2 defaults with storage denied in the EEA/UK/CH, csp() split out so both policy blocks come from one function
  • site/i18n.pypriv_3_analytics in all thirteen locales
  • site/tools/check_headers.pynew. The header and CSP checks, lifted out of site.yml because they had to grow to read more than one policy block, and sixty lines of Python inlined in YAML is sixty lines nobody can run before pushing
  • site/tools/check_no_remote.py — per-host --allow, never applicable under demo/
  • .github/workflows/site.yml — calls the tool; adds a step that builds the analytics shape and puts it through the same gates with a fake id
  • docs/setup/ANALYTICS.mdnew. The procedure, the consent posture, and the alternatives weighed — including the one that costs nothing and keeps the promise literal
  • README.md, CONTRIBUTING.md, DESIGN-SYSTEM.md §7, DEPLOY.md — the claim restated precisely

Verification

Both build shapes, locally:

analytics off:  check: 15 pages, no remote subresources
                check: 1 header block(s), 9 inline scripts, every one pinned
analytics on:   check: 15 pages, no remote subresources beyond www.googletagmanager.com
                       — and none at all on the 1 page(s) under demo/
                check: 2 header block(s), 10 inline scripts, every one pinned; /demo/ reaches nothing

Negative cases confirmed to fail: a tag planted on the demo while the host is allowed; a /* policy reaching a host with no narrower block taking the demo back; an undeclared host; a wildcard in script-src.

Before merging

Read docs/setup/ANALYTICS.md §1 and §3 first. §1 sets expectations on what analytics can actually tell you about an audience (the demographics half is the weakest part); §3 recommends starting with Cloudflare's script-free zone analytics, which costs no code, no copy change and no promise.


Generated by Claude Code

The site has one deployment that would benefit from knowing whether anyone
arrives, and a product whose headline promise is that nothing phones home.
Those are not in conflict as long as the line between them is drawn somewhere
a person cannot forget it, so this draws it in four places rather than one.

`ANALYTICS_ID` in the build environment turns on a GA4 tag for the landing page
in its thirteen locales and for /setup/. Unset — which is what every fork and
every local build gets — there is no tag, the CSP is byte-for-byte the closed
policy it was, and the privacy line still reads "no analytics", because that is
still true for that build. The id is read from the environment rather than
committed: it is not a secret, it is in the source of any page that runs one,
but an id in a public repo follows every fork, and a stranger's readers would
land in this project's property without either of them knowing.

The demo dashboard and the file:// report are never covered. That is enforced
by construction (the demo is rendered by the engine, which is passed no tag),
by check_no_remote.py refusing to apply --allow under demo/, by the second
_headers block that takes the demo back under the closed policy, and by a CI
step that builds the analytics shape and greps the demo for the tag. The
privacy copy is switched by the same variable in all thirteen locales, so no
build can advertise a promise it is not keeping.

The header checks moved out of site.yml into site/tools/check_headers.py. They
had to grow to read more than one policy block, and sixty lines of Python
inlined in YAML is sixty lines a contributor cannot run before pushing.

docs/setup/ANALYTICS.md is the procedure, and weighs the alternatives that cost
less — including the one that costs nothing and keeps the promise literal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LAJJsknjzbsp9tKEVtQ5Y
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
ai-observatory f7c20e2 Commit Preview URL

Branch Preview URL
Aug 23 2026, 05:06 PM

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.

2 participants