site: narrow the analytics gate to what is actually asked of it - #228
Merged
Conversation
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
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
subscribedid not treat a null storage key asclear(), so /privacy could go on displaying an answer the browser no longer held. onebox has had this since8146452.mayRunas the decision function, which never consults the signal; dead:disabledCSS; 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 checkand the build pass. The script was extracted from the builtdist/index.htmland driven with the trace lookup and gtag stubbed, over the nine cases that define the gate:No new infrastructure.
/cdn-cgi/traceis untouched, so the site still degrades safely on a host that does not provide it.https://claude.ai/code/session_01BKURdfwTvLLaBeDUppLHxx