PageSourceSearch

https://abtestly.com/ga4-init.js?v=1d6db3f3

js abtestly.com collected 2026-10-02 01:21:11 UTC 10,075 bytes, 190 lines download raw bytes

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.