From 0e445469d54f9e15bf19318c4918a1d5b27197ff Mon Sep 17 00:00:00 2001 From: Alex Leventer Date: Thu, 8 Oct 2026 16:02:35 -0700 Subject: [PATCH 1/3] Add PostHog and GA4 behind the consent banner Ports landing's posthog-consent.js to Mintlify as one self-contained file: it loads the vendored PostHog bundle from fly.io, asks fly.io/api/consent-scope for the EEA/UK and logged-in checks, and shows the same banner. Pageviews are captured on client-side route changes. A Legal footer adds a Cookie preferences link to reopen the banner. Refs superfly/docs-tracking#304 Co-Authored-By: Claude Opus 5.5 --- docs.json | 17 ++++ fly-analytics.js | 249 +++++++++++++++++++++++++++++++++++++++++++++++ styles.css | 79 +++++++++++++++ 3 files changed, 345 insertions(+) create mode 100644 fly-analytics.js diff --git a/docs.json b/docs.json index 0a13cbe8c1..8b996cedfb 100644 --- a/docs.json +++ b/docs.json @@ -54,6 +54,23 @@ "domain": "docs.fly.io" } }, + "footer": { + "links": [ + { + "header": "Legal", + "items": [ + { + "label": "Privacy Policy", + "href": "https://fly.io/legal/privacy-policy/" + }, + { + "label": "Cookie preferences", + "href": "#cookie-preferences" + } + ] + } + ] + }, "navigation": { "tabs": [ { diff --git a/fly-analytics.js b/fly-analytics.js new file mode 100644 index 0000000000..fe8b29f020 --- /dev/null +++ b/fly-analytics.js @@ -0,0 +1,249 @@ +// PostHog and Google (GA4/GTM) for docs.fly.io, behind the same consent gate as fly.io. +// A port of landing's posthog-consent.js; keep the two in step. +// +// One self-contained file on purpose: Mintlify runs every .js in the content directory +// in no guaranteed order, so this loads its own PostHog bundle (the vendored +// no-external-code build that landing serves from fly.io) instead of relying on another file. +// +// docs.fly.io has no server, so it asks ui-ex's /api/consent-scope on fly.io (cross-origin, +// session cookie included; ui-ex allows exactly https://docs.fly.io) for: +// * eu_scope — did the visitor arrive via an EEA/UK edge (consent needed)? +// * authenticated — is the visitor logged in? +// All tracking is skipped for logged-in visitors. If that check fails (preview hosts, +// local `mint dev`, network errors), nothing loads. +// +// Plausible is separate (docs.json `integrations.plausible`) and sets nothing on the device. +(function () { + if (window.__flyAnalytics) return; + window.__flyAnalytics = true; + + var POSTHOG_KEY = "phc_nuaUijb3iLWut9Kbr56bSHSP4MYdeb8CFd9BgPm3LAVY"; + var POSTHOG_HOST = "https://posthog.fly.io"; + var POSTHOG_SRC = "https://fly.io/static/javascripts/posthog.js"; + var CONSENT_SCOPE_URL = "https://fly.io/api/consent-scope"; + var GA_ID = "G-EX6DMZ1DZV"; + var GTM_ID = "GTM-M35Q2HRQ"; + var ANALYTICS_BASE = "https://analytics.fly.io"; + var CONSENT_KEY = "ph_consent"; // localStorage: "granted" | "denied" | null + var PREFS_HASH = "#cookie-preferences"; + + var started = false; + + function storedConsent() { + try { + return window.localStorage.getItem(CONSENT_KEY); + } catch (e) { + return null; + } + } + + function persistConsent(value) { + try { + window.localStorage.setItem(CONSENT_KEY, value); + } catch (e) { + /* storage disabled: consent is session-only */ + } + } + + fetch(CONSENT_SCOPE_URL, { credentials: "include", headers: { Accept: "application/json" } }) + .then(function (r) { + return r.ok ? r.json() : null; + }) + .catch(function () { + return null; + }) + .then(function (data) { + if (!data) return; + // Identity kill-switch: never run any analytics for a logged-in visitor. + if (data.authenticated) return; + start(Boolean(data.eu_scope)); + }); + + function start(euScope) { + started = true; + var consent = storedConsent(); + loadPosthog(euScope, consent); + initGoogle(euScope, consent); + if (euScope && consent === null) openBanner(); + } + + // The "Cookie preferences" footer link (docs.json) reopens the banner. Delegated because + // Mintlify re-renders the footer on client-side navigation. + document.addEventListener("click", function (e) { + var link = e.target.closest && e.target.closest('a[href$="' + PREFS_HASH + '"]'); + if (!link) return; + e.preventDefault(); + if (started) openBanner(); + }); + + // Hosts we treat as "us" for referrer attribution: fly.io and any *.fly.io subdomain. + function isInternalHost(host, currentHost) { + return host === currentHost || host === "fly.io" || host.endsWith(".fly.io"); + } + + // Rewrite internal referrers to PostHog's "$direct" so moving between fly.io and the + // docs doesn't overwrite the real traffic source. Mirrors landing and ui-ex. + function scrubInternalReferrer(props, currentHost) { + if (!props) return; + var pairs = [ + ["$referring_domain", "$referrer"], + ["$initial_referring_domain", "$initial_referrer"], + ]; + for (var i = 0; i < pairs.length; i++) { + var domain = props[pairs[i][0]]; + if (typeof domain === "string" && domain !== "$direct" && isInternalHost(domain, currentHost)) { + props[pairs[i][0]] = "$direct"; + props[pairs[i][1]] = "$direct"; + } + } + } + + function beforeSend(event) { + if (!event || !event.properties) return event; + var currentHost = window.location.hostname; + scrubInternalReferrer(event.properties, currentHost); + scrubInternalReferrer(event.properties.$set_once, currentHost); + return event; + } + + function loadPosthog(euScope, consent) { + loadScript(POSTHOG_SRC, function () { + if (typeof window.posthog === "undefined") return; + var startOptedOut = euScope && consent !== "granted"; + window.posthog.init(POSTHOG_KEY, { + api_host: POSTHOG_HOST, + ui_host: "https://eu.posthog.com", + disable_external_dependency_loading: true, + before_send: beforeSend, + autocapture: false, + capture_dead_clicks: false, + rageclick: false, + enable_heatmaps: false, + disable_session_recording: true, + disable_surveys: true, + capture_exceptions: false, + person_profiles: "identified_only", + // Mintlify navigates client-side, so count route changes as pageviews. + capture_pageview: "history_change", + capture_pageleave: true, + persistence: startOptedOut ? "memory" : "localStorage+cookie", + opt_out_capturing_by_default: startOptedOut, + advanced_disable_decide: true, + }); + }); + } + + // GA4 (gtag) + GTM through the first-party analytics.fly.io proxy, as on fly.io. + function initGoogle(euScope, consent) { + window.dataLayer = window.dataLayer || []; + window.gtag = + window.gtag || + function () { + window.dataLayer.push(arguments); + }; + + // Consent Mode v2 default: granted outside EEA/UK or once accepted, denied otherwise. + window.gtag("consent", "default", consentPayload(!euScope || consent === "granted" ? "granted" : "denied")); + window.gtag("js", new Date()); + window.gtag("config", GA_ID, { transport_url: ANALYTICS_BASE, first_party_collection: true }); + + loadScript(ANALYTICS_BASE + "/gtag/js?id=" + GA_ID); + loadScript(ANALYTICS_BASE + "/gtm.js?id=" + GTM_ID); + } + + function updateGoogleConsent(state) { + if (typeof window.gtag !== "function") return; + window.gtag("consent", "update", consentPayload(state)); + } + + function consentPayload(state) { + return { + ad_storage: state, + analytics_storage: state, + ad_user_data: state, + ad_personalization: state, + }; + } + + function loadScript(src, onload) { + var s = document.createElement("script"); + s.async = true; + s.src = src; + if (onload) s.onload = onload; + document.head.appendChild(s); + } + + function grant() { + persistConsent("granted"); + if (typeof window.posthog !== "undefined" && window.posthog.opt_in_capturing) { + window.posthog.opt_in_capturing({ captureEventName: false }); + window.posthog.capture("$pageview"); + } + updateGoogleConsent("granted"); + closeBanner(); + } + + function deny() { + persistConsent("denied"); + if (typeof window.posthog !== "undefined" && window.posthog.opt_out_capturing) { + window.posthog.opt_out_capturing(); + } + updateGoogleConsent("denied"); + closeBanner(); + } + + // Copy owned by Legal; keep in sync with landing's _posthog_banner.html.erb and ui-ex. + // Styles live in styles.css (#ph-consent-banner). + function buildBanner() { + var el = document.createElement("div"); + el.id = "ph-consent-banner"; + el.hidden = true; + el.setAttribute("role", "dialog"); + el.setAttribute("aria-live", "polite"); + el.setAttribute("aria-label", "Cookie notice"); + el.innerHTML = + "

