PageSourceSearch

https://www.edizionikarlandrosa.it/wp-content/plugins/mailchimp-fo…imp-woocommerce-pixel-tracking.js?ver=1.0.0

js edizionikarlandrosa.it collected 2026-09-24 15:05:02 UTC 15,709 bytes, 556 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	 * Mailchimp Pixel Tracking Handler
25	 */
26	const MailchimpPixelTracking = {
27		// Debounce timers for cart fetch calls
28		_atcTimer: null,
29		_rfcTimer: null,
30		_atcFetching: false,
31		_rfcFetching: false,
32
33		/**
34		 * Initialize tracking.
35		 * Waits for Pixel SDK with exponential backoff, then sends page events and attaches handlers.
36		 */
37		init: function () {
38			const self = this;
39			this.waitForPixelSDK(PIXEL_SDK_WAIT_CONFIG)
40				.then(function () {
41					self.sendPageEvents();
42					self.attachCartEventListeners();
43					self.interceptStoreApiRequests();
44					console.log('Mailchimp Pixel SDK loaded.');
45				})
46				.catch(function (e) {
47					console.log('Mailchimp Pixel SDK not loaded within timeout. Tracking disabled.', e);
48				});
49		},
50
51		/**
52		 * Wait for Pixel SDK to become available using exponential backoff.
53		 *
54		 * @param {Object} options Optional config
55		 * @param {number} options.initialDelayMs First delay in ms (default 100)
56		 * @param {number} options.maxDelayMs Cap on delay in ms (default 5000)
57		 * @param {number} options.maxAttempts Max number of attempts (default 20)
58		 * @return {Promise<void>} Resolves when SDK is available, rejects after maxAttempts
59		 */
60		waitForPixelSDK: function (options) {
61			const config = options || {};
62			const initialDelayMs = config.initialDelayMs !== undefined ? config.initialDelayMs : PIXEL_SDK_WAIT_CONFIG.initialDelayMs;
63			const maxDelayMs = config.maxDelayMs !== undefined ? config.maxDelayMs : PIXEL_SDK_WAIT_CONFIG.maxDelayMs;
64			const maxAttempts = config.maxAttempts !== undefined ? config.maxAttempts : PIXEL_SDK_WAIT_CONFIG.maxAttempts;
65
66			function isSDKReady() {
67				return typeof window.$mcSite !== 'undefined' &&
68					window.$mcSite.pixel &&
69					typeof window.$mcSite.pixel.api !== 'undefined' &&
70					typeof window.$mcSite.pixel.api.track === 'function' &&
71					window.$mcSite.pixel.installed === true;
72			}
73
74			return new Promise(function (resolve, reject) {
75				let attempt = 0;
76
77				function scheduleCheck() {
78					if (isSDKReady()) {
79						// FIXME: temporary wait to accommodate for not being able to detect ready state
80						console.warn('Pixel SDK - remediation for pixel ready issue')
81						setTimeout(function () {
82							resolve();
83						}, 1000);
84						return;
85					}
86					if (attempt >= maxAttempts) {
87						reject(new Error('Pixel SDK not available'));
88						return;
89					}
90
91					const delay = Math.min(
92						initialDelayMs * Math.pow(2, attempt),
93						maxDelayMs
94					);
95					attempt += 1;
96
97					setTimeout(scheduleCheck, delay);
98				}
99
100				scheduleCheck();
101			});
102		},
103
104		/**
105		 * Synchronous check if Pixel SDK is available (e.g. before each track call).
106		 *
107		 * @return {boolean} True if SDK is available
108		 */
109		isPixelSDKReady: function () {
110			return typeof window.$mcSite !== 'undefined' &&
111				window.$mcSite.pixel &&
112				typeof window.$mcSite.pixel.api !== 'undefined' &&
113				typeof window.$mcSite.pixel.api.track === 'function';
114		},
115
116		/**
117		 * Get cart ID from window.mcPixel
118		 *
119		 * @return {string} Cart ID
120		 */
121		getCartId: function () {
122			return window.mcPixel && window.mcPixel.cartId ? window.mcPixel.cartId : '';
123		},
124
125		/**
126		 * Get the REST API base URL from the localized config.
127		 *
128		 * @return {string} REST base URL
129		 */
130		getRestBase: function () {
131			return (window.mcPixelConfig && window.mcPixelConfig.restBase) || '/wp-json/mailchimp-for-woocommerce/v1/';
132		},
133
134		/**
135		 * Send page-level events based on pre-populated data
136		 */
137		sendPageEvents: function () {
138			if (!window.mcPixel || !window.mcPixel.data) {
139				return;
140			}
141
142			const data = window.mcPixel.data;
143			const events = data.events || [];
144
145			// Send events based on what was set by PHP
146			events.forEach((eventType) => {
147				//console.log('Mailchimp Pixel: Sending event', eventType, data);
148				switch (eventType) {
149					case 'PRODUCT_ADDED_TO_CART':
150						if (data.added_to_cart) {
151							var atcItems = Array.isArray(data.added_to_cart) ? data.added_to_cart : [data.added_to_cart];
152							for (var ai = 0; ai < atcItems.length; ai++) {
153								this.sendProductAddedToCart(atcItems[ai]);
154							}
155						}
156						break;
157					case 'PRODUCT_REMOVED_FROM_CART':
158						if (data.removed_from_cart) {
159							var rfcItems = Array.isArray(data.removed_from_cart) ? data.removed_from_cart : [data.removed_from_cart];
160							for (var ri = 0; ri < rfcItems.length; ri++) {
161								this.sendProductRemovedFromCart(rfcItems[ri]);
162							}
163						}
164						break;
165					case 'IDENTITY':
166						if (data.identity && data.identity.email) {
167							this.sendIdentityEvent(data.identity.email);
168						}
169						break;
170					case 'PRODUCT_VIEWED':
171						// Skip if an add-to-cart or remove-from-cart already fired for the same product.
172						// The cart action implies the view, so firing both is redundant.
173						if (data.product) {
174							const viewedId = String(data.product.productId || data.product.id);
175							const atcArr = Array.isArray(data.added_to_cart) ? data.added_to_cart : (data.added_to_cart ? [data.added_to_cart] : []);
176							const rfcArr = Array.isArray(data.removed_from_cart) ? data.removed_from_cart : (data.removed_from_cart ? [data.removed_from_cart] : []);
177							const atcMatch = atcArr.some(function (p) { return String(p.productId || p.id
177) === viewedId; });
178							const rfcMatch = rfcArr.some(function (p) { return String(p.productId || p.id) === viewedId; });
179
180							if (
181								(events.includes('PRODUCT_ADDED_TO_CART') && atcMatch) ||
182								(events.includes('PRODUCT_REMOVED_FROM_CART') && rfcMatch)
183							) {
184								//console.log('Mailchimp Pixel: Skipping PRODUCT_VIEWED (superseded by cart event for same product)');
185								break;
186							}
187							this.sendProductViewed(data.product);
188						}
189						break;
190					case 'CART_VIEWED':
191						if (data.cart) {
192							this.sendCartViewed(data.cart);
193						}
194						break;
195					case 'CHECKOUT_STARTED':
196						if (data.checkout) {
197							this.sendCheckoutStarted(data.checkout);
198							window.mcPixel._handled.checkout = true;
199						}
200						break;
201					case 'PURCHASED':
202						if (data.order) {
203							this.sendPurchased(data.order);
204						}
205						break;
206					case 'PRODUCT_CATEGORY_VIEWED':
207						if (data.category) {
208							this.sendCategoryViewed(data.category);
209							window.mcPixel._handled.category = true;
210						}
211						break;
212					case 'SEARCH_SUBMITTED':
213						if (data.search) {
214							this.sendSearchSubmitted(data.search);
215							window.mcPixel._handled.search = true;
216						}
217						break;
218				}
219			});
220		},
221
222		/**
223		 * Send PRODUCT_VIEWED event
224		 *
225		 * @param {Object} product Product data
226		 */
227		sendProductViewed: function (product) {
228			if (!this.isPixelSDKReady()) return;
229
230			window.$mcSite.pixel.api.track('PRODUCT_VIEWED', {
231				product: product
232			}).catch((error) => {
233				console.error('Mailchimp Pixel: Error tracking PRODUCT_VIEWED', error);
234			});
235		},
236
237		/**
238		 * Send PRODUCT_ADDED_TO_CART event
239		 *
240		 * @param {Object} product Product data
241		 */
242		sendProductAddedToCart: function (product) {
243			if (!this.isPixelSDKReady()) return;
244
245			const cartId = this.getCartId();
246			const eventData = {
247				cartId: cartId,
248				product: {
249					item: {
250						id: product.id,
251						productId: product.productId,
252						title: product.title,
253						price: product.price,
254						currency: product.currency,
255						sku: product.sku || ''
256					},
257					quantity: product.quantity || 1,
258					price: product.price * (product.quantity || 1),
259					currency: product.currency
260				}
261			};
262
263			window.$mcSite.pixel.api.track('PRODUCT_ADDED_TO_CART', eventData).catch((error) => {
264				console.error('Mailchimp Pixel: Error tracking PRODUCT_ADDED_TO_CART', error);
265			});
266		},
267
268		/**
269		 * Send PRODUCT_REMOVED_FROM_CART event
270		 *
271		 * @param {Object} product Product data
272		 */
273		sendProductRemovedFromCart: function (product) {
274			if (!this.isPixelSDKReady()) return;
275
276			const cartId = this.getCartId();
277			const eventData = {
278				cartId: cartId,
279				product: {
280					item: {
281						id: product.id,
282						productId: product.productId,
283						title: product.title,
284						price: product.price,
285						currency: product.currency,
286						sku: product.sku || ''
287					},
288					quantity: product.quantity || 1,
289					price: product.price * (product.quantity || 1),
290					currency: product.currency
291				}
292			};
293
294			window.$mcSite.pixel.api.track('PRODUCT_REMOVED_FROM_CART', eventData).catch((error) => {
295				console.error('Mailchimp Pixel: Error tracking PRODUCT_REMOVED_FROM_CART', error);
296			});
297		},
298
299		/**
300		 * Send CART_VIEWED event
301		 *
302		 * @param {Object} cart Cart data
303		 */
304		sendCartViewed: function (cart) {
305			if (!this.isPixelSDKReady()) return;
306
307			window.$mcSite.pixel.api.track('CART_VIEWED', {
308				cart: cart
309			}).catch((error) => {
310				console.error('Mailchimp Pixel: Error tracking CART_VIEWED', error);
311			});
312		},
313
314		/**
315		 * Send CHECKOUT_STARTED event
316		 *
317		 * @param {Object} checkout Checkout data
318		 */
319		sendCheckoutStarted: function (checkout) {
320			if (!this.isPixelSDKReady()) return;
321
322			window.$mcSite.pixel.api.track('CHECKOUT_STARTED', {
323				checkout: checkout
324			}).catch((error) => {
325				console.error('Mailchimp Pixel: Error tracking CHECKOUT_STARTED', error);
326			});
327		},
328
329		sendIdentityEvent: function(email) {
330			if (!this.isPixelSDKReady()) return;
331			window.$mcSite.pixel.api.identify({
332				type: 'EMAIL',
333				value: email
334			});
335		},
336
337		/**
338		 * Send PURCHASED event
339		 *
340		 * @param {Object} order Order data
341		 */
342		sendPurchased: function (order) {
343			if (!this.isPixelSDKReady()) return;
344
345			window.$mcSite.pixel.api.track('PURCHASED', {
346				order: order
347			}).catch((error) => {
348				console.error('Mailchimp Pixel: Error tracking PURCHASED', error);
349			});
350		},
351
352		/**
353		 * Send PRODUCT_CATEGORY_VIEWED event
354		 *
355		 * @param {Object} category Category data
356		 */
357		sendCategoryViewed: function (category) {
358			if (!this.isPixelSDKReady()) return;
359
360			window.$mcSite.pixel.api.track('PRODUCT_CATEGORY_VIEWED', category).catch((error) => {
361				console.error('Mailchimp Pixel: Error tracking PRODUCT_CATEGORY_VIEWED', error);
362			});
363		},
364
365		/**
366		 * Send SEARCH_SUBMITTED event
367		 *
368		 * @param {Object} search Search data
369		 */
370		sendSearchSubmitted: function (search) {
371			if (!this.isPixelSDKReady()) return;
372
373			window.$mcSite.pixel.api.track('SEARCH_SUBMITTED', search).catch((error) => {
374				console.error('Mailchimp Pixel: Error tracking SEARCH_SUBMITTED', error);
375			});
376		},
377
378		/**
379		 * Find product by ID from pre-loaded products
380		 *
381		 * @param {number|string} productId Product ID
382		 * @return {Object|null} Product data or null
383		 */
384		findProductById: function (productId) {
385			if (!window.mcPixel || !window.mcPixel.data || !window.mcPixel.data.products) {
386				return null;
387			}
388
389			const products = window.mcPixel.data.products;
390			const id = String(productId);
391
392			for (let i = 0; i < products.length; i++) {
393				if (String(products[i].id) === id) {
394					return products[i];
395				}
396			}
397
398			return null;
399		},
400
401		/**
402		 * Debounced fetch for add-to-cart events.
403		 * Each call resets the timer so rapid-fire triggers coalesce into one
404		 * fetch that drains the entire PHP queue.
405		 * If a fetch is already in flight, schedule another drain after it completes.
406		 */
407		fetchAndTrackAddToCart: function () {
408			var self = this;
409			if (!this.isPixelSDKReady()) return;
410
411			clearTimeout(this._atcTimer);
412			this._atcTimer = setTimeout(function () {
413				self._drainAddToCartQueue();
414			}, 600);
415		},
416
417		_drainAddToCartQueue: async function () {
418			if (this._atcFetching) return;
419			this._atcFetching = true;
420
421			try {
422				var res = await fetch(this.getRestBase() + 'pixel/atc', {
423					method: 'GET',
424					credentials: 'same-origin',
425					headers: { 'Accept': 'application/json' },
426				});
427
428				if (!res.ok) return;
429
430				var data = await res.json();
431				if (!data) return;
432
433				var items = Array.isArray(data) ? data : [data];
434				for (var i = 0; i < items.length; i++) {
435					this.sendProductAddedToCart(items[i]);
436				}
437			} catch (e) {
438				// no-op
439			} finally {
440				this._atcFetching = false;
441			}
442		},
443
444		/**
445		 * Debounced fetch for remove-from-cart events.
446		 * Same debounce pattern as add-to-cart.
447		 */
448		fetchAndTrackRemoveFromCart: function () {
449			var self = this;
450			if (!this.isPixelSDKReady()) return;
451
452			clearTimeout(this._rfcTimer);
453			this._rfcTimer = setTimeout(function () {
454				self._drainRemoveFromCartQueue();
455			}, 600);
456		},
457
458		_drainRemoveFromCartQueue: async function () {
459			if (this._rfcFetching) return;
460			this._rfcFetching = true;
461
462			try {
463				var res = await fetch(this.getRestBase() + 'pixel/rfc', {
464					method: 'GET',
465					credentials: 'same-origin',
466					headers: { 'Accept': 'application/json' },
467				});
468
469				if (!res.ok) return;
470
471				var data = await res.json();
472				if (!data) return;
473
474				var items = Array.isArray(data) ? data : [data];
475				for (var i = 0; i < items.length; i++) {
476					this.sendProductRemovedFromCart(items[i]);
477				}
478			} catch (e) {
479				// no-op
480			} finally {
481				this._rfcFetching = false;
482			}
483		},
484
485		/**
486		 * Attach DOM and jQuery event listeners for cart add/remove.
487		 * These serve as fallback triggers alongside the fetch interceptor.
488		 */
489		attachCartEventListeners: function () {
490			const self = this;
491
492			// Classic themes (jQuery events fired by WooCommerce's add-to-cart.js)
493			$(document.body).on('added_to_cart', function () {
494				self.fetchAndTrackAddToCart();
495			});
496			$(document.body).on('removed_from_cart', function () {
497				self.fetchAndTrackRemoveFromCart();
498			});
499
500			// WC Blocks DOM CustomEvents
501			document.body.addEventListener('wc-blocks_added_to_cart', function () {
502				self.fetchAndTrackAddToCart();
503			});
504			document.body.addEventListener('wc-blocks_removed_from_cart', function () {
505				self.fetchAndTrackRemoveFromCart();
506			});
507		},
508
509		/**
510		 * Intercept window.fetch to detect WooCommerce Store API cart mutations.
511		 *
512		 * This is the most reliable method for catching add-to-cart and remove-from-cart
513		 * in block-based WooCommerce setups, where DOM events may not fire consistently.
514		 * Watches for successful POST requests to the Store API cart endpoints.
515		 */
516		interceptStoreApiRequests: function () {
517			const self = this;
518			const originalFetch = window.fetch;
519
520			window.fetch = function (input, init) {
521				var promise = originalFetch.apply(this, arguments);
522
523				promise.then(function (response) {
524					try {
525						if (!response.ok) return;
526
527						var method = (init && init.method) ? init.method.toUpperCase() :
528							(input instanceof Request ? input.method.toUpperCase() : 'GET');
529						if (method !== 'POST') return;
530
531						var url = typeof input === 'string' ? input :
532							(input instanceof Request ? input.url : String(input));
533
534						if (/wc\/store\/v1\/cart\/add-item/.test(url) || /wc\/store\/v1\/cart\/update-item/.test(url)) {
535							self.fetchAndTrackAddToCart();
536						} else if (/wc\/store\/v1\/cart\/remove-item/.test(url)) {
537							self.fetchAndTrackRemoveFromCart();
538						}
539					} catch (e) {
540						// Silently ignore interceptor errors
541					}
542				}).catch(function () {
543					// Ignore - the original caller handles fetch errors
544				});
545
546				return promise;
547			};
548		},
549	};
550
551	// Initialize when DOM is ready
552	$(document).ready(function () {
553		MailchimpPixelTracking.init();
554	});
555
556})(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.