PageSourceSearch

https://flourish.studio/js/analytics-snowplow.js?104ab42b5439c0c093eac2cbdbb019ac

js flourish.studio collected 2026-09-24 06:03:22 UTC 15,657 bytes, 497 lines download raw bytes

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.