Skip to content

fix(site): make a refusal take effect - #227

Merged
vishr merged 2 commits into
mainfrom
fix/consent-memory-fallback
Sep 6, 2026
Merged

fix(site): make a refusal take effect#227
vishr merged 2 commits into
mainfrom
fix/consent-memory-fallback

Conversation

@vishr

@vishr vishr commented Sep 6, 2026

Copy link
Copy Markdown
Member

What changed

Two ways an explicit Do not allow failed to take effect. Both are in site/src/components/Analytics.astro; neither is new. This component is shared with onebox, which carries an identical copy; the same pair is labstack/onebox#157.

A refusal a browser would not store. readPreference() fell back to the in-memory answer only when localStorage threw. A browser can let the read succeed and refuse the write: Safari's private mode and an exhausted quota both do. There writePreference kept the choice in memory, setItem threw and was swallowed, and the next readPreference() read null back from storage and returned it. apply() then treated the reader as unanswered: the banner stayed on screen, and in a country the code classifies as not_required the fallback called mayRun("not_required", null), which is true, and loaded the tag. The stored value still wins when there is one; the change is that the answer given on this page governs it whenever storage has none of its own.

A withdrawal that never reached the open tabs. The file has claimed since it was written that the preference is mirrored across tabs. Nothing did that. The storage listener lived inside subscribe, whose only caller is the privacy page's control, and that subscriber redraws its own status text rather than re-applying the preference. A reader with a documentation page open who went to /privacy and denied kept being measured in the tab they left behind, until it happened to reload. The preference is now applied on the storage event.

Verification

  • just site-build passes.

  • The first failure needs getItem to succeed while setItem throws, which is why the original catch-only fallback missed it: nothing throws on the read path.

  • memoryPreference is assigned only in writePreference, so falling back to it cannot manufacture a grant the reader never made, and a valid stored value still wins.

  • memoryPreference starts null, so a first visit with empty storage still reads as unanswered and the banner still asks.

  • Nothing in the site removes the key (no removeItem, no localStorage.clear), so a persisted answer is never revoked out from under an open page.

  • The privacy page's control reads through the exposed preference API, which is readPreference itself, so the banner and the control cannot disagree.

  • apply() on the storage event handles both directions: a denied written elsewhere calls stop() and hides the banner, a granted calls start().

  • just check — site build verified via just site-build; the change is a single site component with no Go, API, or UI surface.

  • just test-race when auth, API, ingest, query, MCP, or agent paths changed — not applicable, none touched

  • User-facing behavior and configuration docs are current

  • No credentials, private telemetry, host details, or enterprise-only source are included

  • API, migration, ingest, MCP/AG-UI, or release-contract changes are called out — none

https://claude.ai/code/session_013MqwrF5NA179khDtEQdib5

readPreference fell back to the in-memory answer only when localStorage threw.
A browser can let the read succeed and refuse the write — Safari's private
mode and a full quota both do — and there the stored value came back null, so
an explicit "Do not allow" was read as no answer at all: the banner stayed up
and, outside a prior-consent country, the tag loaded anyway.

The answer given on this page now governs it whenever storage has none of its
own, however storage failed to keep it.

Claude-Session: https://claude.ai/code/session_013MqwrF5NA179khDtEQdib5
This file has claimed since it was written that the preference is mirrored
across tabs. Nothing did that. The storage listener lived inside subscribe,
whose only caller is the privacy page's control, and that subscriber redraws
its own status text rather than re-applying the preference.

So a reader with a documentation page open who went to /privacy and denied
kept being measured in the tab they left behind, until it happened to reload.
Applying the preference on the storage event is what makes the claim true.

Claude-Session: https://claude.ai/code/session_013MqwrF5NA179khDtEQdib5
@vishr vishr changed the title fix(site): keep a refusal a browser would not store fix(site): make a refusal take effect Sep 6, 2026
@vishr
vishr merged commit 05df840 into main Sep 6, 2026
8 checks passed
@vishr
vishr deleted the fix/consent-memory-fallback branch September 6, 2026 14:05
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