1/* 2 * The Google tag: GA4 (G-5PWL8CYZ65) and Google Ads (AW-18449425255), and the 3 * ONLY place either is loaded from. Every page loads this file directly, in 4 * <head>, with no consent gate in front of it. What the visitor's choice 5 * controls is not whether the tag runs but what it is allowed to store, which 6 * is Google Consent Mode v2 in its "advanced" shape. 7 * 8 * WHY IT CHANGED FROM HARD BLOCKING. This file used to be registered in 9 * window.CONSENT_CONFIG.scripts.analytics, so consent.js loaded it only after 10 * the visitor granted analytics. That is Consent Mode's "basic" shape, and it 11 * cost us both halves of paid search measurement: a visitor who never touched 12 * the banner produced no hit, no cookieless ping and no gclid capture, so 13 * Google had nothing to model unobserved conversions from, and the Ads tag was 14 * not present at all. Google's own comparison of the two shapes is explicit: 15 * under basic, "Google tags blocked until consent is granted" and no cookieless 16 * pings are sent; under advanced, "Google tags are loaded before the consent 17 * dialog appears" and "Tags send cookieless pings when cookie consent 18 * declined", which is what enables behavioural and conversion modelling. 19 * https://support.google.com/analytics/answer/9976101 20 * 21 * WHAT THIS DOES NOT CHANGE. Nothing is stored before the visitor agrees. The 22 * default state below denies ad_storage, ad_user_data, ad_personalization and 23 * analytics_storage, and a denied tag writes no cookie: no _ga, no _ga_*, no 24 * _gcl_au. The banner is untouched, still opt-in, still requires an 25 * affirmative action, and the ABTestly self-experiment snippet and 26 * gtm-bridge.js are still hard blocked behind the analytics category exactly 27 * as before. The visitor-facing trade is that a denied visitor now produces an 28 * anonymous, cookieless ping where previously they produced nothing at all. 29 * 30 * WHY THE HOST GATE SURVIVED THE REWRITE, UNCHANGED. The same build is served 31 * on abtestly.com, on the Pages project's own domain abtestly.pages.dev, and 32 * on every preview deployment at <hash>.abtestly.pages.dev. Without this gate a 33 * visitor on any of those sent hits to the production property, and GA4 flagged 34 * the extra domains. So the tag runs on an exact hostname match and nowhere 35 * else: previews, pages.dev, localhost and file pages get no gtag request, no 36 * gtag function and no config call. The check fails closed, so a host missing 37 * from the list loses measurement rather than polluting it. www.abtestly.com is 38 * not listed because it answers with a 301 to abtestly.com and never serves a 39 * page. This gate is now MORE load bearing than it was, not less: it used to 40 * sit behind a consent gate that already stopped most of this, and it is now 41 * the only thing standing between a preview deployment and the live property. 42 * test/ga4-production-host.test.mjs pins it, page by page. 43 * 44 * WHY THE CONSENT WIRING IS HERE AND NOT IN 34 INLINE HEAD SCRIPTS. Google's 45 * snippet is normally pasted inline into <head>. Doing that here would mean 34 46 * copies of a security-critical block, one per page, drifting the first time 47 * somebody edits one of them. The ordering rule Google actually states is 48 * about the dataLayer queue, not about inline-ness: the consent default has to 49 * be set "before any commands that send measurement data (such as config or 50 * event)", and url_passthrough and ads_data_redaction before any config 51 * commands (developers.google.com/tag-platform/security/guides/consent). 52 * Everything in this file runs in one synchronous pass, in that order, and 53 * gtag.js is appended async and replays the queue it finds. One file, one 54 * order, nothing to keep in sync. 55 * 56 * WHY THE STORED CHOICE IS READ HERE RATHER THAN WAITED FOR. consent.js 57 * dispatches `consentchange` on boot, and the listener below catches it. But 58 * this file runs first, and a returning visitor who already granted should not 59 * spend the gap between the two denied. So the cookie is read directly and the 60 * matching update fired before any config call. The read mirrors consent.js's 61 * own validation exactly, version and 180-day expiry included: trusting a 62 * stale record here would grant consent past the point the banner considers it 63 * expired, which is the one way this shortcut could become a compliance defect. 64 * 65 * WHY THIS IS A FILE AND NOT AN INLINE STRING IN window.CONSENT_CONFIG. It 66 * used to live as an `inline:` string there. That put it beyond the reach of 67 * the CSP hash generator, which scans the built HTML for <script> elements and 68 * cannot see JavaScript held in an object literal, so it violated the policy on 69 * every page. The console helpfully offers a sha256 to paste in; do not. A hand 70 * maintained hash is wrong the moment someone edits this by one character, and 71 * the symptom is a flood of reports for a script that is perfectly fine. 72 * 73 * Served from our own origin, so `script-src 'self'` already covers it and no 74 * hash exists to drift. The gtag library it injects, and the Google Ads hosts 75 * the AW- tag talks to, are covered by hand-added entries in 76 * build-plugins/csp-report-only.mjs. 77 */ 78(function () { 79 var PRODUCTION_HOSTS = ['abtestly.com']; 80 var MEASUREMENT_ID = 'G-5PWL8CYZ65'; 81 var ADS_ID = 'AW-18449425255'; 82 83 /* The consent record consent.js writes. Same name, deliberately: the whole 84 * point of the .abtestly.com cookie domain is one choice across every
85 * subdomain, which only works while every reader uses the identical name. */ 86 var COOKIE_NAME = '__cookie_consent_v1'; 87 var FALLBACK_VERSION = '1.0.0'; 88 var FALLBACK_EXPIRY_DAYS = 180; 89 90 if (PRODUCTION_HOSTS.indexOf(window.location.hostname) === -1) return; 91 92 window.dataLayer = window.dataLayer || []; 93 // gtag.js reads the arguments object itself, not an array copy of it. 94 window.gtag = function gtag() { 95 window.dataLayer.push(arguments); 96 }; 97 98 // 1. Deny everything that stores, before anything that sends. functionality 99 // and security are granted because they cover no cookie of ours and 100 // denying them buys nothing; the four that matter are all denied. 101 // wait_for_update holds a hit for 500ms so a consent solution that 102 // resolves a moment late is not raced by the first config call. 103 window.gtag('consent', 'default', { 104 ad_storage: 'denied', 105 ad_user_data: 'denied', 106 ad_personalization: 'denied', 107 analytics_storage: 'denied', 108 functionality_storage: 'granted', 109 security_storage: 'granted', 110 wait_for_update: 500 111 }); 112 113 // 2. url_passthrough carries the gclid across a same-domain navigation in the 114 // URL rather than in a cookie, which is the only way an ad click survives 115 // a denied ad_storage. ads_data_redaction is the other half: "When 116 // ads_data_redaction is true and ad_storage is denied, ad click 117 // identifiers sent in network requests by Google Ads and Floodlight tags 118 // will be redacted." It is inert once ad_storage is granted, so it is set 119 // once here rather than toggled. 120 window.gtag('set', 'url_passthrough', true); 121 window.gtag('set', 'ads_data_redaction', true); 122 123 // 3. A returning visitor's stored choice, before any config call. 124 var stored = readStoredCategories(); 125 if (stored) window.gtag('consent', 'update', signalsFor(stored)); 126 127 // 4. Every later change, including the first choice a new visitor makes. 128 // consent.js dispatches this on window for boot, Accept all, Reject all 129 // and Save preferences alike, so one listener covers all four. 130 window.addEventListener('consentchange', function (e) { 131 var categories = e && e.detail && e.detail.categories; 132 if (!categories) return; 133 window.gtag('consent', 'update', signalsFor(categories)); 134 }); 135 136 var loader = document.createElement('script'); 137 loader.async = true; 138 loader.src = 'https://www.googletagmanager.com/gtag/js?id=' + MEASUREMENT_ID; 139 (document.head || document.documentElement).appendChild(loader); 140 141 window.gtag('js', new Date()); 142 window.gtag('config', MEASUREMENT_ID); 143 window.gtag('config', ADS_ID); 144 145 /** 146 * Map our two optional banner categories onto Consent Mode's four signals. 147 * 148 * analytics -> analytics_storage, and marketing -> the three ad signals. 149 * The split is the point: a visitor who accepts analytics but declines 150 * marketing gets GA4 cookies and no advertising identifier, which is what 151 * the two separate toggles in the banner promise them. Reusing the analytics 152 * grant for the ad signals would quietly make that promise false. 153 */ 154 function signalsFor(categories) { 155 var ads = categories.marketing === true ? 'granted' : 'denied'; 156 return { 157 ad_storage: ads, 158 ad_user_data: ads, 159 ad_personalization: ads, 160 analytics_storage: categories.analytics === true ? 'granted' : 'denied' 161 }; 162 } 163 164 /** 165 * The stored categories, or null when there is no usable record. Mirrors 166 * readStored() in consent.js: a record from another config version, one past 167 * its expiry, or one without a categories object is not a choice this page 168 * may act on. Anything unreadable is treated as no consent. 169 */ 170 function readStoredCategories() { 171 try { 172 var cfg = window.CONSENT_CONFIG || {}; 173 var version = cfg.version || FALLBACK_VERSION; 174 var expiryDays = typeof cfg.expiryDays === 'number' ? cfg.expiryDays : FALLBACK_EXPIRY_DAYS; 175 // Anchored to start-of-string or a semicolon so a cookie whose name ends 176 // in ours (foo__cookie_consent_v1) cannot be mistaken for ours. 177 var m = document.cookie.match(new RegExp('(?:^|;\\s*)' + COOKIE_NAME + '=([^;]+)')); 178 if (!m) return null; 179 var parsed = JSON.parse(decodeURIComponent(m[1])); 180 if (!parsed || parsed.version !== version) return null; 181 var ts = Number(parsed.timestamp); 182 if (!isFinite(ts)) return null; 183 if ((Date.now() - ts) / 86400000 > expiryDays) return null; 184 if (!parsed.categories || typeof parsed.categories !== 'object') return null; 185 return parsed.categories; 186 } catch (e) { 187 return null; 188 } 189 } 190})();
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.