Skip to content

site: narrow the analytics gate to what is actually asked of it - #228

Merged
vishr merged 4 commits into
mainfrom
site/relax-analytics-gates
Sep 6, 2026
Merged

site: narrow the analytics gate to what is actually asked of it#228
vishr merged 4 commits into
mainfrom
site/relax-analytics-gates

Conversation

@vishr

@vishr vishr commented Sep 6, 2026

Copy link
Copy Markdown
Member

Relaxes the consent gate to the one restriction that is actually required of it, and fixes two things review found along the way.

Do Not Track is no longer consulted. The W3C withdrew the specification in 2019, Safari removed the header, and the browsers that still carry the setting leave it off by default. Honouring it was honouring an accident of browser build, not a decision.

Global Privacy Control is now a default rather than a veto. A reader who sends one and has said nothing here is still treated as having declined, and is still not asked. What it no longer does is outrank the reader: Brave and DuckDuckGo send GPC by default, so the old ordering refused a reader who explicitly clicked Allow, on the strength of a preference their browser expressed for them.

The refusal branch at the bottom of the file goes with that, and with it the revoke ported from onebox's 3fa596f. It existed to delete an identifier left behind when a granted reader later switched a signal on; a granted reader is now measured with the signal on, so the case cannot arise.

The Crown Dependencies stay on the prior-consent list. They were removed and then restored — the statutory argument for removing them (no PECR equivalent in Guernsey, the Isle of Man or Jersey) may be right, but their data protection authorities publish cookie guidance asking for consent anyway, and three jurisdictions of negligible traffic are not worth being right about.

Also fixed:

  • subscribe did not treat a null storage key as clear(), so /privacy could go on displaying an answer the browser no longer held. onebox has had this since 8146452.
  • Review catches: a comment that sent the next reader to mayRun as the decision function, which never consults the signal; dead :disabled CSS; a 125-column line.

The privacy page and the standing control on it say all of this. The buttons there are no longer disabled underneath a sentence explaining that they do not matter.

Verification

astro check and the build pass. The script was extracted from the built dist/index.html and driven with the trace lookup and gtag stubbed, over the nine cases that define the gate:

GPC loc stored GA banner
no US runs no
no DE no yes
no GB no yes
no JE no yes
no XX no yes
no US denied no no
yes US no no
yes US granted runs no
yes DE granted runs no

No new infrastructure. /cdn-cgi/trace is untouched, so the site still degrades safely on a host that does not provide it.

https://claude.ai/code/session_01BKURdfwTvLLaBeDUppLHxx

The gate refused three things beyond the one the law requires, and each was
costing readers that the numbers then had to be read without.

Do Not Track is gone. The W3C withdrew the specification in 2019, Safari
removed the header, and the browsers that still carry the setting leave it off,
so a site that honours it is not honouring a decision -- it is honouring an
accident of which browser build the reader happens to run. There is no
obligation behind it and no intent expressed by it.

Global Privacy Control stays, but as a default rather than a veto. It is a
recognised opt-out signal in several US states and worth respecting where the
reader has said nothing else, so a reader who sends one and has not used the
control is treated as having declined, and is not asked either. What it no
longer does is outrank the reader. Brave and DuckDuckGo send GPC by default, so
the old ordering meant a reader who explicitly clicked Allow was still refused
by a preference their browser had expressed on their behalf -- the site
overriding a person with a default, while claiming to do the opposite.

Guernsey, the Isle of Man and Jersey leave the prior-consent list. Their data
protection law follows the GDPR, but none of them enacted a PECR equivalent,
and it is PECR, not the GDPR, that governs storing an identifier on a device.
They were on the list out of caution rather than because the rule reached them.

The refusal branch at the bottom of the file goes with them, and with it the
revoke that 3fa596f added upstream in onebox. That code existed to delete an
identifier left behind when a granted reader later switched a signal on. A
granted reader is now measured with the signal on, so nothing is left behind to
delete, and the case it guarded cannot arise. The stop() that remains is in
apply(), where a signal without a stored answer clears an identifier whose
preference has since been cleared.

The privacy page and the standing control on it say all of this. The buttons
there are no longer disabled underneath a sentence explaining that they do not
matter, because they now do.

Driven against the script extracted from the built dist/index.html, with the
trace lookup and gtag stubbed, over the nine cases that define the gate:

  no GPC  US  no answer     GA runs, no banner
  no GPC  DE  no answer     no GA, banner
  no GPC  GB  no answer     no GA, banner
  no GPC  JE  no answer     GA runs, no banner
  no GPC  XX  no answer     no GA, banner
  no GPC  US  denied        no GA, no banner
  GPC     US  no answer     no GA, no banner
  GPC     US  granted       GA runs, no banner
  GPC     DE  granted       GA runs, no banner

Claude-Session: https://claude.ai/code/session_01BKURdfwTvLLaBeDUppLHxx
The listener that re-applies the preference treats a null storage key as the
clear() case and acts on it. The one inside subscribe, which exists to redraw
the control on /privacy, did not: it compared the key to STORAGE_KEY and
nothing else.