Hi, it's us, Fly.io. We hate these things as much as anyone, but the " + + "marketing gods demand conversion metrics.

" + + "

We set analytics cookies to measure traffic and ad conversion on our marketing and docs " + + "pages. These cookies only cover logged-out visits, never what you do in the dashboard.

" + + '

Read all about it in our Privacy Policy.

' + + '"; + el.querySelector("#ph-consent-accept").addEventListener("click", grant); + el.querySelector("#ph-consent-reject").addEventListener("click", deny); + return el; + } + + function banner() { + var el = document.getElementById("ph-consent-banner"); + if (!el && document.body) { + el = buildBanner(); + document.body.appendChild(el); + } + return el; + } + + var bannerOpen = false; + + function openBanner() { + bannerOpen = true; + var el = banner(); + if (el) el.hidden = false; + } + + function closeBanner() { + bannerOpen = false; + var el = document.getElementById("ph-consent-banner"); + if (el) el.hidden = true; + } + + // Client-side navigation can re-render (see auth-nav.js); put an open banner back. + var observer = new MutationObserver(function () { + if (bannerOpen) openBanner(); + if (document.body) observer.observe(document.body, { childList: true }); + }); + observer.observe(document.documentElement, { childList: true }); + if (document.body) observer.observe(document.body, { childList: true }); +})(); diff --git a/styles.css b/styles.css index 4aae8c5d65..e200eb7b83 100644 --- a/styles.css +++ b/styles.css @@ -129,3 +129,82 @@ div.frame_purple > div > div > div.relative.rounded-xl.overflow-hidden.flex.just align-items: center; padding: 0.375rem 0.875rem; } + +/* Consent banner, injected by fly-analytics.js for EEA/UK visitors. Mirrors the fly.io banner. */ +#ph-consent-banner { + position: fixed; + left: 1rem; + right: 1rem; + bottom: 1rem; + z-index: 50; + padding: 0.75rem; + border: 1px solid #e5e7eb; + border-radius: 0.5rem; + background: #fff; + color: #374151; + font-size: 0.75rem; + line-height: 1.375; + box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); +} +#ph-consent-banner[hidden] { + display: none; +} +#ph-consent-banner p { + margin: 0 0 0.5rem; +} +#ph-consent-banner a { + text-decoration: underline; +} +#ph-consent-banner .ph-consent-actions { + display: flex; + justify-content: flex-end; + gap: 0.5rem; +} +#ph-consent-banner button { + padding: 0.375rem 0.75rem; + border-radius: 0.375rem; + font-weight: 500; + cursor: pointer; + transition: background-color 150ms ease; +} +#ph-consent-reject { + background: #e5e7eb; + color: #1f2937; +} +#ph-consent-reject:hover { + background: #d1d5db; +} +#ph-consent-accept { + background: #7c3aed; + color: #fff; +} +#ph-consent-accept:hover { + background: #6d28d9; +} +.dark #ph-consent-banner { + border-color: #374151; + background: #111827; + color: #e5e7eb; +} +.dark #ph-consent-banner a:hover { + color: #fff; +} +.dark #ph-consent-reject { + background: #374151; + color: #f3f4f6; +} +.dark #ph-consent-reject:hover { + background: #4b5563; +} +@media (min-width: 640px) { + #ph-consent-banner { + left: auto; + max-width: 24rem; + padding: 1rem; + font-size: 0.875rem; + line-height: 1.625; + } + #ph-consent-banner p { + margin-bottom: 0.75rem; + } +} From 9be54bd90618e097064c6060be73ded9918cef4c Mon Sep 17 00:00:00 2001 From: Alex Leventer Date: Fri, 9 Oct 2026 15:08:48 -0700 Subject: [PATCH 2/3] Fix consent edge cases from review, drop GTM - A stored Reject now holds outside the EEA/UK too (the footer link is shown to everyone). - Read consent when posthog.js loads, so an Accept clicked before then isn't lost. - Switch PostHog to persistent storage on Accept; Mintlify's client-side nav kept the whole session on a memory-only id. - Only send the catch-up $pageview if PostHog was opted out. - 'Cookie preferences' falls through to the Privacy Policy when the banner can't open. - Drop GTM: ui-ex removed it for double-reporting conversions to GA4. Co-Authored-By: Claude Opus 5.5 --- docs.json | 2 +- fly-analytics.js | 37 +++++++++++++++++++++++++------------ 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/docs.json b/docs.json index 8b996cedfb..df3ec70cf6 100644 --- a/docs.json +++ b/docs.json @@ -65,7 +65,7 @@ }, { "label": "Cookie preferences", - "href": "#cookie-preferences" + "href": "https://fly.io/legal/privacy-policy/#cookie-preferences" } ] } diff --git a/fly-analytics.js b/fly-analytics.js index fe8b29f020..4ad73766a6 100644 --- a/fly-analytics.js +++ b/fly-analytics.js @@ -1,4 +1,4 @@ -// PostHog and Google (GA4/GTM) for docs.fly.io, behind the same consent gate as fly.io. +// PostHog and Google (GA4) for docs.fly.io, behind the same consent gate as fly.io. // A port of landing's posthog-consent.js; keep the two in step. // // One self-contained file on purpose: Mintlify runs every .js in the content directory @@ -22,7 +22,6 @@ var POSTHOG_SRC = "https://fly.io/static/javascripts/posthog.js"; var CONSENT_SCOPE_URL = "https://fly.io/api/consent-scope"; var GA_ID = "G-EX6DMZ1DZV"; - var GTM_ID = "GTM-M35Q2HRQ"; var ANALYTICS_BASE = "https://analytics.fly.io"; var CONSENT_KEY = "ph_consent"; // localStorage: "granted" | "denied" | null var PREFS_HASH = "#cookie-preferences"; @@ -59,10 +58,16 @@ start(Boolean(data.eu_scope)); }); + // Tracking needs consent in the EEA/UK. Elsewhere it is on unless the visitor rejected it + // from the "Cookie preferences" link, which docs.json shows to everyone. + function trackingAllowed(euScope, consent) { + return consent === "granted" || (!euScope && consent !== "denied"); + } + function start(euScope) { started = true; var consent = storedConsent(); - loadPosthog(euScope, consent); + loadPosthog(euScope); initGoogle(euScope, consent); if (euScope && consent === null) openBanner(); } @@ -71,9 +76,11 @@ // Mintlify re-renders the footer on client-side navigation. document.addEventListener("click", function (e) { var link = e.target.closest && e.target.closest('a[href$="' + PREFS_HASH + '"]'); - if (!link) return; + // Not started (logged in, preview, failed check): nothing to manage, so let the link + // fall through to the Privacy Policy. + if (!link || !started) return; e.preventDefault(); - if (started) openBanner(); + openBanner(); }); // Hosts we treat as "us" for referrer attribution: fly.io and any *.fly.io subdomain. @@ -106,10 +113,11 @@ return event; } - function loadPosthog(euScope, consent) { + function loadPosthog(euScope) { loadScript(POSTHOG_SRC, function () { if (typeof window.posthog === "undefined") return; - var startOptedOut = euScope && consent !== "granted"; + // Read consent now, not in start(): the banner is up before this async load finishes. + var startOptedOut = !trackingAllowed(euScope, storedConsent()); window.posthog.init(POSTHOG_KEY, { api_host: POSTHOG_HOST, ui_host: "https://eu.posthog.com", @@ -133,7 +141,8 @@ }); } - // GA4 (gtag) + GTM through the first-party analytics.fly.io proxy, as on fly.io. + // GA4 (gtag) through the first-party analytics.fly.io proxy, as on fly.io. No GTM: ui-ex + // dropped it because it double-reported conversions to GA4. function initGoogle(euScope, consent) { window.dataLayer = window.dataLayer || []; window.gtag = @@ -142,13 +151,12 @@ window.dataLayer.push(arguments); }; - // Consent Mode v2 default: granted outside EEA/UK or once accepted, denied otherwise. - window.gtag("consent", "default", consentPayload(!euScope || consent === "granted" ? "granted" : "denied")); + // Consent Mode v2 default. + window.gtag("consent", "default", consentPayload(trackingAllowed(euScope, consent) ? "granted" : "denied")); window.gtag("js", new Date()); window.gtag("config", GA_ID, { transport_url: ANALYTICS_BASE, first_party_collection: true }); loadScript(ANALYTICS_BASE + "/gtag/js?id=" + GA_ID); - loadScript(ANALYTICS_BASE + "/gtm.js?id=" + GTM_ID); } function updateGoogleConsent(state) { @@ -176,8 +184,13 @@ function grant() { persistConsent("granted"); if (typeof window.posthog !== "undefined" && window.posthog.opt_in_capturing) { + var wasOptedOut = window.posthog.has_opted_out_capturing(); + // Started opted out in memory. Mintlify navigates client-side, so without this the + // rest of the session would go to an id that's gone on the next full load. + window.posthog.set_config({ persistence: "localStorage+cookie" }); window.posthog.opt_in_capturing({ captureEventName: false }); - window.posthog.capture("$pageview"); + // Already capturing (reopened from the footer): this page was counted. + if (wasOptedOut) window.posthog.capture("$pageview"); } updateGoogleConsent("granted"); closeBanner(); From 5e9a98fec9753d63bc37b989385c9dcc84824d42 Mon Sep 17 00:00:00 2001 From: Alex Leventer Date: Fri, 9 Oct 2026 15:41:01 -0700 Subject: [PATCH 3/3] Drop the docs consent banner; track only logged-out visitors outside the EEA/UK The banner on docs kept its own consent record, separate from fly.io's, while the cookies it governs are shared on .fly.io. Rather than sync the two, docs shows no banner: EEA/UK visitors get Plausible only, and the banner stays on fly.io. Co-Authored-By: Claude Opus 5.5 --- docs.json | 4 -- fly-analytics.js | 172 ++++------------------------------------------- styles.css | 79 ---------------------- 3 files changed, 13 insertions(+), 242 deletions(-) diff --git a/docs.json b/docs.json index df3ec70cf6..e0f2d201ce 100644 --- a/docs.json +++ b/docs.json @@ -62,10 +62,6 @@ { "label": "Privacy Policy", "href": "https://fly.io/legal/privacy-policy/" - }, - { - "label": "Cookie preferences", - "href": "https://fly.io/legal/privacy-policy/#cookie-preferences" } ] } diff --git a/fly-analytics.js b/fly-analytics.js index 4ad73766a6..c1c18a6617 100644 --- a/fly-analytics.js +++ b/fly-analytics.js @@ -1,5 +1,7 @@ -// PostHog and Google (GA4) for docs.fly.io, behind the same consent gate as fly.io. -// A port of landing's posthog-consent.js; keep the two in step. +// PostHog and Google (GA4) for docs.fly.io, for logged-out visitors outside the EEA/UK. +// +// There is no consent banner here. EEA/UK visitors get nothing beyond Plausible (docs.json +// `integrations.plausible`, which sets nothing on the device). The banner lives on fly.io only. // // One self-contained file on purpose: Mintlify runs every .js in the content directory // in no guaranteed order, so this loads its own PostHog bundle (the vendored @@ -7,12 +9,10 @@ // // docs.fly.io has no server, so it asks ui-ex's /api/consent-scope on fly.io (cross-origin, // session cookie included; ui-ex allows exactly https://docs.fly.io) for: -// * eu_scope — did the visitor arrive via an EEA/UK edge (consent needed)? +// * eu_scope — did the visitor arrive via an EEA/UK edge? // * authenticated — is the visitor logged in? -// All tracking is skipped for logged-in visitors. If that check fails (preview hosts, -// local `mint dev`, network errors), nothing loads. -// -// Plausible is separate (docs.json `integrations.plausible`) and sets nothing on the device. +// Anything other than a clear "not EEA/UK, logged out" (including preview hosts, local +// `mint dev` and network errors) loads nothing. (function () { if (window.__flyAnalytics) return; window.__flyAnalytics = true; @@ -23,26 +23,6 @@ var CONSENT_SCOPE_URL = "https://fly.io/api/consent-scope"; var GA_ID = "G-EX6DMZ1DZV"; var ANALYTICS_BASE = "https://analytics.fly.io"; - var CONSENT_KEY = "ph_consent"; // localStorage: "granted" | "denied" | null - var PREFS_HASH = "#cookie-preferences"; - - var started = false; - - function storedConsent() { - try { - return window.localStorage.getItem(CONSENT_KEY); - } catch (e) { - return null; - } - } - - function persistConsent(value) { - try { - window.localStorage.setItem(CONSENT_KEY, value); - } catch (e) { - /* storage disabled: consent is session-only */ - } - } fetch(CONSENT_SCOPE_URL, { credentials: "include", headers: { Accept: "application/json" } }) .then(function (r) { @@ -52,37 +32,11 @@ return null; }) .then(function (data) { - if (!data) return; - // Identity kill-switch: never run any analytics for a logged-in visitor. - if (data.authenticated) return; - start(Boolean(data.eu_scope)); + if (!data || data.authenticated || data.eu_scope !== false) return; + loadPosthog(); + initGoogle(); }); - // Tracking needs consent in the EEA/UK. Elsewhere it is on unless the visitor rejected it - // from the "Cookie preferences" link, which docs.json shows to everyone. - function trackingAllowed(euScope, consent) { - return consent === "granted" || (!euScope && consent !== "denied"); - } - - function start(euScope) { - started = true; - var consent = storedConsent(); - loadPosthog(euScope); - initGoogle(euScope, consent); - if (euScope && consent === null) openBanner(); - } - - // The "Cookie preferences" footer link (docs.json) reopens the banner. Delegated because - // Mintlify re-renders the footer on client-side navigation. - document.addEventListener("click", function (e) { - var link = e.target.closest && e.target.closest('a[href$="' + PREFS_HASH + '"]'); - // Not started (logged in, preview, failed check): nothing to manage, so let the link - // fall through to the Privacy Policy. - if (!link || !started) return; - e.preventDefault(); - openBanner(); - }); - // Hosts we treat as "us" for referrer attribution: fly.io and any *.fly.io subdomain. function isInternalHost(host, currentHost) { return host === currentHost || host === "fly.io" || host.endsWith(".fly.io"); @@ -113,11 +67,9 @@ return event; } - function loadPosthog(euScope) { + function loadPosthog() { loadScript(POSTHOG_SRC, function () { if (typeof window.posthog === "undefined") return; - // Read consent now, not in start(): the banner is up before this async load finishes. - var startOptedOut = !trackingAllowed(euScope, storedConsent()); window.posthog.init(POSTHOG_KEY, { api_host: POSTHOG_HOST, ui_host: "https://eu.posthog.com", @@ -134,8 +86,7 @@ // Mintlify navigates client-side, so count route changes as pageviews. capture_pageview: "history_change", capture_pageleave: true, - persistence: startOptedOut ? "memory" : "localStorage+cookie", - opt_out_capturing_by_default: startOptedOut, + persistence: "localStorage+cookie", advanced_disable_decide: true, }); }); @@ -143,36 +94,18 @@ // GA4 (gtag) through the first-party analytics.fly.io proxy, as on fly.io. No GTM: ui-ex // dropped it because it double-reported conversions to GA4. - function initGoogle(euScope, consent) { + function initGoogle() { window.dataLayer = window.dataLayer || []; window.gtag = window.gtag || function () { window.dataLayer.push(arguments); }; - - // Consent Mode v2 default. - window.gtag("consent", "default", consentPayload(trackingAllowed(euScope, consent) ? "granted" : "denied")); window.gtag("js", new Date()); window.gtag("config", GA_ID, { transport_url: ANALYTICS_BASE, first_party_collection: true }); - loadScript(ANALYTICS_BASE + "/gtag/js?id=" + GA_ID); } - function updateGoogleConsent(state) { - if (typeof window.gtag !== "function") return; - window.gtag("consent", "update", consentPayload(state)); - } - - function consentPayload(state) { - return { - ad_storage: state, - analytics_storage: state, - ad_user_data: state, - ad_personalization: state, - }; - } - function loadScript(src, onload) { var s = document.createElement("script"); s.async = true; @@ -180,83 +113,4 @@ if (onload) s.onload = onload; document.head.appendChild(s); } - - function grant() { - persistConsent("granted"); - if (typeof window.posthog !== "undefined" && window.posthog.opt_in_capturing) { - var wasOptedOut = window.posthog.has_opted_out_capturing(); - // Started opted out in memory. Mintlify navigates client-side, so without this the - // rest of the session would go to an id that's gone on the next full load. - window.posthog.set_config({ persistence: "localStorage+cookie" }); - window.posthog.opt_in_capturing({ captureEventName: false }); - // Already capturing (reopened from the footer): this page was counted. - if (wasOptedOut) window.posthog.capture("$pageview"); - } - updateGoogleConsent("granted"); - closeBanner(); - } - - function deny() { - persistConsent("denied"); - if (typeof window.posthog !== "undefined" && window.posthog.opt_out_capturing) { - window.posthog.opt_out_capturing(); - } - updateGoogleConsent("denied"); - closeBanner(); - } - - // Copy owned by Legal; keep in sync with landing's _posthog_banner.html.erb and ui-ex. - // Styles live in styles.css (#ph-consent-banner). - function buildBanner() { - var el = document.createElement("div"); - el.id = "ph-consent-banner"; - el.hidden = true; - el.setAttribute("role", "dialog"); - el.setAttribute("aria-live", "polite"); - el.setAttribute("aria-label", "Cookie notice"); - el.innerHTML = - "

