Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 44 additions & 25 deletions site/src/components/Analytics.astro
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,15 @@
* 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.
* Global Privacy Control stands in for an answer that has not been given here.
* A reader who sends one and has not used the control below is treated as
* having declined, and is not asked either. It does not outrank an answer given
* on this site: Allow is the later and more specific statement of the same
* preference, so a reader who clicks it is measured with the signal still on.
*
* Do Not Track is not consulted. The W3C withdrew the specification in 2019,
* Safari removed the header, and Chrome and Firefox leave it off by default, so
* it carries neither a legal obligation nor a reliable statement of intent.
*
* The preference lives in localStorage rather than a cookie, so declining
* stores nothing that travels with a request, and is mirrored across tabs. The
Expand Down Expand Up @@ -54,6 +60,14 @@ const enabled = Boolean(measurementId) && import.meta.env.PROD;
// The EEA, the UK and the Crown Dependencies: the places that require
// permission before a browser stores an analytics identifier. A country
// this list does not name gets measurement with an opt-out instead.
//
// Guernsey, the Isle of Man and Jersey are here on the regulators' word
// rather than on a reading of the statutes. None of the three enacted a
// PECR equivalent, and it is PECR, not the GDPR, that makes storage
// itself the thing consent is owed for -- but their data protection
// authorities publish cookie guidance that asks for consent anyway, and
// three jurisdictions of negligible traffic are not worth being right
// about. Removing them is not a decision to take from this file.
var PRIOR_CONSENT = [
"AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR", "DE",
"GR", "HU", "IE", "IT", "LV", "LT", "LU", "MT", "NL", "PL", "PT",
Expand All @@ -66,12 +80,11 @@ const enabled = Boolean(measurementId) && import.meta.env.PROD;
var listenersAttached = false;
var requirementPromise = null;

// A reader who has set either signal has answered this already.
function signalsRefusal() {
var flags = [navigator.doNotTrack, window.doNotTrack, navigator.msDoNotTrack];
for (var i = 0; i < flags.length; i++) {
if (flags[i] === "1" || flags[i] === "yes") return true;
}
// A default answer for a reader who has not given one here, rather than a
// veto over the one they do give. apply() is the only caller and the only
// place the signal is enforced; mayRun deliberately knows nothing about
// it, so do not reach for mayRun as the decision function.
function signalsOptOut() {
return navigator.globalPrivacyControl === true;
}

Expand Down Expand Up @@ -144,10 +157,6 @@ const enabled = Boolean(measurementId) && import.meta.env.PROD;
}

function start() {
// Above the stored preference on purpose: a browser-level refusal is
// not overridden by a click on this site's own control.
if (signalsRefusal()) return;

window["ga-disable-" + measurementId] = false;

// Allowing after declining, without leaving the page. The tag is
Expand Down Expand Up @@ -315,6 +324,17 @@ const enabled = Boolean(measurementId) && import.meta.env.PROD;
if (accepted) accepted.hidden = true;
return;
}
// No answer stored here. A browser-level opt-out supplies one, and a
// reader who has already refused globally is not asked again. stop()
// rather than nothing, so an identifier left by an earlier grant whose
// preference has since been cleared does not survive the signal.
if (signalsOptOut()) {
stop();
var refused = banner();
if (refused) refused.hidden = true;
return;
}

consentRequirement().then(function (requirement) {
if (readPreference() !== null) return;
if (mayRun(requirement, null)) {
Expand All @@ -331,11 +351,20 @@ const enabled = Boolean(measurementId) && import.meta.env.PROD;
preference: readPreference,
setPreference: writePreference,
requirement: consentRequirement,
refused: signalsRefusal,
optedOut: signalsOptOut,
subscribe: function (onChange) {
window.addEventListener(PREFERENCE_EVENT, onChange);
window.addEventListener("storage", function (event) {
if (event.key === STORAGE_KEY) onChange();
// A null key is clear(), where the preference is gone rather than
// changed. The listener that re-applies the preference already
// covers it; this one redraws the control that displays it.
//
// It reaches a tab that has not recorded an answer itself. A tab
// that has keeps it: readPreference falls back to memoryPreference,
// which a clear() in another tab does not reset, and should not --
// the reader answered in this tab, and clearing storage elsewhere
// did not withdraw that.
if (event.key === STORAGE_KEY || event.key === null) onChange();
});
},
};
Expand All @@ -359,16 +388,6 @@ const enabled = Boolean(measurementId) && import.meta.env.PROD;
}
}

if (signalsRefusal()) {
// Neither measured nor asked. The banner stays in the document,
// hidden, so nothing depends on it having rendered.
ready(function () {
var element = banner();
if (element) element.hidden = true;
});
return;
}

ready(function () {
var element = banner();
if (element) {
Expand Down
32 changes: 14 additions & 18 deletions site/src/components/AnalyticsPreference.astro
Original file line number Diff line number Diff line change
Expand Up @@ -60,19 +60,10 @@ const enabled =
function render() {
var preference = api.preference();

if (api.refused()) {
status.textContent =
"Your browser sends a Do Not Track or Global Privacy Control signal, so " +
"analytics do not run here whatever this control says.";
for (var i = 0; i < buttons.length; i++) buttons[i].disabled = true;
return;
}

for (var j = 0; j < buttons.length; j++) {
buttons[j].disabled = false;
buttons[j].setAttribute(
for (var i = 0; i < buttons.length; i++) {
buttons[i].setAttribute(
"aria-pressed",
String(buttons[j].dataset.consent === preference),
String(buttons[i].dataset.consent === preference),
);
}

Expand All @@ -85,6 +76,16 @@ const enabled =
return;
}

// Nothing has been answered here. A Global Privacy Control signal is the
// answer until this control gives a different one, which is why the
// buttons stay live rather than being disabled underneath the sentence.
if (api.optedOut()) {
status.textContent =
"Your browser sends a Global Privacy Control signal, so analytics are off " +
"in this browser. Allow turns them on.";
return;
}

api.requirement().then(function (requirement) {
if (api.preference() !== null) return;
status.textContent =
Expand Down Expand Up @@ -138,7 +139,7 @@ const enabled =
cursor: pointer;
}

.fo-preference-button:hover:not(:disabled) {
.fo-preference-button:hover {
border-color: var(--sl-color-gray-4);
color: var(--sl-color-white);
}
Expand All @@ -152,11 +153,6 @@ const enabled =
color: var(--sl-color-black);
}

.fo-preference-button:disabled {
opacity: 0.55;
cursor: not-allowed;
}

.fo-preference-button:focus-visible {
outline: 2px solid var(--sl-color-accent-high);
outline-offset: 2px;
Expand Down
11 changes: 8 additions & 3 deletions site/src/content/docs/privacy.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,14 @@ nothing on this page is in it.
loaded and no identifier is stored until you say yes. The prompt is the only
thing that appears before you answer.
- **Everywhere else**, analytics run and the control above turns them off.
- **If your browser sends Do Not Track or Global Privacy Control**, analytics
never run and you are never asked. That signal wins over everything else on
this page, including the buttons above.
- **If your browser sends Global Privacy Control**, analytics do not run and
you are not asked, because you have already answered. Choosing Allow above
overrides it for this browser: it is the more specific answer, and this site
does not treat a browser default as outranking your own decision.

Do Not Track is not consulted. The specification was withdrawn in 2019, Safari
removed the header, and the browsers that still send one leave it off by
default, so it says nothing reliable about what a reader wants.

Which of these applies is decided from the country Cloudflare reports for your
connection. If that lookup fails, the site asks rather than assumes.
Expand Down
Loading