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.