So a reader who cleared site data in another tab left /privacy displaying the
answer they had just deleted, next to buttons whose pressed state described it.
Measurement was correct throughout -- the other listener saw the event -- but
the page said otherwise, which is the half of this feature the page exists to
get right.

onebox has carried the null case since 8146452. This is the same fix.

Claude-Session: https://claude.ai/code/session_01BKURdfwTvLLaBeDUppLHxx
Review of the two commits before this one. No behaviour changes; the harness
over the built script still passes all nine cases.

signalsOptOut's comment sent the next reader to mayRun as well as apply. mayRun
never consults the signal -- apply is the sole enforcement point -- so the
obvious next step, reusing mayRun as the decision function, would have dropped
the check and measured GPC readers again. The comment now says that, and says
not to.

The null-key note on subscribe claimed more than the change delivers. It
reaches a tab that has not recorded an answer of its own; a tab that has keeps
it, because readPreference falls back to memoryPreference and a clear()
elsewhere does not reset it. That is the right behaviour -- the reader answered
in this tab, and clearing storage in another did not withdraw it -- but the
previous commit message described the fix as broader than it is.

Dead CSS: nothing sets `disabled` on the preference buttons since the branch
that did was removed, so the `:disabled` rule and the `:not(:disabled)`
qualifier described a state the script can no longer produce.

Also rewraps a comment line the earlier edit left at 125 columns in a block
that wraps at 80.

Claude-Session: https://claude.ai/code/session_01BKURdfwTvLLaBeDUppLHxx
Removing them was argued from the statutes: Guernsey, the Isle of Man and
Jersey follow the GDPR but none enacted a PECR equivalent, and it is PECR that
makes storing something on a device the thing consent is owed for. That reading
may well be right, and it is not the whole question -- their data protection
authorities publish cookie guidance that asks for consent regardless, and a
site is answerable to the regulator before it is answerable to a reading.

The traffic involved is negligible, so the measurement given up is close to
nothing and the exposure taken on was not. Restored, with the reasoning written
down so the next person to look at the list finds an argument rather than three
country codes that seem out of place.

The rest of the relaxation is untouched. Do Not Track is still gone and Global
Privacy Control is still a default rather than a veto, which is where the
coverage was.

Harness over the built script, nine cases, JE now expecting the banner again:
all pass.

Claude-Session: https://claude.ai/code/session_01BKURdfwTvLLaBeDUppLHxx
@vishr
vishr merged commit d15701e into main Sep 6, 2026
8 checks passed
@vishr
vishr deleted the site/relax-analytics-gates branch September 6, 2026 14:51
vishr added a commit to labstack/onebox that referenced this pull request Sep 6, 2026
The same change fanout took in labstack/fanout#228, on the file both sites
grew from.

Do Not Track is no longer consulted. The W3C withdrew the specification in
2019, Safari removed the header, and the browsers that still carry the setting
leave it off by default, so honouring it honoured an accident of browser build
rather than a decision. Rule 4 in this file's header claimed to respect what a
reader had asked for; for Do Not Track there was no ask to respect.

Global Privacy Control stays, as a default rather than a veto. A reader who
sends one and has said nothing here is still treated as having declined and is
still not asked. What it no longer does is outrank the reader: Brave and
DuckDuckGo send GPC by default, so the old ordering refused a reader who
explicitly clicked Allow on the strength of a preference their browser had
expressed for them. A site whose whole argument is that it does not decide
things quietly on a reader's behalf should not have been doing that.

The refusal branch at the bottom goes, and 3fa596f's revoke with it. That
revoke deleted an identifier left behind when a granted reader later switched a
signal on. A granted reader is now measured with the signal on, so nothing is
left behind and the case cannot arise. The stop() that remains is in apply(),
where a signal without a stored answer clears an identifier whose preference
has since been cleared.

The Crown Dependencies stay on the prior-consent list. Removing them was tried
in the fanout change and reverted: the statutory argument is that none of
Guernsey, the Isle of Man or Jersey enacted a PECR equivalent, which may well
be right, but their data protection authorities publish cookie guidance asking
for consent anyway, and three jurisdictions of negligible traffic are not worth
being right about.

Review of the fanout change also caught dead `:disabled` CSS on the standing
control, left behind when the branch that disabled the buttons was removed.
Fixed here in the same commit rather than shipped and found twice.

The privacy page and the control on /privacy say all of this. The buttons there
are no longer disabled underneath a sentence explaining that they do not
matter.

Driven against the script extracted from the built dist/index.html, with the
trace lookup and gtag stubbed, over the nine cases that define the gate:

  no GPC  US  no answer   GA runs, no banner
  no GPC  DE  no answer   no GA, banner
  no GPC  GB  no answer   no GA, banner
  no GPC  JE  no answer   no GA, banner
  no GPC  XX  no answer   no GA, banner
  no GPC  US  denied      no GA, no banner
  GPC     US  no answer   no GA, no banner
  GPC     US  granted     GA runs, no banner
  GPC     DE  granted     GA runs, no banner

Claude-Session: https://claude.ai/code/session_01BKURdfwTvLLaBeDUppLHxx
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