Hi, it's us, Fly.io. We hate these things as much as anyone, but the " + - "marketing gods demand conversion metrics.

" + - "

We set analytics cookies to measure traffic and ad conversion on our marketing and docs " + - "pages. These cookies only cover logged-out visits, never what you do in the dashboard.

" + - '

Read all about it in our Privacy Policy.

' + - '"; - el.querySelector("#ph-consent-accept").addEventListener("click", grant); - el.querySelector("#ph-consent-reject").addEventListener("click", deny); - return el; - } - - function banner() { - var el = document.getElementById("ph-consent-banner"); - if (!el && document.body) { - el = buildBanner(); - document.body.appendChild(el); - } - return el; - } - - var bannerOpen = false; - - function openBanner() { - bannerOpen = true; - var el = banner(); - if (el) el.hidden = false; - } - - function closeBanner() { - bannerOpen = false; - var el = document.getElementById("ph-consent-banner"); - if (el) el.hidden = true; - } - - // Client-side navigation can re-render (see auth-nav.js); put an open banner back. - var observer = new MutationObserver(function () { - if (bannerOpen) openBanner(); - if (document.body) observer.observe(document.body, { childList: true }); - }); - observer.observe(document.documentElement, { childList: true }); - if (document.body) observer.observe(document.body, { childList: true }); })(); diff --git a/styles.css b/styles.css index e200eb7b83..4aae8c5d65 100644 --- a/styles.css +++ b/styles.css @@ -129,82 +129,3 @@ div.frame_purple > div > div > div.relative.rounded-xl.overflow-hidden.flex.just align-items: center; padding: 0.375rem 0.875rem; } - -/* Consent banner, injected by fly-analytics.js for EEA/UK visitors. Mirrors the fly.io banner. */ -#ph-consent-banner { - position: fixed; - left: 1rem; - right: 1rem; - bottom: 1rem; - z-index: 50; - padding: 0.75rem; - border: 1px solid #e5e7eb; - border-radius: 0.5rem; - background: #fff; - color: #374151; - font-size: 0.75rem; - line-height: 1.375; - box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); -} -#ph-consent-banner[hidden] { - display: none; -} -#ph-consent-banner p { - margin: 0 0 0.5rem; -} -#ph-consent-banner a { - text-decoration: underline; -} -#ph-consent-banner .ph-consent-actions { - display: flex; - justify-content: flex-end; - gap: 0.5rem; -} -#ph-consent-banner button { - padding: 0.375rem 0.75rem; - border-radius: 0.375rem; - font-weight: 500; - cursor: pointer; - transition: background-color 150ms ease; -} -#ph-consent-reject { - background: #e5e7eb; - color: #1f2937; -} -#ph-consent-reject:hover { - background: #d1d5db; -} -#ph-consent-accept { - background: #7c3aed; - color: #fff; -} -#ph-consent-accept:hover { - background: #6d28d9; -} -.dark #ph-consent-banner { - border-color: #374151; - background: #111827; - color: #e5e7eb; -} -.dark #ph-consent-banner a:hover { - color: #fff; -} -.dark #ph-consent-reject { - background: #374151; - color: #f3f4f6; -} -.dark #ph-consent-reject:hover { - background: #4b5563; -} -@media (min-width: 640px) { - #ph-consent-banner { - left: auto; - max-width: 24rem; - padding: 1rem; - font-size: 0.875rem; - line-height: 1.625; - } - #ph-consent-banner p { - margin-bottom: 0.75rem; - } -}