-
Notifications
You must be signed in to change notification settings - Fork 0
feat(site): ask for analytics consent instead of measuring nothing #154
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+737
−49
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
ded027f
feat(site): ask for analytics consent instead of measuring nothing
vishr 05e7153
fix(deps): take the x/crypto SSH deadlock fixes
vishr c8266c7
docs(site): say what rule 2 actually promises
vishr 3fa596f
fix(site): revoke the identifier when the browser starts refusing
vishr 8146452
fix(site): carry a withdrawal to the tab that is being measured
vishr df7e187
fix(site): keep the privacy control parseable, and stop overclaiming …
vishr File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
Large diffs are not rendered by default.
Oops, something went wrong.
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| --- | ||
| /* The consent prompt, for readers whose location requires one. | ||
| * | ||
| * Markup only. Analytics.astro decides whether this is ever shown and handles | ||
| * the clicks: this renders hidden and is revealed by that script alone, once it | ||
| * has either a stored preference or an answer from the location lookup. Putting | ||
| * the logic in two places is how the banner ends up shown to someone who | ||
| * already declined. | ||
| * | ||
| * It renders on every page rather than only where the site footer does, because | ||
| * a reader who arrives from a search result lands on a documentation page, and | ||
| * a consent prompt that only appears on the landing page asks the question of | ||
| * the wrong half of the audience. | ||
| * | ||
| * `hidden` in the source, not `display: none` in a stylesheet: the prompt is | ||
| * absent for the reader whose location does not require it and for the reader | ||
| * who has already answered, and neither should see it flash while a stylesheet | ||
| * loads. A build without a measurement id never renders it at all. | ||
| */ | ||
| const enabled = | ||
| Boolean(import.meta.env.PUBLIC_GA_MEASUREMENT_ID?.trim()) && import.meta.env.PROD; | ||
| --- | ||
|
|
||
| { | ||
| enabled && ( | ||
| <section | ||
| id="ob-analytics-consent" | ||
| class="ob-consent" | ||
| role="region" | ||
| aria-labelledby="ob-consent-title" | ||
| hidden | ||
| > | ||
| <div class="ob-consent-body"> | ||
| <p id="ob-consent-title" class="ob-consent-title">Analytics on this site</p> | ||
| <p class="ob-consent-copy"> | ||
| Onebox would like to count visits and which pages get read, using Google Analytics. | ||
| Nothing about the servers you deploy to is involved. <a href="/privacy"> | ||
| What is collected | ||
| </a>. | ||
| </p> | ||
| </div> | ||
| <div class="ob-consent-actions"> | ||
| <button type="button" class="ob-consent-button" data-consent="denied"> | ||
| Do not allow | ||
| </button> | ||
| <button | ||
| type="button" | ||
| class="ob-consent-button ob-consent-button-primary" | ||
| data-consent="granted" | ||
| > | ||
| Allow | ||
| </button> | ||
| </div> | ||
| </section> | ||
| ) | ||
| } | ||
|
|
||
| <style> | ||
| .ob-consent { | ||
| position: fixed; | ||
| z-index: calc(var(--sl-z-index-navbar) + 1); | ||
| inset-block-end: max(1rem, env(safe-area-inset-bottom)); | ||
| inset-inline: 1rem; | ||
| margin-inline: auto; | ||
| max-width: 34rem; | ||
| display: flex; | ||
| flex-wrap: wrap; | ||
| align-items: center; | ||
| justify-content: space-between; | ||
| gap: 0.75rem 1.5rem; | ||
| padding: 1rem 1.15rem; | ||
| border: 1px solid var(--sl-color-gray-5); | ||
| border-radius: 0.6rem; | ||
| background: var(--sl-color-black); | ||
| box-shadow: 0 1rem 2.5rem -0.75rem rgb(0 0 0 / 0.5); | ||
| } | ||
|
|
||
| .ob-consent-title { | ||
| margin: 0 0 0.2rem; | ||
| font-size: var(--sl-text-sm); | ||
| font-weight: 600; | ||
| color: var(--sl-color-white); | ||
| } | ||
|
|
||
| .ob-consent-copy { | ||
| margin: 0; | ||
| font-size: var(--sl-text-xs); | ||
| line-height: 1.6; | ||
| color: var(--sl-color-gray-3); | ||
| } | ||
|
|
||
| .ob-consent-body { | ||
| flex: 1 1 16rem; | ||
| } | ||
|
|
||
| .ob-consent-actions { | ||
| display: flex; | ||
| gap: 0.5rem; | ||
| } | ||
|
|
||
| .ob-consent-button { | ||
| padding: 0.4rem 0.85rem; | ||
| border: 1px solid var(--sl-color-gray-5); | ||
| border-radius: 0.35rem; | ||
| background: transparent; | ||
| color: var(--sl-color-gray-2); | ||
| font: inherit; | ||
| font-size: var(--sl-text-xs); | ||
| cursor: pointer; | ||
| } | ||
|
|
||
| .ob-consent-button:hover { | ||
| border-color: var(--sl-color-gray-4); | ||
| color: var(--sl-color-white); | ||
| } | ||
|
|
||
| /* The landing page's primary action, in miniature: a solid fill of the | ||
| token that contrasts with the page, labelled in the token that contrasts | ||
| with the fill. `--sl-color-white` is a role rather than a colour — it is | ||
| near-black under the light theme — so a label set in it on an accent fill | ||
| is dark-on-blue in daylight. The pair below inverts correctly in both. */ | ||
| .ob-consent-button-primary { | ||
| border-color: var(--sl-color-white); | ||
| background: var(--sl-color-white); | ||
| color: var(--sl-color-black); | ||
| } | ||
|
|
||
| .ob-consent-button-primary:hover { | ||
| border-color: var(--sl-color-gray-1); | ||
| background: var(--sl-color-gray-1); | ||
| color: var(--sl-color-black); | ||
| } | ||
|
|
||
| .ob-consent-button:focus-visible { | ||
| outline: 2px solid var(--sl-color-accent-high); | ||
| outline-offset: 2px; | ||
| } | ||
|
|
||
| /* Stacked, the buttons want the full width rather than the right edge. */ | ||
| @media (max-width: 30rem) { | ||
| .ob-consent-actions { | ||
| flex: 1 1 100%; | ||
| } | ||
|
|
||
| .ob-consent-button { | ||
| flex: 1; | ||
| } | ||
| } | ||
| </style> |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,170 @@ | ||
| --- | ||
| /* The standing analytics control, for the privacy page. | ||
| * | ||
| * The banner asks once. This is where the answer is changed afterwards, which | ||
| * is the half of consent that is easy to leave out: permission that cannot be | ||
| * withdrawn as readily as it was given was never permission. | ||
| * | ||
| * It states the current situation before offering the buttons, because "allowed | ||
| * or not" is the question a reader arrives with, and in the regions that do not | ||
| * require a prompt the honest answer is that measurement is already running. | ||
| * | ||
| * The status line is scripted rather than rendered, since the answer lives in | ||
| * the reader's browser and this site is static: every reader would otherwise be | ||
| * served the same cached sentence about a choice that is theirs alone. | ||
| */ | ||
| const enabled = | ||
| Boolean(import.meta.env.PUBLIC_GA_MEASUREMENT_ID?.trim()) && import.meta.env.PROD; | ||
| --- | ||
|
|
||
| { | ||
| enabled ? ( | ||
| <> | ||
| <p> | ||
| The site uses Google Analytics 4 to count visits and see which pages get read. It runs | ||
| only when it is allowed to, and it is configured to learn as little as will still | ||
| answer that question. | ||
| </p> | ||
| <div id="ob-analytics-preference" class="ob-preference"> | ||
| <p class="ob-preference-status" aria-live="polite"> | ||
| Checking whether analytics are running in this browser. | ||
| </p> | ||
| <div class="ob-preference-actions"> | ||
| <button type="button" class="ob-preference-button" data-consent="denied"> | ||
| Do not allow | ||
| </button> | ||
| <button type="button" class="ob-preference-button" data-consent="granted"> | ||
| Allow | ||
| </button> | ||
| </div> | ||
| </div> | ||
| </> | ||
| ) : ( | ||
| <p> | ||
| This build of the site has no analytics configured, so nothing is measured, nothing is | ||
| stored, and there is nothing to turn off. The rest of this section describes what a build | ||
| that has them does. | ||
| </p> | ||
| ) | ||
| } | ||
|
|
||
| <script is:inline> | ||
| (function () { | ||
| var root = document.getElementById("ob-analytics-preference"); | ||
| var api = window.oneboxAnalytics; | ||
| if (!root || !api) return; | ||
|
|
||
| var status = root.querySelector(".ob-preference-status"); | ||
| var buttons = root.querySelectorAll("[data-consent]"); | ||
|
|
||
| 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; | ||
| // No trailing comma after the last argument: `is:inline` ships this | ||
| // script verbatim, and a trailing comma in a call is ES2017. Every | ||
| // browser too old for it fails to parse the whole script rather than | ||
| // one line of it, which would leave this control inert -- a standing | ||
| // opt-out that renders and does nothing is the worst of the states | ||
| // this file can be in. | ||
| buttons[j].setAttribute( | ||
| "aria-pressed", | ||
| String(buttons[j].dataset.consent === preference) | ||
| ); | ||
| } | ||
|
|
||
| if (preference === "granted") { | ||
| status.textContent = "Analytics are allowed in this browser."; | ||
| return; | ||
| } | ||
| if (preference === "denied") { | ||
| status.textContent = "Analytics are not allowed in this browser."; | ||
| return; | ||
| } | ||
|
|
||
| api.requirement().then(function (requirement) { | ||
| if (api.preference() !== null) return; | ||
| status.textContent = | ||
| requirement === "not_required" | ||
| ? "Analytics are running in this browser. You can turn them off here." | ||
| : "You have not yet answered the analytics question in this browser."; | ||
| }); | ||
| } | ||
|
|
||
| root.addEventListener("click", function (event) { | ||
| var button = event.target instanceof Element && event.target.closest("[data-consent]"); | ||
| if (!button) return; | ||
| api.setPreference(button.dataset.consent); | ||
| }); | ||
|
|
||
| api.subscribe(render); | ||
| render(); | ||
| })(); | ||
| </script> | ||
|
|
||
| <style> | ||
| .ob-preference { | ||
| margin-block: 1rem; | ||
| padding: 1rem 1.15rem; | ||
| border: 1px solid var(--sl-color-gray-5); | ||
| border-radius: 0.6rem; | ||
| background: var(--sl-color-black); | ||
| } | ||
|
|
||
| .ob-preference-status { | ||
| margin: 0; | ||
| font-size: var(--sl-text-sm); | ||
| line-height: 1.6; | ||
| color: var(--sl-color-gray-2); | ||
| } | ||
|
|
||
| .ob-preference-actions { | ||
| display: flex; | ||
| gap: 0.5rem; | ||
| margin-block-start: 0.85rem; | ||
| } | ||
|
|
||
| .ob-preference-button { | ||
| padding: 0.4rem 0.85rem; | ||
| border: 1px solid var(--sl-color-gray-5); | ||
| border-radius: 0.35rem; | ||
| background: transparent; | ||
| color: var(--sl-color-gray-2); | ||
| font: inherit; | ||
| font-size: var(--sl-text-xs); | ||
| cursor: pointer; | ||
| } | ||
|
|
||
| .ob-preference-button:hover:not(:disabled) { | ||
| border-color: var(--sl-color-gray-4); | ||
| color: var(--sl-color-white); | ||
| } | ||
|
|
||
| /* Same inversion as the banner's primary action: `--sl-color-white` is the | ||
| token that contrasts with the page, so it is the fill here and never the | ||
| label on top of it. */ | ||
| .ob-preference-button[aria-pressed="true"] { | ||
| border-color: var(--sl-color-white); | ||
| background: var(--sl-color-white); | ||
| color: var(--sl-color-black); | ||
| } | ||
|
|
||
| .ob-preference-button:disabled { | ||
| opacity: 0.55; | ||
| cursor: not-allowed; | ||
| } | ||
|
|
||
| .ob-preference-button:focus-visible { | ||
| outline: 2px solid var(--sl-color-accent-high); | ||
| outline-offset: 2px; | ||
| } | ||
| </style> |
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
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
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
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not what happens — Astro scopes this rule, so it already reaches only the Privacy link.
.meta acompiles to a selector carrying this component's scope on both elements:And in the built page,
EditLink's anchor carries its own scope class, not this one:Scoped styles don't cross into child components, which is exactly why line 75 needs
:globalto reach the<p>thatLastUpdatedrenders. The same boundary is what keeps this rule offEditLink, so "one text link, not the whole band" is the behaviour and not just the intent. Adding an explicit class here would restate a scope the compiler already applies.