1(function () { 2 const flourishConfig = JSON.parse(document.getElementById('flourish-config').textContent); 3 4 if (!flourishConfig.enableAnalytics) return; 5 6 const spConfig = flourishConfig.snowplow; 7 8 window.snowplowEventQueue = []; 9 window.gaEventQueue = []; 10 11 let is_initialising = false; 12 let analytics_has_initialized = false; 13 14 window._originalGtag = window.gtag; 15 16 window.gtag = function (...args) { 17 window.gaEventQueue.push({ 18 type: 'gtag', 19 args: args, 20 timestamp: new Date().toISOString() 21 }); 22 23 if (typeof window._originalGtag === 'function') { 24 window._originalGtag.apply(window, args); 25 } 26 }; 27 28 window.dataLayer = window.dataLayer || []; 29 const originalDataLayerPush = Array.prototype.push; 30 31 window.dataLayer.push = function (...args) { 32 args.forEach(arg => { 33 window.gaEventQueue.push({ 34 type: 'dataLayer', 35 data: arg, 36 timestamp: new Date().toISOString() 37 }); 38 }); 39 40 return originalDataLayerPush.apply(window.dataLayer, args); 41 }; 42 43 window.sp = { 44 trackInteraction: function (properties) { 45 window.snowplowEventQueue.push({ 46 type: 'interaction', 47 properties: properties, 48 timestamp: new Date().toISOString() 49 }); 50 }, 51 trackFormInteraction: function (properties) { 52 window.snowplowEventQueue.push({ 53 type: 'formInteraction', 54 properties: properties, 55 timestamp: new Date().toISOString() 56 }); 57 } 58 }; 59 60 const queue_function_impl = { 61 trackInteraction: window.sp.trackInteraction, 62 trackFormInteraction: window.sp.trackFormInteraction, 63 gtag: window.gtag, 64 dataLayerPush: window.dataLayer.push 65 }; 66 67 let live_function_impl = null; 68 69 /** Shared predicate for Snowplow cross-domain URL decoration (`?_sp=`). Also used when re-applying the linker after consent or bfcache restore. */ 70 function crossDomainLinkerForFlourish(linkElement) { 71 const flourishDomains = spConfig.crossDomainTargets || []; 72 return linkElement.hostname !== location.hostname && 73 flourishDomains.includes(linkElement.hostname); 74 } 75 76 function debounce(func, wait) { 77 let timeout; 78 return function () { 79 const context = this; 80 const args = arguments; 81 const later = function () { 82 timeout = null; 83 func.apply(context, args); 84 }; 85 clearTimeout(timeout); 86 timeout = setTimeout(later, wait); 87 }; 88 } 89 90 function processQueuedEvents() { 91 const trackGAEvent = function (properties) { 92 const eventData = { 93 category: 'ga4_event', 94 action: properties.action || properties.event || 'unknown' 95 }; 96 97 window.snowplow('trackSelfDescribingEvent', { 98 event: { 99 schema: 'iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0', 100 data: { 101 schema: 'iglu:com.google.analytics.measurement-protocol/event/jsonschema/1-0-0', 102 data: eventData 103 } 104 }, 105 context: [{ 106 schema: 'iglu:com.google.analytics.measurement-protocol/hit/jsonschema/1-0-0', 107 data: { type: 'event' } 108 }] 109 }); 110 }; 111 112 if (window.snowplowEventQueue && window.snowplowEventQueue.length > 0) { 113 window.snowplowEventQueue.forEach(event => { 114 if (event.type === 'interaction') { 115 window.sp.trackInteraction(event.properties); 116 } else if (event.type === 'formInteraction') { 117 window.sp.trackFormInteraction(event.properties); 118 } 119 }); 120 window.snowplowEventQueue = []; 121 } 122 123 if (window.gaEventQueue && window.gaEventQueue.length > 0) { 124 window.gaEventQueue.forEach(event => { 125 if (event.type === 'dataLayer') { 126 const data = event.data; 127 128 if (Array.isArray(data) && data[0] === 'event') { 129 const [_, eventName, eventParams] = data;
130 trackGAEvent({ ...eventParams, category: 'ga4_event', action: eventName }); 131 } else if (typeof data === 'object' && data !== null && 'event' in data) { 132 const { event, ...params } = data; 133 trackGAEvent({ ...params, category: 'ga4_event', action: event }); 134 } 135 } else if (event.type === 'gtag') { 136 const args = event.args; 137 const [command, action, parameters] = args; 138 139 if (command === 'event' && action && parameters) { 140 trackGAEvent({ ...parameters, category: 'ga4_event', action }); 141 } 142 } 143 }); 144 window.gaEventQueue = []; 145 } 146 } 147 148 const initialiseSnowplow = debounce(function () { 149 const hasConsent = checkOneTrustConsent(); 150 151 if (is_initialising) return; 152 if (analytics_has_initialized && window.snowplowInitialized) return; 153 if (!hasConsent) return; 154 155 is_initialising = true; 156 157 if (analytics_has_initialized && (!window.snowplowEventQueue || !Array.isArray(window.snowplowEventQueue))) { 158 window.snowplowEventQueue = []; 159 window.sp.trackInteraction = queue_function_impl.trackInteraction; 160 window.sp.trackFormInteraction = queue_function_impl.trackFormInteraction; 161 } 162 163 try { 164 ;(function (p, l, o, w, i, n, g) { 165 if (!p[i]) { 166 p.GlobalSnowplowNamespace = p.GlobalSnowplowNamespace || []; 167 p.GlobalSnowplowNamespace.push(i); 168 p[i] = function () { 169 (p[i].q = p[i].q || []).push(arguments) 170 }; 171 p[i].q = p[i].q || []; 172 n = l.createElement(o); 173 g = l.getElementsByTagName(o)[0]; 174 n.async = 1; 175 n.src = w; 176 g.parentNode.insertBefore(n, g) 177 } 178 }(window, document, 'script', '/js/9e10734c6f18ce57.js', 'snowplow')); 179 180 const trackerConfig = { 181 appId: spConfig.appId, 182 platform: 'web', 183 cookieDomain: spConfig.cookieDomain, 184 contexts: { 185 webPage: true, 186 session: true, 187 browser: true, 188 }, 189 stateStorageStrategy: 'cookieAndLocalStorage', 190 referrerPolicy: 'no-referrer-when-downgrade', 191 credentials: 'omit', 192 eventMethod: 'post', 193 // Snowplow `newTracker` option: decorate matching outbound links with `?_sp` (cross-domain docs). 194 // Same predicate in `applyCrossDomainLinker`; Snowplow avoids duplicate listeners for the same callback. 195 crossDomainLinker: crossDomainLinkerForFlourish 196 }; 197 198 window.snowplow('newTracker', 'marketing_site', spConfig.collectorUrl, trackerConfig); 199 200 window.snowplow('addPlugin', 201 'https://cdn.jsdelivr.net/npm/@snowplow/browser-plugin-client-hints@latest/dist/index.umd.min.js', 202 ['snowplowClientHints', 'ClientHintsPlugin'] 203 ); 204 205 window.snowplow('addPlugin', 206 'https://cdn.jsdelivr.net/npm/@snowplow/browser-plugin-ga-cookies@latest/dist/index.umd.min.js', 207 ['snowplowGaCookies', 'GaCookiesPlugin'], 208 [{ ga4: true, ua: false, ga4MeasurementId: 'G-KW52XHYN9H' }] 209 ); 210 211 window.sp = { 212 trackInteraction: function (properties) { 213 window.snowplow('trackSelfDescribingEvent', { 214 event: { 215 schema: 'iglu:studio.flourish/flourish_interacted/jsonschema/1-0-0', 216 data: properties 217 } 218 }); 219 }, 220 221 trackFormInteraction: function (properties) { 222 window.snowplow('trackSelfDescribingEvent', { 223 event: { 224 schema: 'iglu:studio.flourish/flourish_form_interacted/jsonschema/1-0-0', 225 data: properties 226 } 227 }); 228 }, 229 }; 230 231 window.snowplowInitialized = true; 232 analytics_has_initialized = true; 233 // Keep in sync with OneTrust so a later `OneTrustGroupsUpdated` is not mistaken for consent restore 234 // (would re-run processQueuedEvents / applyCrossDomainLinker). Deferred `load` path only calls 235 // `initialiseSnowplow()` â no wrapper sets this anymore. 236 currentConsentState = checkOneTrustConsent(); 237 238 window.snowplow('trackPageView'); 239 240 processQueuedEvents(); 241 242 const trackGAEvent = function (properties) { 243 const eventData = { 244 category: 'ga4_event', 245 action: properties.action || properties.event || 'unknown' 246 }; 247 248 window.snowplow('trackSelfDescribingEvent', { 249 event: { 250 schema: 'iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0', 251 data: { 252 schema: 'iglu:com.google.analytics.measurement-protocol/event/jsonschema/1-0-0', 253 data: eventData 254 } 255 }, 256 context: [{ 257 schema: 'iglu:com.google.analytics.measurement-protocol/hit/jsonschema/1-0-0', 258 data: { type: 'event' } 259 }] 260 }); 261 }; 262 263 window.dataLayer.push = function (...args) {
264 args.forEach(arg => { 265 if (Array.isArray(arg) && arg[0] === 'event') { 266 const [_, eventName, eventParams] = arg; 267 trackGAEvent({ ...eventParams, category: 'ga4_event', action: eventName }); 268 } else if (typeof arg === 'object' && arg !== null && 'event' in arg) { 269 const { event, ...params } = arg; 270 trackGAEvent({ ...params, category: 'ga4_event', action: event }); 271 } 272 }); 273 274 return originalDataLayerPush.apply(window.dataLayer, args); 275 }; 276 277 window.gtag = function (...args) { 278 if (typeof window._originalGtag === 'function') { 279 window._originalGtag.apply(window, args); 280 } 281 282 const [command, action, parameters] = args; 283 if (command === 'set' || command === 'consent') return; 284 if (command === 'event') { 285 trackGAEvent({ ...parameters, category: 'ga4_event', action }); 286 } 287 }; 288 289 live_function_impl = { 290 trackInteraction: window.sp.trackInteraction, 291 trackFormInteraction: window.sp.trackFormInteraction, 292 gtag: window.gtag, 293 dataLayerPush: window.dataLayer.push 294 }; 295 296 let pageLoadTime = Date.now(); 297 let pageScrollDepth = 0; 298 let scrollTimeout; 299 let isUnloading = false; 300 301 function trackClosed(action) { 302 if (!currentConsentState) return; 303 304 const properties = { 305 seconds_on_page: Math.round((Date.now() - pageLoadTime) / 1000).toString(), 306 page_scroll_depth: pageScrollDepth.toString(), 307 action: action 308 }; 309 310 window.snowplow('trackSelfDescribingEvent', { 311 event: { 312 schema: 'iglu:studio.flourish/flourish_closed/jsonschema/1-0-0', 313 data: properties 314 } 315 }); 316 } 317 318 function updateScrollDepth() { 319 const html = document.documentElement; 320 const currentScrollDepth = Math.round( 321 (html.scrollTop / (html.scrollHeight - html.clientHeight)) * 100 322 ); 323 pageScrollDepth = Math.max(pageScrollDepth, currentScrollDepth); 324 } 325 326 window.addEventListener('scroll', () => { 327 clearTimeout(scrollTimeout); 328 scrollTimeout = setTimeout(updateScrollDepth, 100); 329 }); 330 331 window.addEventListener('beforeunload', () => { 332 isUnloading = true; 333 trackClosed('window_close'); 334 }); 335 336 document.addEventListener('visibilitychange', () => { 337 if (document.visibilityState === 'hidden' && !isUnloading) { 338 trackClosed('tab_hidden'); 339 } else if (document.visibilityState === 'visible') { 340 isUnloading = false; 341 pageLoadTime = Date.now(); 342 // window.snowplow('trackPageView'); 343 } 344 }); 345 346 } catch (error) { 347 console.error('[SnowplowAnalytics] Error:', error); 348 } finally { 349 is_initialising = false; 350 } 351 }, 100); 352 353 function checkOneTrustConsent() { 354 if (flourishConfig.disableCookieConsent) return true; 355 356 if (window.OnetrustActiveGroups && typeof window.OnetrustActiveGroups === 'string') { 357 return window.OnetrustActiveGroups.includes('C0002'); 358 } 359 return false; 360 } 361 362 /** 363 * Revoking performance consent: we also call `crossDomainLinker(function () { return false; })` so Snowplow 364 * does not wire *new* anchors. Existing anchors keep their capture listeners (Snowplow evaluates the linker 365 * only when attaching, not on each click). Bubble phase here runs after those handlers mutate `href`, then 366 * we strip `_sp` when C0002 is off. 367 */ 368 function stripSpFromAnchorWhenConsentRevoked(evt) { 369 if (flourishConfig.disableCookieConsent) return; 370 if (checkOneTrustConsent()) return; 371 372 var target = evt.target; 373 if (!target || typeof target.closest !== 'function') return; 374 375 var anchor = target.closest('a[href], area[href]'); 376 if (!anchor || !anchor.href) return; 377 378 try { 379 var url = new URL(anchor.href, window.location.href); 380 if (!url.searchParams.has('_sp')) return; 381 url.searchParams.delete('_sp'); 382 anchor.href = url.toString(); 383 } catch (e) {} 384 } 385 386 if (!flourishConfig.disableCookieConsent) { 387 document.addEventListener('click', stripSpFromAnchorWhenConsentRevoked, false); 388 document.addEventListener('mousedown', stripSpFromAnchorWhenConsentRevoked, false); 389 } 390 391 /** 392 * Re-run Snowplow link decoration after init â e.g. OneTrust performance cookies back on, or bfcache 393 * restore. Initial decoration is configured on `newTracker` (`crossDomainLinker`); this calls the tracker 394 * command with the same predicate once `snowplowInitialized` is true. 395 */ 396 function applyCrossDomainLinker() { 397 if (!window.snowplow || !window.snowplowInitialized) return; 398 if (!checkOneTrustConsent()) return; 399 try { 400 window.snowplow('crossDomainLinker', crossDomainLinkerForFlourish); 401 } catch (e) {} 402 } 403 404 // Exposed so dynamically rendered pages (e.g. examples) can re-register 405 // new anchors with Snowplow after injecting them into the DOM. 406 window.flourishApplyCrossDomainLinker = applyCrossDomainLinker; 407 408 let currentConsentState = null; 409 410 function setupAnalyticsListeners() { 411 window.flourishOneTrust = window.flourishOneTrust || { ready: false }; 412 413 const handleConsentChange = function () { 414 const hasConsent = checkOneTrustConsent(); 415 416 if (!analytics_has_initialized) { 417 currentConsentState = hasConsent; 418 if (hasConsent) { 419 initialiseSnowplow(); 420 } 421 return; 422 } 423 424 if (currentConsentState !== hasConsent) { 425 currentConsentState = hasConsent; 426 427 try { 428 if (hasConsent && live_function_impl) { 429 window.sp.trackInteraction = live_function_impl.trackInteraction; 430 window.sp.trackFormInteraction = live_function_impl.trackFormInteraction; 431 window.gtag = live_function_impl.gtag; 432 window.dataLayer.push = live_function_impl.dataLayerPush; 433 processQueuedEvents(); 434 applyCrossDomainLinker(); 435 } else if (!hasConsent) { 436 window.sp.trackInteraction = queue_function_impl.trackInteraction; 437 window.sp.trackFormInteraction = queue_function_impl.trackFormInteraction; 438 window.gtag = queue_function_impl.gtag; 439 window.dataLayer.push = queue_function_impl.dataLayerPush; 440 if (window.snowplow && window.snowplowInitialized) { 441 try { 442 window.snowplow('crossDomainLinker', function () { return false; }); 443 } catch (e) {} 444 } 445 } 446 } catch (error) { 447 console.error('[SnowplowAnalytics] Failed to toggle tracking mode:', error); 448 } 449 } 450 }; 451 452 window.addEventListener('onetrustloaded', handleConsentChange); 453 window.addEventListener('onetrustconsentchanged', handleConsentChange); 454 document.addEventListener('OneTrustGroupsUpdated', handleConsentChange); 455 } 456 457 setupAnalyticsListeners(); 458 459 /** 460 * Hard refresh with stored performance consent: synchronous `initialiseSnowplow()` and early `onetrustloaded` 461 * can run before `window.OnetrustActiveGroups` reflects C0002; `onetrustconsentchanged` does not fire when 462 * consent did not change. Retry once when the document has loaded so restored consent can initialize Snowplow 463 * and register `crossDomainLinker` ( ?_sp on app links ). 464 */ 465 if (!flourishConfig.disableCookieConsent) { 466 if (document.readyState === 'complete') { 467 setTimeout(function () { 468 initialiseSnowplow(); 469 }, 0); 470 } else { 471 window.addEventListener('load', function snowplowOtConsentFallback() { 472 window.removeEventListener('load', snowplowOtConsentFallback); 473 initialiseSnowplow(); 474 }); 475 } 476 } 477 478 window.addEventListener('pageshow', function (event) { 479 if (event.persisted) { 480 applyCrossDomainLinker(); 481 } 482 }); 483 484 if (flourishConfig.disableCookieConsent) { 485 setTimeout(() => { 486 if (!analytics_has_initialized) { 487 const hasConsent = checkOneTrustConsent(); 488 currentConsentState = hasConsent; 489 initialiseSnowplow(); 490 } 491 }, 2000); // mocking the time it takes for getting consent in prod 492 } else { 493 const hasConsent = checkOneTrustConsent(); 494 currentConsentState = hasConsent; 495 initialiseSnowplow(); 496 } 497})();
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.