PageSourceSearch

https://www.garrisons.com/js/google-analytics.js

js garrisons.com collected 2026-09-29 00:08:25 UTC 17,075 bytes, 707 lines download raw bytes

1/**
2 * A class that handles Google Analytics events.
3 */
4class GoogleAnalytics {
5	
6	/**
7	 * A boolean indicating whether Google Analytics has been initialized.
8	 * 
9	 * @type {boolean}
10	 */
11	static #isInitialized = false;
12	
13	/**
14	 * A mapping from page types to functions that should be called when the current
15	 * page is of that type.
16	 * 
17	 * @type Record<string, () => any>
18	 */
19	static #PAGE_VIEW_HANDLER_MAP = {
20		"cart": GoogleAnalytics.onCartPageView,
21		"catalog": GoogleAnalytics.onCatalogPageView,
22		"checkout": GoogleAnalytics.onCheckoutStarted,
23		"product": GoogleAnalytics.onProductPageView,
24		"promotion": GoogleAnalytics.onPromotionPageView,
25		"order-review": GoogleAnalytics.onPurchase,
26	};
27	
28	/**
29	 * A mapping from event names to information about how to deduplicate them.
30	 * @type Record<string, { getKey: () => any, keyDescriptor: string }>
31	 */
32	static #EVENT_DEDUPLICATION_MAP = {
33		"begin_checkout": {
34			getKey: () => GoogleAnalytics.#getStructuredData().cart.id,
35			keyDescriptor: "cart ID",
36			sessionOnly: false,
37		},
38		"add_shipping_info": {
39			getKey: () => GoogleAnalytics.#getStructuredData().cart.id,
40			keyDescriptor: "cart ID",
41			sessionOnly: false,
42		},
43		"add_payment_info": {
44			getKey: () => GoogleAnalytics.#getStructuredData().cart.id,
45			keyDescriptor: "cart ID",
46			sessionOnly: false,
47		},
48		"purchase": {
49			getKey: () => GoogleAnalytics.#getStructuredData().order.id,
50			keyDescriptor: "order ID",
51			sessionOnly: false,
52		},
53	};
54	
55	/**
56	 * Initializes a new GoogleAnalytics instance.
57	 */
58	static initialize() {
59		
60		if (GoogleAnalytics.#isInitialized) return;
61		else GoogleAnalytics.#isInitialized = true;
62		
63		let data, measurementID;
64		
65		try {
66			
67			data = GoogleAnalytics.#getStructuredData();
68			measurementID = GoogleAnalytics.#getMeasurementID();
69			
70		} catch (error) {
71			
72			console.error("Google Analytics failed to initialize!\n", error);
73			
74			return;
75			
76		}
77		
78		window.dataLayer = window.dataLayer || [];
79		window.gtag = function() { dataLayer.push(arguments); }
80		gtag("js", new Date());
81		gtag("config", measurementID);
82		
83		console.info("Google Analytics initialized!");
84		
85		const pageViewHandler =
86			GoogleAnalytics.#PAGE_VIEW_HANDLER_MAP[data?.page_type];
87		
88		if (pageViewHandler !== undefined) pageViewHandler();
89		
90	}
91	
92	/**
93	 * Gets the structured data for the current page, throwing an error if it is not
94	 * available.
95	 * 
96	 * @returns {object} The structured data for the current page.
97	 * @throws {Error} If structured data is not available.
98	 */
99	static #getStructuredData() {
100		
101		const STRUCTURED_DATA_VARIABLE = "V5_STRUCTURED_DATA";
102		
103		if (STRUCTURED_DATA_VARIABLE in window) {
104			
105			return window[STRUCTURED_DATA_VARIABLE];
106			
107		} else {
108			
109			throw new Error(
110				"Structured data is not available " +
111				`(\`window.${STRUCTURED_DATA_VARIABLE}\` is missing)!`
112			);
113			
114		}
115		
116	}
117	
118	/**
119	 * Returns the measurement ID for the configured Google Analytics property.
120	 * 
121	 * @returns {string} The measurement ID for the configured Google Analytics
122	 * property.
123	 * @throws {Error} If the measurement ID is not available.
124	 */
125	static #getMeasurementID() {
126		
127		const data = GoogleAnalytics.#getStructuredData();
128		
129		const measurementID = data?.google_analytics?.measurement_id;
130		
131		if (measurementID === undefined) {
132			
133			throw new Error("No measurement ID was provided via structured data.");
134			
135		}
136		
137		return measurementID; 
138		
139	}
140	
141	/**
142	 * Maps one or more V5_STRUCTURED_DATA products to Google Analytics items.
143	 * 
144	 * @param {object | array} products The product(s) to map. 
145	 * @param includeIndex Whether to include the index of the product in the
146	 * resulting item.
147	 * @param includePrice Whether to include the price of the product in the
148	 * resulting item.
149	 * @param includeQuantity Whether to include the quantity of the product in
150	 * the resulting item.
151	 * @returns {object | array} The mapped product(s). If an array was passed
152	 * in, an array will be returned. Otherwise, an object will be returned.
153	 */
154	static #productToItem(products, {
155		includeIndex = true,
156		includePrice = true,
157		includeQuantity = true,
158	} = {}) {
159		
160		const callback = (product, index) => {
161			
162			let result = {
163				item_id: product.retailSku,
164				item_name: product.name,
165				item_brand: product.brand,
166				item_category: product.categories[0],
167				item_category2: product.categories[1],
168				item_category3: product.categories[2],
169				item_url: product.url,
170			};
171			
172			if (includeIndex) result.index = index;
173			if (includePrice) result.price = product.unit_price;
174			if (includeQuantity) result.quantity = product.quantity;
175			
176			return result;
177			
178		};
179		
180		if (Array.isArray(products)) return products.map(callback);
181		else return callback(products, 1);
182		
183	}
184	
185	/**
186	 * Searches the structured data for a product with the given local product
187	 * ID.
188	 * 
189	 * @param {string} localProductID The local ID of the product to search for.
190	 * @returns {object} The product with the given local product ID.
191	 * @throws {Error} If no product with the given local product ID was found.
192	 */
193	static #searchForProduct(localProductID) {
194		
195		const data = GoogleAnalytics.#getStructuredData();
196		
197		const allProducts = [
198			data?.page_product,
199			...(data?.page_product_cross_sells ?? []),
200			...(data?.cart?.products ?? []),
201			...(data?.order?.products ?? []),
202		];
203		
204		const result = allProducts.filter(
205			product => product !== undefined && product !== null
206		).find(product => product.local_id == localProductID);
207		
208		if (result === undefined) {
209			
210			throw new Error(
211				`Failed to find product with local ID '${localProductID}'!`
212			);
213			
214		}
215		
216		return result;
217		
218	}
219	
220	/**
221	 * Returns the properties of the current cart to send to Google Analytics.
222	 * 
223	 * @returns {object} The properties of the current cart to send to Google
224	 * Analytics.
225	 */
226	static #getCartProperties() {
227		
228		const data = GoogleAnalytics.#getStructuredData();
229		
230		return {
231			send_to: GoogleAnalytics.#getMeasurementID(),
232			currency: data.currency,
233			value: data.cart.value,
234			items: GoogleAnalytics.#productToItem(data.cart.products),
235		};
236		
237	}
238	
239	/**
240	 * Sends the given event (and optional properties) to Google Analytics,
241	 * after checking for duplicates.
242	 * 
243	 * @param event The name of the event to send.
244	 * @param properties An optional value to send as the event's properties.
245	 * @returns {void}
246	 */
247	static #sendEvent(event, properties = undefined) {
248		
249		if (event in GoogleAnalytics.#EVENT_DEDUPLICATION_MAP) {
250			
251			const {
252				getKey,
253				keyDescriptor,
254				sessionOnly,
255			} = GoogleAnalytics.#EVENT_DEDUPLICATION_MAP[event];
256			
257			const storageType = sessionOnly ? "sessionStorage" : "localStorage";
258			const storage = window[storageType];
259			const key = getKey(properties);
260			const eventID = `ga_event_${event}_${key}`;
261			
262			// If the event has already been sent, don't send it again.
263			if (storage.getItem(eventID) !== null) {
264				
265				console.warn(
266					"An attempt was made (and blocked) to fire a duplicate " +
267					`Google Analytics event: ${event}, identified by ` +
268					`${keyDescriptor} '${key}'.`
269				);
270				
271				return;
272				
273			} else storage.setItem(eventID, "true");
274			
275		}
276		
277		console.groupCollapsed(`Google Analytics event: ${event}`);
278		
279		const gtagParams = ["event", event];
280		
281		if (properties !== undefined || Object.keys(properties).length <= 0) {
282			
283			gtagParams.push(properties);
284			console.dir(properties);
285			
286		} else {
287			
288			console.info(
289				"No properties provided for event - sending event without " +
290				"properties."
291			);
292			
293		}
294		
295		gtag(...gtagParams);
296		
297		console.groupEnd();
298		
299	}
300	
301	/**
302	 * Sends a 'search' event to Google Analytics.
303	 * 
304	 * @param {string} query The search query to that originated this event.
305	 * @returns {void}
306	 */
307	static onSearch(query) {
308		
309		// TODO - Add search results to the event.
310		
311		GoogleAnalytics.#sendEvent("search", {
312			send_to: GoogleAnalytics.#getMeasurementID(),
313			"search_term": query,
314		});
315		
316	}
317	
318	/**
319	 * Sends a 'view_promotion' event to Google Analytics with the appropriate
320	 * properties.
321	 *
322	 * @returns {void}
323	 */
324	static onPromotionPageView() {
325		
326		GoogleAnalytics.#sendEvent("view_promotion", {
327			send_to: GoogleAnalytics.#getMeasurementID(),
328			creative_name: "",
329			creative_slot: "",
330			promotion_id: "",
331			promotion_name: "",
332			items: "",
333		});
334		
335	}
336	
337	/**
338	 * Sends a 'view_item_list' event to Google Analytics with the appropriate
339	 * properties.
340	 *
341	 * @returns {void}
342	 */
343	static onCatalogPageView() {
344		
345		GoogleAnalytics.#sendEvent("view_item_list", {
346			send_to: GoogleAnalytics.#getMeasurementID(),
347			item_list_id: "",
348			item_list_name: "",
349			items: "",
350		});
351		
352	}
353	
354	/**
355	 * Sends a 'view_item' event to Google Analytics with the appropriate
356	 * properties.
357	 *
358	 * @returns {void}
359	 */
360	static onProductPageView() {
361		
362		const data = GoogleAnalytics.#getStructuredData();
363		const item = GoogleAnalytics.#productToItem(data.page_product, {
364			includeIndex: false,
365			includeQuantity: false,
366		});
367		
368		GoogleAnalytics.#sendEvent("view_item", {
369			send_to: GoogleAnalytics.#getMeasurementID(),
370			currency: data.currency,
371			value: data.page_product.value,
372			items: [item],
373		});
374		
375	}
376	
377	/**
378	 * Sends a 'view_cart' event to Google Analytics with the appropriate
379	 * properties.
380	 *
381	 * @returns {void}
382	 */
383	static onCartPageView() {
384		
385		const data = GoogleAnalytics.#getStructuredData();
386		let properties = undefined;
387		
388		if ("cart" in data) {
389			
390			properties = GoogleAnalytics.#getCartProperties();
391			
392		}
393		
394		GoogleAnalytics.#sendEvent("view_cart", properties);
395		
396	}
397	
398	/**
399	 * Sends a 'generate_lead' event to Google Analytics with the appropriate
400	 * properties.
401	 *
402	 * @returns {void}
403	 */
404	static onCustomFormSubmission() {
405		
406		/* GoogleAnalytics.#sendEvent("generate_lead", {
407			send_to: GoogleAnalytics.#getMeasurementID()
408		});*/
409		
410	}
411	
412	/**
413	 * Sends a 'generate_lead' event to Google Analytics with the appropriate
414	 * properties.
415	 *
416	 * @returns {void}
417	 */
418	static onEmailSignupFormSubmission() {
419		
420		GoogleAnalytics.#sendEvent("generate_lead", {
421			send_to: GoogleAnalytics.#getMeasurementID(),
422			form_type: "email_signup",
423		});
424		
425	}
426	
427	/**
428	 * Sends a 'generate_lead' event to Google Analytics with the appropriate
429	 * properties.
430	 *
431	 * @returns {void}
432	 */
433	static onProductRequestFormSubmission(retailSku) {
434		
435		const data = GoogleAnalytics.#getStructuredData();
436		
437		GoogleAnalytics.#sendEvent("generate_lead", {
438			send_to: GoogleAnalytics.#getMeasurementID(),
439			form_type: "product_request",
440			product_id: data.page_product.retailSku,
441		});
442		
443	}
444	
445	/**
446	 * Sends a 'generate_lead' event to Google Analytics with the appropriate
447	 * properties.
448	 * 
449	 * @returns {void}
450	 */
451	static onContactFormSubmission() {
452		
453		GoogleAnalytics.#sendEvent("generate_lead", {
454			send_to: GoogleAnalytics.#getMeasurementID(),
455			form_type: "contact",
456		});
457		
458	}
459	
460	/**
461	 * Sends a 'sign_up' event to Google Analytics with the appropriate
462	 * properties.
463	 * 
464	 * @param method The name of the method that was used to sign up.
465	 * Examples: "Email", "Google", "Facebook", etc.
466	 * @returns {void}
467	 */
468	static onSignUp(method) {
469		
470		GoogleAnalytics.#sendEvent("sign_up", {
471			send_to: GoogleAnalytics.#getMeasurementID(),
472			method
473		});
474		
475	}
476	
477	/**
478	 * Sends a 'login' event to Google Analytics with the appropriate
479	 * properties.
480	 *
481	 * @param method The name of the method that was used to sign in.
482	 * Examples: "Email", "Google", "Facebook", etc.
483	 * @returns {void}
484	 */
485	static onSignIn(method) {
486		
487		GoogleAnalytics.#sendEvent("login", {
488			send_to: GoogleAnalytics.#getMeasurementID(),
489			method
490		});
491		
492	}
493	
494	/**
495	 * Sends an 'add_to_wishlist' event to Google Analytics with the appropriate
496	 * properties.
497	 * 
498	 * @param localProductID The local ID of the product that was added to the
499	 * wishlist.
500	 * @returns {void}
501	 */
502	static onAddToWishlist(localProductID) {
503		
504		const data = GoogleAnalytics.#getStructuredData();
505		const product = GoogleAnalytics.#searchForProduct(localProductID);
506		const item = GoogleAnalytics.#productToItem(product, {
507			includeIndex: false,
508			includeQuantity: false,
509		});
510		
511		GoogleAnalytics.#sendEvent("add_to_wishlist", {
512			send_to: GoogleAnalytics.#getMeasurementID(),
513			currency: data.currency,
514			value: product.value,
515			items: [item],
516		});
517		
518	}
519	
520	/**
521	 * Sends a 'add_to_cart' event to Google Analytics with the appropriate
522	 * properties.
523	 *
524	 * @param {string} localProductID The local ID of the product that was added
525	 * to the cart.
526	 * @param {number} quantity The quantity of the product that was added to
527	 * the cart.
528	 * @returns {void}
529	 */
530	static onAddToCart(localProductID, quantity) {
531		
532		//Multiple products can be added to cart through cross sells
533		const productIds = localProductID.toString().split(',');
534
535		quantity = parseFloat(quantity);
536		
537		const data = GoogleAnalytics.#getStructuredData();
538
539		productIds.forEach((localProduct) => {
540			const product = GoogleAnalytics.#searchForProduct(localProduct);
541			
542			const item = GoogleAnalytics.#productToItem(product, {
543				includeIndex: false,
544				includeQuantity: false,
545			});
546			
547			item.quantity = quantity;
548			
549			if (product.unit_price === undefined) {
550				
551				throw new Error(
552					"Cannot add product with no unit price to cart! " +
553					`(local ID: '${localProduct}')`
554				);
555				
556			}
557			
558			const value = product.unit_price * quantity;
559			
560			// V5_STRUCTURED_DATA.cart ??= {};
561			// V5_STRUCTURED_DATA.cart.value ??= 0;
562			// V5_STRUCTURED_DATA.cart.coupons ??= [];
563			// V5_STRUCTURED_DATA.cart.products ??= [];
564			//
565			// V5_STRUCTURED_DATA.cart.value += value;
566			// V5_STRUCTURED_DATA.cart.products.push(product);
567			
568			GoogleAnalytics.#sendEvent("add_to_cart", {
569				send_to: GoogleAnalytics.#getMeasurementID(),
570				currency: data.currency,
571				value,
572				items: [item]
573			});
574		});
575		
576	}
577	
578	/**
579	 * Sends a 'remove_from_cart' event to Google Analytics with the appropriate
580	 * properties.
581	 *
582	 * @param {string} localProductID The local ID of the product that was
583	 * removed from the cart.
584	 * @param {number} quantity The quantity of the product that was removed
585	 * from the cart.
586	 * @returns {void}
587	 */
588	static onRemoveFromCart(localProductID, quantity) {
589		
590		quantity = parseFloat(quantity);
591		
592		const data = GoogleAnalytics.#getStructuredData();
593		const product = GoogleAnalytics.#searchForProduct(localProductID);
594		
595		const item = GoogleAnalytics.#productToItem(product, {
596			includeIndex: false,
597			includeQuantity: false,
598		});
599		
600		item.quantity = quantity;
601		
602		if (product.unit_price === undefined) {
603			
604			throw new Error(
605				"Cannot remove product with no unit price from cart! " +
606				`(local ID: '${localProductID}')`
607			);
608			
609		}
610		
611		const value = product.unit_price * quantity;
612		
613		// V5_STRUCTURED_DATA.cart ??= {};
614		// V5_STRUCTURED_DATA.cart.value ??= 0;
615		// V5_STRUCTURED_DATA.cart.coupons ??= [];
616		// V5_STRUCTURED_DATA.cart.products ??= [];
617		//
618		// V5_STRUCTURED_DATA.cart.value += value;
619		// V5_STRUCTURED_DATA.cart.products.push(product);
620		
621		GoogleAnalytics.#sendEvent("remove_from_cart", {
622			send_to: GoogleAnalytics.#getMeasurementID(),
623			currency: data.currency,
624			value,
625			items: [item]
626		});
627		
628	}
629	
630	
631	/**
632	 * Sends a 'begin_checkout' event to Google Analytics with the appropriate
633	 * properties.
634	 * 
635	 * @returns {void}
636	 */
637	static onCheckoutStarted() {
638		
639		GoogleAnalytics.#sendEvent("begin_checkout", {
640			send_to: GoogleAnalytics.#getMeasurementID(),
641			...GoogleAnalytics.#getCartProperties(),
642		});
643		
644	}
645	
646	/**
647	 * Sends a 'add_shipping_info' event to Google Analytics with the
648	 * appropriate properties.
649	 * 
650	 * @returns {void}
651	 */
652	static onAddShippingInfo() {
653		
654		GoogleAnalytics.#sendEvent("add_shipping_info", {
655			send_to: GoogleAnalytics.#getMeasurementID(),
656			...GoogleAnalytics.#getCartProperties(),
657		});
658		
659	}
660	
661	/**
662	 * Sends a 'add_payment_info' event to Google Analytics with the appropriate
663	 * properties.
664	 *
665	 * @returns {void}
666	 */
667	static onAddPaymentInfo(payment_type) {
668		
669		GoogleAnalytics.#sendEvent("add_payment_info", {
670			send_to: GoogleAnalytics.#getMeasurementID(),
671			...GoogleAnalytics.#getCartProperties(),
672			payment_type,
673		});
674		
675	}
676	
677	/**
678	 * Sends a 'purchase' event to Google Analytics with the appropriate
679	 * properties.
680	 * 
681	 * @returns {void}
682	 */
683	static onPurchase() {
684		
685		const data = GoogleAnalytics.#getStructuredData();
686		
687		const properties = {
688			send_to: GoogleAnalytics.#getMeasurementID(),
689			currency: data.currency,
690			transaction_id: data.order.order_number,
691			value: data.order.value,
692			shipping: data.order.delivery + data.order.shipping,
693			tax: data.order.tax,
694			items: GoogleAnalytics.#productToItem(data.order.products),
695		};
696		
697		const first_coupon = (data?.order?.coupons ?? [])[0];
698		
699		if (first_coupon !== undefined) properties.coupon = first_coupon;
700		
701		GoogleAnalytics.#sendEvent("purchase", properties);
702		
703	}
704	
705}
706
707document.addEventListener("DOMContentLoaded", GoogleAnalytics.initialize);

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.