PageSourceSearch

https://petremedy.co.uk/wp-content/plugins/mailchimp-for-woocommer…chimp-woocommerce-pixel-tracking.js?ver=6.3

js petremedy.co.uk collected 2026-09-24 11:58:07 UTC 24,422 bytes, 796 lines download raw bytes

1/**
2 * Mailchimp Pixel Tracking for WooCommerce
3 *
4 * Listens for WooCommerce events and sends them to the Mailchimp Pixel SDK.
5 * Only tracks if window.$mcSite.pixel.api is available (MC.js loaded).
6 *
7 * @package MailChimp_WooCommerce
8 * @since 1.0.0
9 */
10
11(function ($) {
12	'use strict';
13
14	/**
15	 * Default config for waitForPixelSDK exponential backoff
16	 */
17	const PIXEL_SDK_WAIT_CONFIG = {
18		initialDelayMs: 100,
19		maxDelayMs: 5000,
20		maxAttempts: 20
21	};
22
23	/**
24	 * How long PRODUCT_VIEWED waits on a variable product for WooCommerce to resolve a
25	 * default or deep-linked variation before settling for the parent.
26	 */
27	const VARIATION_RESOLVE_WAIT_MS = 1200;
28
29	/**
30	 * Settle time after a form interaction before re-reading the variation id input.
31	 * The blocks form writes it through the Interactivity API on the next render.
32	 */
33	const VARIATION_INPUT_SETTLE_MS = 150;
34
35	/**
36	 * Mailchimp Pixel Tracking Handler
37	 */
38	const MailchimpPixelTracking = {
39		// Debounce timers for cart fetch calls
40		_atcTimer: null,
41		_rfcTimer: null,
42		_atcFetching: false,
43		_rfcFetching: false,
44		// PRODUCT_VIEWED state for a variable product being resolved to a variation
45		_viewTimer: null,
46		_viewedParent: null,
47		_viewedKeys: null,
48		_inputTimer: null,
49
50		/**
51		 * Initialize tracking.
52		 * Waits for Pixel SDK with exponential backoff, then sends page events and attaches handlers.
53		 */
54		init: function () {
55			const self = this;
56
57			// Registered before the SDK wait on purpose: that wait runs for seconds, and an
58			// integration that resolves its variation early must not race us.
59			window.mcPixel = window.mcPixel || {};
60			window.mcPixel.setVariation = function (variationId, product) {
61				self.setVariation(variationId, product);
62			};
63			document.addEventListener('mailchimp_woocommerce_set_variation', function (event) {
64				const detail = event.detail || {};
65				self.setVariation(detail.variationId, detail.product);
66			});
67
68			this.waitForPixelSDK(PIXEL_SDK_WAIT_CONFIG)
69				.then(function () {
70					self.sendPageEvents();
71					self.attachCartEventListeners();
72					self.interceptStoreApiRequests();
73					console.log('Mailchimp Pixel SDK loaded.');
74				})
75				.catch(function (e) {
76					console.log('Mailchimp Pixel SDK not loaded within timeout. Tracking disabled.', e);
77				});
78		},
79
80		/**
81		 * Wait for Pixel SDK to become available using exponential backoff.
82		 *
83		 * @param {Object} options Optional config
84		 * @param {number} options.initialDelayMs First delay in ms (default 100)
85		 * @param {number} options.maxDelayMs Cap on delay in ms (default 5000)
86		 * @param {number} options.maxAttempts Max number of attempts (default 20)
87		 * @return {Promise<void>} Resolves when SDK is available, rejects after maxAttempts
88		 */
89		waitForPixelSDK: function (options) {
90			const config = options || {};
91			const initialDelayMs = config.initialDelayMs !== undefined ? config.initialDelayMs : PIXEL_SDK_WAIT_CONFIG.initialDelayMs;
92			const maxDelayMs = config.maxDelayMs !== undefined ? config.maxDelayMs : PIXEL_SDK_WAIT_CONFIG.maxDelayMs;
93			const maxAttempts = config.maxAttempts !== undefined ? config.maxAttempts : PIXEL_SDK_WAIT_CONFIG.maxAttempts;
94
95			function isSDKReady() {
96				return typeof window.$mcSite !== 'undefined' &&
97					window.$mcSite.pixel &&
98					typeof window.$mcSite.pixel.api !== 'undefined' &&
99					typeof window.$mcSite.pixel.api.track === 'function' &&
100					window.$mcSite.pixel.installed === true;
101			}
102
103			return new Promise(function (resolve, reject) {
104				let attempt = 0;
105
106				function scheduleCheck() {
107					if (isSDKReady()) {
108						// FIXME: temporary wait to accommodate for not being able to detect ready state
109						console.warn('Pixel SDK - remediation for pixel ready issue')
110						setTimeout(function () {
111							resolve();
112						}, 1000);
113						return;
114					}
115					if (attempt >= maxAttempts) {
116						reject(new Error('Pixel SDK not available'));
117						return;
118					}
119
120					const delay = Math.min(
121						initialDelayMs * Math.pow(2, attempt),
122						maxDelayMs
123					);
124					attempt += 1;
125
126					setTimeout(scheduleCheck, delay);
127				}
128
129				scheduleCheck();
130			});
131		},
132
133		/**
134		 * Synchronous check if Pixel SDK is available (e.g. before each track call).
135		 *
136		 * @return {boolean} True if SDK is available
137		 */
138		isPixelSDKReady: function () {
139			return typeof window.$mcSite !== 'undefined' &&
140				window.$mcSite.pixel &&
141				typeof window.$mcSite.pixel.api !== 'undefined' &&
142				typeof window.$mcSite.pixel.api.track === 'function';
143		},
144
145		/**
146		 * Get cart ID from window.mcPixel
147		 *
148		 * @return {string} Cart ID
149		 */
150		getCartId: function () {
151			return window.mcPixel && window.mcPixel.cartId ? window.mcPixel.cartId : '';
152		},
153
154		/**
155		 * Get the REST API base URL from the localized config.
156		 *
157		 * @return {string} REST base URL
158		 */
159		getRestBase: function () {
160			return (window.mcPixelConfig && window.mcPixelConfig.restBase) || '/wp-json/mailchimp-for-woocommerce/v1/';
161		},
162
163		/**
164		 * Send page-level events based on pre-populated data
165		 */
166		sendPageEvents: function () {
167			if (!window.mcPixel || !window.mcPixel.data) {
168				return;
169			}
170
171			const data = window.mcPixel.data;
172			const events = data.events || [];
173
174			// Send events based on what was set by PHP
175			events.forEach((eventType) => {
176				//console.log('Mailchimp Pixel: Sending event', eventType, data);
177				switch (eventType) {
178					case 'PRODUCT_ADDED_TO_CART':
179						if (data.added_to_cart) {
180							var atcItems = Array.isArray(data.added_to_cart) ? data.added_to_cart : [data.added_to_cart];
181							for (var ai = 0; ai < atcItems.length; ai++) {
182								this.sendProductAddedToCart(atcItems[ai]);
183							}
184						}
185						break;
186					case 'PRODUCT_REMOVED_FROM_CART':
187						if (data.removed_from_cart) {
188							var rfcItems = Array.isArray(data.removed_from_cart) ? data.removed_from_cart : [data.removed_from_cart];
189							for (var ri = 0; ri < rfcItems.length; ri++) {
190								this.sendProductRemovedFromCart(rfcItems[ri]);
191							}
192						}
193						break;
194					case 'IDENTITY':
195						if (data.identity && data.identity.email) {
196							this.sendIdentityEvent(data.identity.email);
197						}
198						break;
199					case 'PRODUCT_VIEWED':
200						// Skip if an add-to-cart or remove-from-cart already fired for the same product.
201						// The cart action implies the view, so firing both is redundant.
202						if (data.product) {
203							const viewedId = String(data.product.productId || data.product.id);
204							const atcArr = Array.isArray(data.added_to_cart) ? data.added_to_cart : (data.added_to_cart ? [data.added_to_cart] : []);
205							const rfcArr = Array.isArray(data.removed_from_cart) ? data.removed_from_cart : (data.removed_from_cart ? [data.removed_from_cart] : []);
206							const atcMatch = atcArr.some(function (p) { return String(p.productId || p.id
206) === viewedId; });
207							const rfcMatch = rfcArr.some(function (p) { return String(p.productId || p.id) === viewedId; });
208
209							if (
210								(events.includes('PRODUCT_ADDED_TO_CART') && atcMatch) ||
211								(events.includes('PRODUCT_REMOVED_FROM_CART') && rfcMatch)
212							) {
213								//console.log('Mailchimp Pixel: Skipping PRODUCT_VIEWED (superseded by cart event for same product)');
214								break;
215							}
216							this.trackProductViewed(data.product);
217						}
218						break;
219					case 'CART_VIEWED':
220						if (data.cart) {
221							this.sendCartViewed(data.cart);
222						}
223						break;
224					case 'CHECKOUT_STARTED':
225						if (data.checkout) {
226							this.sendCheckoutStarted(data.checkout);
227							window.mcPixel._handled.checkout = true;
228						}
229						break;
230					case 'PURCHASED':
231						if (data.order) {
232							this.sendPurchased(data.order);
233						}
234						break;
235					case 'PRODUCT_CATEGORY_VIEWED':
236						if (data.category) {
237							this.sendCategoryViewed(data.category);
238							window.mcPixel._handled.category = true;
239						}
240						break;
241					case 'SEARCH_SUBMITTED':
242						if (data.search) {
243							this.sendSearchSubmitted(data.search);
244							window.mcPixel._handled.search = true;
245						}
246						break;
247				}
248			});
249		},
250
251		/**
252		 * PRODUCT_VIEWED for the single product page.
253		 *
254		 * A variable product renders before the customer has picked anything, so the
255		 * server-side payload only knows the parent. WooCommerce announces the chosen
256		 * variation on the .variations_form with found_variation, so we hold the event
257		 * until that fires - or until it's clear nothing is going to resolve.
258		 *
259		 * @param {Object} product Parent product data from PHP
260		 */
261		trackProductViewed: function (product) {
262			const self = this;
263			this._viewedKeys = this._viewedKeys || {};
264
265			// not a variable product - the server already knew everything there is to know
266			if (!window.mcPixel.data.product_variations) {
267				this.sendProductViewedOnce(product, product.id);
268				return;
269			}
270
271			this._viewedParent = product;
272			this.attachVariationListeners();
273
274			const resolved = this.currentVariationId();
275			if (resolved) {
276				this.sendProductViewedOnce(this.resolveVariationPayload({ variation_id: resolved }), resolved);
277				return;
278			}
279
280			// nothing picked yet, but a default variation or ?attribute_* deep link still
281			// resolves a tick after the form initialises - give WooCommerce that tick
282			// before settling for the parent
283			this._viewTimer = setTimeout(function () {
284				self.sendProductViewedOnce(product, product.id);
285			}, VARIATION_RESOLVE_WAIT_MS);
286		},
287
288		/**
289		 * One PRODUCT_VIEWED per thing actually viewed: the parent, then each variation
290		 * the customer lands on. Switching back to an earlier pick sends nothing, so
291		 * clicking through a dropdown doesn't turn into a burst of events.
292		 *
293		 * @param {Object} product Product data to send
294		 * @param {string} key Dedup key - the variation id, or the parent id
295		 */
296		sendProductViewedOnce: function (product, key) {
297			clearTimeout(this._viewTimer);
298			if (this._viewedKeys[key]) {
299				return;
300			}
301			// only claim the key once it actually went out - an integration can call
302			// setVariation() before the SDK is up, and that must not count as sent
303			if (this.sendProductViewed(product)) {
304				this._viewedKeys[key] = true;
305			}
306		},
307
308		/**
309		 * Public API for themes, swatch plugins and custom templates.
310		 *
311		 * Detection covers what WooCommerce itself ships: the classic variations form,
312		 * and the hidden variation_id input the block form maintains. Anything else - a
313		 * custom swatch UI, a page builder widget, a headless front end - can announce
314		 * the selection itself, either directly:
315		 *
316		 *     window.mcPixel.setVariation( 123 );
317		 *
318		 * or, when it cannot reach our global, by dispatching an event:
319		 *
320		 *     document.dispatchEvent( new CustomEvent(
321		 *         'mailchimp_woocommerce_set_variation',
322		 *         { detail: { variationId: 123 } }
323		 *     ) );
324		 *
325		 * Pass a second argument to supply the whole product payload instead of letting
326		 * us resolve it. Sending the same variation twice is a no-op either way.
327		 *
328		 * @param {number|string} variationId Variation being viewed
329		 * @param {Object} [product] Optional product payload, sent as-is
330		 */
331		setVariation: function (variationId, product) {
332			const id = String(variationId || '');
333
334			if (!id || id === '0') {
335				return;
336			}
337			// an integration may beat PRODUCT_VIEWED to it - nothing to dedup against yet
338			this._viewedKeys = this._viewedKeys || {};
339			this.sendProductViewedOnce(
340				product || this.resolveVariationPayload({ variation_id: id }),
341				id
342			);
343		},
344
345		/**
346		 * found_variation is WooCommerce's own signal that every attribute is chosen and
347		 * a variation matched. Bound delegated off body so a form rendered late - blocks,
348		 * lazy themes, quick-view modals - is still covered.
349		 */
350		attachVariationListeners: function () {
351			const self = this;
352
353			$(document.body).on('found_variation', '.variations_form', function (event, variation) {
354				if (!variation || !variation.variation_id) {
355					return;
356				}
357				self.sendProductViewedOnce(
358					self.resolveVariationPayload(variation),
359					String(variation.variation_id)
360				);
361			});
362
363			// covers the block form, which fires no event at all
364			this.watchVariationInput();
365		},
366
367		/**
368		 * WooCommerce often resolves a variation before we boot - the SDK wait runs over a
369		 * second, and the form fires found_variation during its own init. The form parks
370		 * the answer in its variation_id input, so read that rather than assuming we were
371		 * listening at the right moment.
372		 *
373		 * @return {string} Resolved variation id, or '' when nothing is selected
374		 */
375		currentVariationId: function () {
376			// Classic puts this in .variations_form, blocks puts it in .single_variation_wrap
377			// inside its own form element - so key off the input, not either wrapper.
378			const inputs = document.querySelectorAll('form input[name="variation_id"], form input.variation_id');
379
380			for (let i = 0; i < inputs.length; i++) {
381				const id = parseInt(inputs[i].value, 10);
382				if (id > 0) {
383					return String(id);
384				}
385			}
386
387			return '';
388		},
389
390		/**
391		 * Catch variation changes on templates that announce nothing.
392		 *
393		 * The blocks "Add to Cart with Options" form runs on the Interactivity API and
394		 * fires no jQuery event and no CustomEvent, so found_variation never arrives.
395		 * What it does keep is the hidden variation_id input - WooCommerce renders it
396		 * specifically so "extensions or Express Payment methods" can read the form
397		 * state - so re-read that whenever the customer touches the form. Its value is
398		 * set as a property by the Interactivity API, which a MutationObserver would
399		 * never see, hence driving off the interaction instead.
400		 */
401		watchVariationInput: function () {
402			const self = this;
403
404			const recheck = function (event) {
405				// blocks renders its attribute options as buttons, so change alone misses them
406				if (!event.target || !event.target.closest || !event.target.closest('form')) {
407					return;
408				}
409				clearTimeout(self._inputTimer);
410				self._inputTimer = setTimeout(function () {
411					const id = self.currentVariationId();
412					if (id) {
413						self.sendProductViewedOnce(self.resolveVariationPayload({ variation_id: id }), id);
414					}
415				}, VARIATION_INPUT_SETTLE_MS);
416			};
417
418			document.addEventListener('change', recheck, true);
419			document.addEventListener('click', recheck, true);
420		},
421
422		/**
423		 * Turn a resolved variation into the product shape the pixel expects.
424		 *
425		 * Prefer the server-rendered variation: it carries the sku, categories and
426		 * parent-derived permalink that the event payload has no idea about, and it
427		 * matches what add-to-cart sends for the same variation. Past the pre-render cap
428		 * there is nothing to look up, so patch the parent with what the event does give
429		 * us - the id being right matters more than the rest.
430		 *
431		 * @param {Object} variation found_variation payload (or just {variation_id})
432		 * @return {Object} Product data for the pixel
433		 */
434		resolveVariationPayload: function (variation) {
435			const parent = this._viewedParent || {};
436			const id = String(variation.variation_id);
437			const rendered = window.mcPixel.data.product_variations[id];
438
439			if (rendered) {
440				return rendered;
441			}
442
443			const patched = $.extend({}, parent, {
444				id: id,
445				productId: parent.productId || parent.id || id
446			});
447			if (variation.sku) {
448				patched.sku = variation.sku;
449			}
450			if (variation.display_price !== undefined && variation.display_price !== null) {
451				patched.price = parseFloat(variation.display_price);
452			}
453			if (variation.image && variation.image.full_src) {
454				patched.imageUrl = variation.image.full_src;
455			}
456
457			return patched;
458		},
459
460		/**
461		 * Send PRODUCT_VIEWED event
462		 *
463		 * @param {Object} product Product data
464		 */
465		sendProductViewed: function (product) {
466			if (!this.isPixelSDKReady()) return false;
467
468			window.$mcSite.pixel.api.track('PRODUCT_VIEWED', {
469				product: product
470			}).catch((error) => {
471				console.error('Mailchimp Pixel: Error tracking PRODUCT_VIEWED', error);
472			});
473
474			return true;
475		},
476
477		/**
478		 * Send PRODUCT_ADDED_TO_CART event
479		 *
480		 * @param {Object} product Product data
481		 */
482		sendProductAddedToCart: function (product) {
483			if (!this.isPixelSDKReady()) return;
484
485			const cartId = this.getCartId();
486			const eventData = {
487				cartId: cartId,
488				product: {
489					item: {
490						id: product.id,
491						productId: product.productId,
492						title: product.title,
493						price: product.price,
494						currency: product.currency,
495						sku: product.sku || ''
496					},
497					quantity: product.quantity || 1,
498					price: product.price * (product.quantity || 1),
499					currency: product.currency
500				}
501			};
502
503			window.$mcSite.pixel.api.track('PRODUCT_ADDED_TO_CART', eventData).catch((error) => {
504				console.error('Mailchimp Pixel: Error tracking PRODUCT_ADDED_TO_CART', error);
505			});
506		},
507
508		/**
509		 * Send PRODUCT_REMOVED_FROM_CART event
510		 *
511		 * @param {Object} product Product data
512		 */
513		sendProductRemovedFromCart: function (product) {
514			if (!this.isPixelSDKReady()) return;
515
516			const cartId = this.getCartId();
517			const eventData = {
518				cartId: cartId,
519				product: {
520					item: {
521						id: product.id,
522						productId: product.productId,
523						title: product.title,
524						price: product.price,
525						currency: product.currency,
526						sku: product.sku || ''
527					},
528					quantity: product.quantity || 1,
529					price: product.price * (product.quantity || 1),
530					currency: product.currency
531				}
532			};
533
534			window.$mcSite.pixel.api.track('PRODUCT_REMOVED_FROM_CART', eventData).catch((error) => {
535				console.error('Mailchimp Pixel: Error tracking PRODUCT_REMOVED_FROM_CART', error);
536			});
537		},
538
539		/**
540		 * Send CART_VIEWED event
541		 *
542		 * @param {Object} cart Cart data
543		 */
544		sendCartViewed: function (cart) {
545			if (!this.isPixelSDKReady()) return;
546
547			window.$mcSite.pixel.api.track('CART_VIEWED', {
548				cart: cart
549			}).catch((error) => {
550				console.error('Mailchimp Pixel: Error tracking CART_VIEWED', error);
551			});
552		},
553
554		/**
555		 * Send CHECKOUT_STARTED event
556		 *
557		 * @param {Object} checkout Checkout data
558		 */
559		sendCheckoutStarted: function (checkout) {
560			if (!this.isPixelSDKReady()) return;
561
562			window.$mcSite.pixel.api.track('CHECKOUT_STARTED', {
563				checkout: checkout
564			}).catch((error) => {
565				console.error('Mailchimp Pixel: Error tracking CHECKOUT_STARTED', error);
566			});
567		},
568
569		sendIdentityEvent: function(email) {
570			if (!this.isPixelSDKReady()) return;
571			window.$mcSite.pixel.api.identify({
572				type: 'EMAIL',
573				value: email
574			});
575		},
576
577		/**
578		 * Send PURCHASED event
579		 *
580		 * @param {Object} order Order data
581		 */
582		sendPurchased: function (order) {
583			if (!this.isPixelSDKReady()) return;
584
585			window.$mcSite.pixel.api.track('PURCHASED', {
586				order: order
587			}).catch((error) => {
588				console.error('Mailchimp Pixel: Error tracking PURCHASED', error);
589			});
590		},
591
592		/**
593		 * Send PRODUCT_CATEGORY_VIEWED event
594		 *
595		 * @param {Object} category Category data
596		 */
597		sendCategoryViewed: function (category) {
598			if (!this.isPixelSDKReady()) return;
599
600			window.$mcSite.pixel.api.track('PRODUCT_CATEGORY_VIEWED', category).catch((error) => {
601				console.error('Mailchimp Pixel: Error tracking PRODUCT_CATEGORY_VIEWED', error);
602			});
603		},
604
605		/**
606		 * Send SEARCH_SUBMITTED event
607		 *
608		 * @param {Object} search Search data
609		 */
610		sendSearchSubmitted: function (search) {
611			if (!this.isPixelSDKReady()) return;
612
613			window.$mcSite.pixel.api.track('SEARCH_SUBMITTED', search).catch((error) => {
614				console.error('Mailchimp Pixel: Error tracking SEARCH_SUBMITTED', error);
615			});
616		},
617
618		/**
619		 * Find product by ID from pre-loaded products
620		 *
621		 * @param {number|string} productId Product ID
622		 * @return {Object|null} Product data or null
623		 */
624		findProductById: function (productId) {
625			if (!window.mcPixel || !window.mcPixel.data || !window.mcPixel.data.products) {
626				return null;
627			}
628
629			const products = window.mcPixel.data.products;
630			const id = String(productId);
631
632			for (let i = 0; i < products.length; i++) {
633				if (String(products[i].id) === id) {
634					return products[i];
635				}
636			}
637
638			return null;
639		},
640
641		/**
642		 * Debounced fetch for add-to-cart events.
643		 * Each call resets the timer so rapid-fire triggers coalesce into one
644		 * fetch that drains the entire PHP queue.
645		 * If a fetch is already in flight, schedule another drain after it completes.
646		 */
647		fetchAndTrackAddToCart: function () {
648			var self = this;
649			if (!this.isPixelSDKReady()) return;
650
651			clearTimeout(this._atcTimer);
652			this._atcTimer = setTimeout(function () {
653				self._drainAddToCartQueue();
654			}, 600);
655		},
656
657		_drainAddToCartQueue: async function () {
658			if (this._atcFetching) return;
659			this._atcFetching = true;
660
661			try {
662				var res = await fetch(this.getRestBase() + 'pixel/atc', {
663					method: 'GET',
664					credentials: 'same-origin',
665					headers: { 'Accept': 'application/json' },
666				});
667
668				if (!res.ok) return;
669
670				var data = await res.json();
671				if (!data) return;
672
673				var items = Array.isArray(data) ? data : [data];
674				for (var i = 0; i < items.length; i++) {
675					this.sendProductAddedToCart(items[i]);
676				}
677			} catch (e) {
678				// no-op
679			} finally {
680				this._atcFetching = false;
681			}
682		},
683
684		/**
685		 * Debounced fetch for remove-from-cart events.
686		 * Same debounce pattern as add-to-cart.
687		 */
688		fetchAndTrackRemoveFromCart: function () {
689			var self = this;
690			if (!this.isPixelSDKReady()) return;
691
692			clearTimeout(this._rfcTimer);
693			this._rfcTimer = setTimeout(function () {
694				self._drainRemoveFromCartQueue();
695			}, 600);
696		},
697
698		_drainRemoveFromCartQueue: async function () {
699			if (this._rfcFetching) return;
700			this._rfcFetching = true;
701
702			try {
703				var res = await fetch(this.getRestBase() + 'pixel/rfc', {
704					method: 'GET',
705					credentials: 'same-origin',
706					headers: { 'Accept': 'application/json' },
707				});
708
709				if (!res.ok) return;
710
711				var data = await res.json();
712				if (!data) return;
713
714				var items = Array.isArray(data) ? data : [data];
715				for (var i = 0; i < items.length; i++) {
716					this.sendProductRemovedFromCart(items[i]);
717				}
718			} catch (e) {
719				// no-op
720			} finally {
721				this._rfcFetching = false;
722			}
723		},
724
725		/**
726		 * Attach DOM and jQuery event listeners for cart add/remove.
727		 * These serve as fallback triggers alongside the fetch interceptor.
728		 */
729		attachCartEventListeners: function () {
730			const self = this;
731
732			// Classic themes (jQuery events fired by WooCommerce's add-to-cart.js)
733			$(document.body).on('added_to_cart', function () {
734				self.fetchAndTrackAddToCart();
735			});
736			$(document.body).on('removed_from_cart', function () {
737				self.fetchAndTrackRemoveFromCart();
738			});
739
740			// WC Blocks DOM CustomEvents
741			document.body.addEventListener('wc-blocks_added_to_cart', function () {
742				self.fetchAndTrackAddToCart();
743			});
744			document.body.addEventListener('wc-blocks_removed_from_cart', function () {
745				self.fetchAndTrackRemoveFromCart();
746			});
747		},
748
749		/**
750		 * Intercept window.fetch to detect WooCommerce Store API cart mutations.
751		 *
752		 * This is the most reliable method for catching add-to-cart and remove-from-cart
753		 * in block-based WooCommerce setups, where DOM events may not fire consistently.
754		 * Watches for successful POST requests to the Store API cart endpoints.
755		 */
756		interceptStoreApiRequests: function () {
757			const self = this;
758			const originalFetch = window.fetch;
759
760			window.fetch = function (input, init) {
761				var promise = originalFetch.apply(this, arguments);
762
763				promise.then(function (response) {
764					try {
765						if (!response.ok) return;
766
767						var method = (init && init.method) ? init.method.toUpperCase() :
768							(input instanceof Request ? input.method.toUpperCase() : 'GET');
769						if (method !== 'POST') return;
770
771						var url = typeof input === 'string' ? input :
772							(input instanceof Request ? input.url : String(input));
773
774						if (/wc\/store\/v1\/cart\/add-item/.test(url) || /wc\/store\/v1\/cart\/update-item/.test(url)) {
775							self.fetchAndTrackAddToCart();
776						} else if (/wc\/store\/v1\/cart\/remove-item/.test(url)) {
777							self.fetchAndTrackRemoveFromCart();
778						}
779					} catch (e) {
780						// Silently ignore interceptor errors
781					}
782				}).catch(function () {
783					// Ignore - the original caller handles fetch errors
784				});
785
786				return promise;
787			};
788		},
789	};
790
791	// Initialize when DOM is ready
792	$(document).ready(function () {
793		MailchimpPixelTracking.init();
794	});
795
796})(jQuery);

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.