1/* Consent gate for the two optional trackers (legal audit 2026-10-06, commit 4). 2 * 3 * ONE file for both sites: datalookout.com serves this copy at /assets/consent.js and 4 * dashboard.datalookout.com serves a byte-identical copy at /static/js/consent.js 5 * (tests/test_consent_dashboard.py pins the parity). Configuration rides on the <script> 6 * tag's data attributes, so the file itself never changes per host: 7 * 8 * data-ga Google Analytics measurement id (loads the gtag library + config) 9 * data-attr URL of the first-touch attribution script to load (marketing only; 10 * the dashboard sets its attribution cookie server-side and gates it on 11 * the same dl_consent cookie this file writes) 12 * data-privacy href for the "Privacy policy" link (default /privacy) 13 * 14 * Region comes from the server: the page's <html data-dl-region="eu|other"> attribute, 15 * set by site/functions/_middleware.js (Pages) or dashboard/main.py (FastAPI) from 16 * Cloudflare's country code. A MISSING attribute counts as "eu", so an unknown country 17 * is asked, never tracked. 18 * 19 * Rules (the owner's, 2026-10-06): 20 * - eu, no choice yet: show the bar, load NOTHING until Accept. 21 * - other, no choice yet: load, no prompt (today's behaviour). 22 * - dl_consent=no, any region: load nothing, ever; expire the trackers' cookies. 23 * - dl_consent=yes, any region: load. 24 * - Ignoring, scrolling or closing never counts as consent (there is no close button). 25 * - The choice lives 180 days in one cookie on .datalookout.com, so it carries across 26 * both sites. A footer link with data-dl-consent-open reopens the bar. 27 * - Two buttons, same size, same style, neither pre-selected. Keyboard reachable with a 28 * visible focus ring; the bar is an aria-live region so screen readers announce it. 29 */ 30(function () { 31 "use strict"; 32 var script = document.currentScript; 33 var ds = (script && script.dataset) || {}; 34 var GA_ID = ds.ga || ""; 35 var ATTR_SRC = ds.attr || ""; 36 var PRIVACY_HREF = ds.privacy || "/privacy"; 37 38 var COOKIE = "dl_consent"; 39 var MAX_AGE = 180 * 24 * 60 * 60; // six months 40 var DOMAIN = ".datalookout.com"; 41 var TEXT = "We use Google Analytics and a cookie that remembers how you found us, to see how people find and use DataLookout."; 42 43 function readChoice() { 44 var m = document.cookie.match(/(?:^|;\s*)dl_consent=(yes|no)(?:;|$)/); 45 return m ? m[1] : null; 46 } 47 function writeChoice(v) { 48 document.cookie = COOKIE + "=" + v + "; Domain=" + DOMAIN + "; Path=/; Max-Age=" + MAX_AGE + "; Secure; SameSite=Lax"; 49 } 50 function expire(name) { 51 // Both scopes: GA writes on the registrable domain, dl_src too; be thorough. 52 document.cookie = name + "=; Domain=" + DOMAIN + "; Path=/; Max-Age=0; Secure; SameSite=Lax"; 53 document.cookie = name + "=; Path=/; Max-Age=0; Secure; SameSite=Lax"; 54 } 55 function clearTrackingCookies() { 56 var names = document.cookie.split(";").map(function (c) { return c.split("=")[0].trim(); }); 57 names.forEach(function (n) { if (n === "_ga" || n.indexOf("_ga_") === 0 || n === "dl_src") expire(n); }); 58 } 59 60 var region = document.documentElement.getAttribute("data-dl-region") === "other" ? "other" : "eu"; 61 var loaded = false; 62 function loadTrackers() { 63 if (loaded) return; 64 loaded = true; 65 if (GA_ID) { 66 window.dataLayer = window.dataLayer || []; 67 window.gtag = window.gtag || function () { window.dataLayer.push(arguments); }; 68 window.gtag("js", new Date()); 69 window.gtag("config", GA_ID); 70 var g = document.createElement("script"); 71 g.async = true; 72 g.src = "https://www.googletagmanager.com/gtag/js?id=" + encodeURIComponent(GA_ID); 73 document.head.appendChild(g); 74 } 75 if (ATTR_SRC) { 76 var a = document.createElement("script"); 77 a.defer = true; 78 a.src = ATTR_SRC; 79 document.head.appendChild(a); 80 } 81 } 82 83 var CSS = 84 "#dl-consent{position:fixed;left:0;right:0;bottom:0;z-index:1000;box-sizing:border-box;" + 85 "background:var(--dl-panel,#fff);color:var(--dl-ink-1,#1e1d19);border-top:1px solid var(--dl-rule,#e4e1d7);" + 86 "box-shadow:0 -2px 12px rgba(0,0,0,.08);padding:14px 16px;font:16px/1.5 var(--dl-font-sans,system-ui,sans-serif);" + 87 "display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:12px 24px}" + 88 "#dl-consent p{margin:0;flex:1 1 320px;max-width:760px}" + 89 "#dl-consent a{color:inherit;text-decoration:underline}" + 90 "#dl-consent .dl-consent-actions{display:flex;gap:10px;flex:0 0 auto}" + 91 "#dl-consent button{font:inherit;font-weight:500;padding:10px 18px;min-width:112px;cursor:pointer;" + 92 "background:transparent;color:var(--dl-ink-1,#1e1d19);border:1px solid var(--dl-ink-1,#1e1d19);border-radius:var(--dl-radius-sm,4px)}" + 93 "#dl-consent button:hover{background:var(--dl-canvas,#f6f5f0)}" + 94 "#dl-consent button:focus-visible,#dl-consent a:focus-visible{outline:3px solid var(--dl-action,#0f6e4d);outline-offset:2px}" + 95 "@media (max-width:480px){#dl-consent .dl-consent-actions{flex:1 1 100%}#dl-consent button{flex:1 1 0;min-width:0}}"; 96 97 var bar = null; 98 function hide() { 99 if (!bar) return; 100 bar.parentNode.removeChild(bar); 101 bar = null; 102 document.body.style.paddingBottom = ""; 103 } 104 function choose(v) { 105 writeChoice(v); 106 hide(); 107 if (v === "yes") loadTrackers(); 108 else clearTrackingCookies(); 109 } 110 function show() { 111 if (bar) return; 112 if (!document.getElementById("dl-consent-style")) { 113 var st = document.createElement("style"); 114 st.id = "dl-consent-style"; 115 st.textContent = CSS; 116 document.head.appendChild(st); 117 } 118 bar = document.createElement("div"); 119 bar.id = "dl-consent"; 120 bar.setAttribute("role", "region"); 121 bar.setAttribute("aria-label", "Cookie notice"); 122 bar.setAttribute("aria-live", "polite"); 123 var p = document.createElement("p"); 124 p.id = "dl-consent-text"; 125 p.appendChild(document.createTextNode(TEXT + " ")); 126 var link = document.createElement("a"); 127 link.href = PRIVACY_HREF; 128 link.textContent = "Privacy policy"; 129 p.appendChild(link); 130 var actions = document.createElement("div"); 131 actions.className = "dl-consent-actions";
132 [["yes", "Accept"], ["no", "Decline"]].forEach(function (pair) { 133 var b = document.createElement("button"); 134 b.type = "button"; 135 b.textContent = pair[1]; 136 b.setAttribute("data-dl-consent", pair[0]); 137 b.addEventListener("click", function () { choose(pair[0]); }); 138 actions.appendChild(b); 139 }); 140 bar.appendChild(p); 141 bar.appendChild(actions); 142 document.body.appendChild(bar); 143 // Keep the page's own bottom content reachable while the bar is up. 144 document.body.style.paddingBottom = bar.offsetHeight + "px"; 145 } 146 147 // Footer "Cookie settings": reopen the bar so the choice can be changed as easily as made. 148 document.addEventListener("click", function (e) { 149 var t = e.target && e.target.closest ? e.target.closest("[data-dl-consent-open]") : null; 150 if (!t) return; 151 e.preventDefault(); 152 show(); 153 var first = bar && bar.querySelector("button"); 154 if (first) first.focus(); 155 }); 156 157 var choice = readChoice(); 158 if (choice === "no") return; // declined: nothing loads, no prompt 159 if (choice === "yes" || region === "other") { loadTrackers(); return; } 160 show(); // eu, undecided: ask, load nothing 161})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.