1;(() => { 2 // Polyfill for requestIdleCallback (Safari compatibility) 3 if (typeof requestIdleCallback === 'undefined') { 4 window.requestIdleCallback = function(callback, options) { 5 const timeout = options?.timeout || 0; 6 const startTime = performance.now(); 7 8 return setTimeout(() => { 9 callback({ 10 didTimeout: false, 11 timeRemaining() { 12 return Math.max(0, 50 - (performance.now() - startTime)); 13 } 14 }); 15 }, timeout || 1); 16 }; 17 18 window.cancelIdleCallback = function(id) { 19 clearTimeout(id); 20 }; 21 } 22 23 // Cache frequently accessed values 24 const cache = { 25 currency: null, 26 locale: null, 27 rayId: null, 28 experiments: null, 29 pageData: null 30 } 31 32 // Event templates to reduce object creation overhead 33 const eventTemplates = { 34 elysium: (name, action, data = {}) => ({ 35 event: 'elysiumEvent', 36 event_name: name, 37 action, 38 eventData: { 39 eventPage: window?.location?.pathname, 40 ...data 41 } 42 }), 43 44 ecommerce: (name, ecommerceData, value = 0) => ({ 45 event: 'ecom_event', 46 event_name: name, 47 ecommerce: { 48 currencyCode: cache.currency || 'GBP', 49 value, 50 url: window.location.href, 51 ...ecommerceData 52 } 53 }), 54 55 pubsub: (operationName, eventData, rayId) => ({ 56 operationName, 57 metadata: {}, 58 path: window.location.pathname + window.location.search, 59 version: 'altitude', 60 rayId: rayId || cache.rayId || 'unknown', 61 currency: cache.currency, 62 shippingDestination: Tracker.store.get('ship')?.value, 63 locale: cache.locale 64 }) 65 } 66 67 // Batch DOM operations 68 const batchDOMOperations = (() => { 69 let operations = [] 70 let scheduled = false 71 72 const flush = () => { 73 operations.forEach(op => op()) 74 operations = [] 75 scheduled = false 76 } 77 78 return (operation) => { 79 operations.push(operation) 80 if (!scheduled) { 81 scheduled = true 82 requestAnimationFrame(flush) 83 } 84 } 85 })() 86 87 // Optimized event binding with delegation 88 const optimizedEventBind = () => { 89 const trackSelector = Tracker.config.selectors.track 90 const typeSelector = Tracker.config.selectors.type 91 92 // Use event delegation for better performance 93 document.addEventListener('click', (e) => { 94 const target = e.target.closest(`[${trackSelector}]`) 95 if (!target) return 96 97 const handler = target.getAttribute(trackSelector) 98 const eventTypes = target.getAttribute(typeSelector)?.split('|') || ['click'] 99 100 if (eventTypes.includes('click') && Tracker.handlers[handler]) { 101 Tracker.handlers[handler](target) 102 } 103 }, { passive: true }) 104 105 // Handle impression and onload events 106 batchDOMOperations(() => { 107 const elements = document.querySelectorAll(`[${trackSelector}]`) 108 const observer = new IntersectionObserver((entries) => { 109 entries.forEach((entry) => { 110 if (entry.isIntersecting) { 111 const handler = entry.target.getAttribute(trackSelector) 112 const eventTypes = entry.target.getAttribute(typeSelector)?.split('|') || [] 113 114 if (eventTypes.includes('impression') && Tracker.handlers[handler]) { 115 Tracker.handlers[handler](entry.target) 116 observer.unobserve(entry.target) 117 } 118 } 119 }) 120 }, { threshold: 0.1 }) 121 122 elements.forEach(el => { 123 const eventTypes = el.getAttribute(typeSelector)?.split('|') || ['click'] 124 const handler = el.getAttribute(trackSelector) 125 126 if (eventTypes.includes('impression') && Tracker.handlers[handler]) { 127 observer.observe(el) 128 } else if (eventTypes.includes('onload') && Tracker.handlers[handler]) { 129 Tracker.handlers[handler](el) 130 } 131 }) 132 }) 133 } 134 135 // Optimized cache refresh 136 const refreshCache = () => { 137 cache.currency = Tracker.store.get('currency') || 'GBP' 138 cache.locale = Tracker.store.get('locale')?.value 139 cache.rayId = Tracker.store.get('rayId') 140 141 // Cache experiments parsing 142 if (window.__EXPERIMENTS__ && !cache.experiments) { 143 cache.experiments = [] 144 const chunks = window.__EXPERIMENTS__.split(',') 145 chunks.forEach((pair) => { 146 const [name, value] = pair.split(':') 147 if (name && value) { 148 cache.experiments.push({ name, value }) 149 } 150 }) 151 } 152 } 153 154 // Generalized product data handler 155 const getProductData = (identifier) => { 156 const products = Tracker.store.get('products') 157 if (!products) return null 158 159 if (Array.isArray(products)) { 160 return products[identifier] || products.find(p => p?.item_id === identifier) 161 } else if (typeof products === 'object') { 162 return products[identifier] 163 } 164 165 // Fallback: if identifier is an object, use it directly 166 return typeof identifier === 'object' && identifier !== null ? identifier : null 167 } 168 169 // Optimized event creation factory 170 const createProductEvent = (eventName, productData, additionalData = {}) => { 171 if (!productData) { 172 console.warn('[Tracker]: createProductEvent called with null/undefined productData') 173 return null 174 } 175 176 // For adding item bundles to cart 177 if (Array.isArray(productData) && productData.length > 0) { 178 // Item price is single-unit; event value sums unit price x quantity. 179 const totalValue = productData.reduce((sum, item) => { 180 const unitPrice = parseFloat(item.value ?? item.price ?? 0) 181 return sum + unitPrice * parseInt(item.quantity || 1) 182 }, 0) 183 184 const itemList = productData.map((item) => ({ 185 item_id: item.item_id || item.sku || '', 186 item_name: item.item_name || item.title || '', 187 item_brand: item.item_brand || item.brand?.name || '', 188 price: parseFloat(item.value ?? item.price ?? 0), 189 index: item.index, 190 discount: item.discount, 191 affiliation: item.affiliation, 192 coupon: item.coupon, 193 item_category: item.item_category || item.categoryLevel1 || '', 194 item_list_name: item.item_list_name || 'Product List', 195 item_variant: item.variant || item.sku, 196 quantity: item.quantity || 1 197 })) 198 199 return { 200 ...eventTemplates.ecommerce(eventName, { 201 items: itemList, 202 ...additionalData 203 }, totalValue) 204 } 205 } 206 // Ensure we have at least basic product data 207 if (!productData.item_id && !productData.sku) { 208 console.warn('[Tracker]: createProductEvent called without item_id or sku') 209 return null 210 } 211 212 // Item price is single-unit; event value is unit price x quantity. 213 const unitPrice = parseFloat(productData.value ?? productData.price ?? 0) 214 const quantity = parseInt(productData.quantity || 1) 215 216 return { 217 ...eventTemplates.ecommerce(eventName, { 218 items: [{ 219 item_id: productData.item_id || productData.sku || '', 220 item_name: productData.item_name || productData.title || '', 221 item_brand: productData.item_brand || productData.brand?.name || '', 222 price: unitPrice, 223 index: productData.index, 224 discount: productData.discount, 225 affiliation: productData.affiliation, 226 coupon: productData.coupon, 227 item_category: productData.item_category || productData.categoryLevel1 || '', 228 item_list_name: productData.item_list_name || 'Product List', 229 item_variant: productData.variant || productData.sku, 230 quantity: quantity 231 }], 232 ...additionalData 233 }, unitPrice * quantity) 234 } 235 } 236 237 const Tracker = { 238 init: (config) => { 239 Tracker.config = config 240 Tracker.log(`Initialised`, 'log') 241 Tracker.nonce = crypto.randomUUID() 242 243 // Defer setup to after initial render 244 if (document.readyState === 'loading') { 245 window.addEventListener('load', () => requestIdleCallback(() => Tracker.setup()), { once: true }) 246 } else { 247 requestIdleCallback(() => Tracker.setup()) 248 } 249 }, 250 251 refresh: () => requestIdleCallback(() => Tracker.setup()), 252 253 setup: () => { 254 // Batch all setup operations 255 Tracker.assignTracker() 256 refreshCache() 257 Tracker.getPageData() 258 optimizedEventBind() 259 Tracker.bindCheckoutButton() 260 Tracker.bindNewsletterSignupButton() 261 Tracker.urlEvent() 262 Tracker.handlers.pageVisit() 263 }, 264 265 assignTracker: () => { 266 window.Tracker = Object.assign(Tracker, window.Tracker) 267 }, 268 269 urlEvent: () => { 270 if (!Tracker.config.watchers) return 271
272 const keys = Object.keys(Tracker.config.watchers.params || {}) 273 const searchParams = new URLSearchParams(window.location.search) 274 275 keys.forEach((key) => { 276 const param = Tracker.config.watchers.params[key] 277 if (searchParams.get(param.paramName) && Tracker.handlers[param.eventName]) { 278 Tracker.handlers[param.eventName]() 279 const url = new URL(window.location.href) 280 url.searchParams.delete(param.paramName) 281 history.replaceState(history.state, '', url.href) 282 } 283 }) 284 }, 285 286 // Optimized button binding methods 287 bindCheckoutButton: () => { 288 batchDOMOperations(() => { 289 const checkoutButton = document.querySelector('[data-testid="checkout-button"]') 290 if (checkoutButton) { 291 checkoutButton.addEventListener('click', Tracker.handlers.checkoutButtonClick, { passive: true }) 292 } 293 }) 294 }, 295 296 bindNewsletterSignupButton: () => { 297 batchDOMOperations(() => { 298 const newsletterButton = document.querySelector('newsletter-signup-form > button') 299 if (newsletterButton) { 300 newsletterButton.addEventListener('click', Tracker.handlers.newsletterSignupComplete, { passive: true }) 301 } 302 }) 303 }, 304 305 eventBind: optimizedEventBind, 306 307 getPageData: () => { 308 const currencyEl = document.querySelector(`[${Tracker.config.selectors.currency}]`) 309 Tracker.page = {} 310 311 if (currencyEl) { 312 const newCurrency = currencyEl.getAttribute(Tracker.config.selectors.currency) 313 if (cache.currency !== newCurrency) { 314 Tracker.store.set('currency', newCurrency) 315 cache.currency = newCurrency 316 } 317 } else { 318 Tracker.log(`Required attribute ${Tracker.config.selectors.currency} not found.`) 319 } 320 }, 321 322 getAttributes: (el) => { 323 const target = el.currentTarget || el 324 return { 325 store: target.getAttribute(Tracker.config.selectors.store), 326 type: target.getAttribute(Tracker.config.selectors.type), 327 track: target.getAttribute(Tracker.config.selectors.track) 328 } 329 }, 330 331 push: (obj, config) => { 332 Tracker.log(`Pushed event: ${JSON.stringify(obj)}`, 'log') 333 if (window.dataLayer) { 334 if (config?.commerce) { 335 window.dataLayer.push({ commerce: true }) 336 } 337 window.dataLayer.push(obj) 338 } 339 }, 340 341 marshalPubSub: (operationName, url, eventData, legacyRayId, experiments) => { 342 const rayId = legacyRayId || cache.rayId || 'unknown' 343 const { origin: eventOrigin, ...restEventData } = eventData 344 345 const legacyData = { 346 ...restEventData, 347 ray_ids: [rayId], 348 nonce: Tracker.nonce, 349 server: { elysium_version: Tracker.trackApiConfig.elysium_config.version }, 350 request: { start_timestamp: Date.now(), url }, 351 origin: { referrer: document.referrer, ...(eventOrigin || {}) }, 352 property: { 353 site_id: Tracker.trackApiConfig.elysium_config.site_id, 354 channel: Tracker.trackApiConfig.elysium_config.channel, 355 subsite: Tracker.trackApiConfig.elysium_config.subsite, 356 locale: cache.locale 357 }, 358 experiments: experiments || cache.experiments || [] 359 } 360 361 const basket = Tracker.store.get('basket') 362 if (basket?.items?.length) { 363 legacyData.cart = { 364 items: basket.items.map((item) => ({ 365 product_group: { 366 id: parseInt(item.id), 367 selected_variant: { 368 price: { 369 currency: cache.currency, 370 value: parseFloat(item.product.price?.price?.amount) 371 }, 372 sku: parseInt(item.product.product?.sku) 373 }, 374 total_variants: item.product.product?.variants?.length || null 375 }, 376 quantity: parseInt(item.quantity) 377 })), 378 total_size: basket.totalQuantity || 0, 379 total_price: { 380 currency: cache.currency, 381 value: parseFloat(basket.chargePrice?.amount) 382 } 383 } 384 } 385 386 const payload = { 387 ...eventTemplates.pubsub(operationName, eventData, rayId), 388 legacy: legacyData 389 } 390 391 return payload 392 }, 393 394 pushToTrackAPI: async (payload) => { 395 const trackUrl = Tracker.trackApiConfig?.trackAPIUrl 396 if (!trackUrl || Tracker.config?.dev) return 397 398 const dataBlob = new Blob([JSON.stringify(payload)], { type: 'application/json' }) 399 navigator.sendBeacon(trackUrl, dataBlob) 400 401 if (window.debugger) { 402 window.debugger.push({ 403 message: payload?.legacy?.event?.type, 404 snippet: JSON.stringify(payload), 405 type: 'event' 406 }) 407 } 408 }, 409 410 sendPubSubEvent: async (operationName, eventData, legacyRayId = null) => { 411 const payload = Tracker.marshalPubSub( 412 operationName, 413 window.location.pathname + window.location.search, 414 eventData, 415 legacyRayId, 416 cache.experiments 417 ) 418 return Tracker.pushToTrackAPI(payload) 419 }, 420 421 log: (message, type = 'warn') => { 422 if (type === 'warn') console.warn('[Tracker]: %s', message) 423 if (type === 'log') console.debug('[Tracker]: %s', message) 424 }, 425 426 translateTarget: (e) => { 427 if (e instanceof Event) { 428 return e.currentTarget.getAttribute(Tracker.config.selectors?.store) 429 } else if (e instanceof HTMLElement) { 430 return e.getAttribute(Tracker.config.selectors?.store) 431 } else if (e?.store) { 432 return e.store 433 } 434 return e 435 }, 436 437 handlers: { 438 // Optimized handlers using templates and caching 439 pageLoad: (e) => { 440 const pageInfo = window?.Tracker?.store?.pageInfo 441 if (pageInfo && window.location.pathname.includes('/p/')) { 442 Tracker.push({ ...pageInfo, pageVisitId: crypto.randomUUID() }) 443 } else { 444 Tracker.push(pageInfo) 445 } 446 }, 447 448 pageVisit: () => { 449 Tracker.sendPubSubEvent('PageVisit', { event: { type: 'page_visit' } }, cache.rayId) 450 }, 451 452 userDetails: (data) => { 453 const isLoggedIn = data.visitorLoginState === 'LOGGED_IN'; 454 const hasCustomer = data.visitorId && data.visitorId !== ''; 455 const isRegisterMethod = data.visitorLoginMethod === 'Register'; 456 457 let visitorType = ''; 458 let visitorExistingCustomer = 'unknown'; 459 let visitorRecognisedState = ''; 460 461 if (hasCustomer) { 462 if (isRegisterMethod) { 463 visitorType = 'New'; 464 visitorExistingCustomer = 'false'; 465 } else { 466 visitorType = 'Existing'; 467 visitorExistingCustomer = 'true'; 468 } 469 } 470 471 Tracker.push({ 472 event: 'userDetails', 473 page_title: data.page_title, 474 page_location: data.page_location, 475 visitorId: data.visitorId ? String(data.visitorId) : '', 476 visitorLoginState: isLoggedIn ? 'loggedin' : 'loggedout', 477 visitorLoginMethod: data.visitorLoginMethod, 478 visitorExistingCustomer: visitorExistingCustomer, 479 visitorType: visitorType, 480 visitorEmailAddress: data.visitorEmailAddress, 481 visitorRecognisedState: isLoggedIn ? 'LOGGED_IN' : 'NOT_RECOGNISED', 482 visitorAffiliate: '' 483 }) 484 }, 485 486 // Cookie handlers using template 487 cookieModalOpen: () => { 488 Tracker.push(eventTemplates.elysium('cookie_modal_shown', 'shown', {
489 eventCategory: 'Cookie Modal', 490 eventAction: 'Shown', 491 eventLabel: 'Accept Cookie Button' 492 })) 493 }, 494 495 cookieModalAccepted: () => { 496 Tracker.push(eventTemplates.elysium('cookie_modal_clicked', 'accepted all', { 497 eventCategory: 'Cookie Modal', 498 eventAction: 'Accept All', 499 eventLabel: 'Accept Cookie Button' 500 })) 501 }, 502 503 cookieModalRejected: () => { 504 Tracker.push(eventTemplates.elysium('cookie_modal_clicked', 'reject all', { 505 eventCategory: 'Cookie Modal', 506 eventAction: 'Reject All', 507 eventLabel: 'Reject Cookie Button' 508 })) 509 }, 510 511 cookieSet: (el) => { 512 Tracker.push(eventTemplates.elysium('cookie_modal_clicked', `set preference to be ${el}`, { 513 eventCategory: 'Cookie Modal', 514 eventAction: 'Set preference', 515 eventLabel: 'Set Preference Button', 516 eventValue: el 517 })) 518 }, 519 520 viewItemList: () => { 521 try { 522 const list = window.Tracker.store.get('productList') 523 if (!list || !list.items || list.items.length === 0) { 524 Tracker.log('No product list data found for viewItemList', 'warn') 525 return 526 } 527 528 Tracker.push({ 529 event: 'view_item_list', 530 currencyCode: list?.currency || cache.currency || 'GBP', 531 ecommerce: { 532 item_list_id: list?.id ?? '', 533 item_list_name: list?.title ?? '', 534 url: list?.url ?? window.location.href, 535 items: list?.items || [] 536 }, 537 platformType: 'Altitude' 538 }) 539 } catch (error) { 540 Tracker.log(`Error in viewItemList handler: ${error.message}`, 'warn') 541 } 542 }, 543 544 // Optimized product handlers 545 addToCart: (prodInfo) => { 546 try { 547 // Handle both direct data objects and store lookups 548 let data = null; 549 550 if ((typeof prodInfo === 'object' && prodInfo.item_id) || (Array.isArray(prodInfo) && prodInfo.length > 0)) { 551 552 // Direct data object passed from component 553 data = prodInfo; 554 } else { 555 // Try to get from store (legacy behavior) 556 data = getProductData(prodInfo); 557 } 558 559 if (!data) { 560 Tracker.log(`No product data found for: ${JSON.stringify(e)}`, 'warn') 561 return 562 } 563 564 const elyEvent = eventTemplates.elysium('', '', { 565 eventCategory: 'Product | AddToBasket', 566 eventAction: 'success', 567 eventLabel: data.item_id || data.map((item) => item.item_id).join(','), 568 eventLabelValue: crypto.randomUUID() 569 }) 570 571 const ecommerceEvent = createProductEvent('add_to_cart', data, { 572 basketItems: Tracker.store.get('basket')?.items?.map((item) => ({ 573 sku: item.product.sku, 574 quantity: item.quantity, 575 vipPrice: item.product.vipPrice, 576 price: item.product.price 577 })) 578 }) 579 580 if (ecommerceEvent) { 581 Tracker.push(ecommerceEvent, { commerce: true }) 582 Tracker.push(elyEvent) 583 584 Tracker.sendPubSubEvent('AddToCart', { 585 event: { 586 type: 'cart_interaction_event', 587 subtype: 'initial_add', 588 items: [{ 589 product_group: { 590 id: parseInt(data.item_master) || null, 591 selected_variant: { 592 price: { 593 currency: data.currency || cache.currency, 594 value: parseFloat(data.price || data.value || 0) * parseInt(data.quantity || 1) 595 }, 596 sku: parseInt(data.item_id) 597 } 598 }, 599 quantity: parseInt(data.quantity || 1) 600 }] 601 } 602 }, data.rayId || null) 603 } 604 } catch (error) { 605 Tracker.log(`Error in addToCart handler: ${error.message}`, 'warn') 606 } 607 }, 608 609 wishlistAdded: (el) => { 610 try { 611 const item = getProductData(el) 612 if (!item) { 613 Tracker.log(`No product data found for wishlist item: ${el}`, 'warn') 614 return 615 } 616 617 const elyEvent = eventTemplates.elysium('add_to_wishlist', '', { 618 eventCategory: 'Wishlist Engagement', 619 eventAction: 'added to wishlist', 620 eventLabel: item.item_id 621 }) 622 elyEvent.product_id = item.item_id 623 elyEvent.product_name = item.item_name 624 625 const gaEvent = createProductEvent('add_to_wishlist', item, { 626 currency: item.currency, 627 page_type: window?.location?.pathname, 628 item_list_name: item.item_list_name 629 }) 630 631 // Override items array to match v7 structure 632 if (gaEvent?.ecommerce?.items?.[0]) { 633 gaEvent.ecommerce.items[0] = { 634 item_id: item?.item_id, 635 item_name: item?.item_name, 636 item_brand: item?.item_brand, 637 price: item?.price, 638 discount: item?.discount, 639 affiliation: item?.affiliation, 640 coupon: item?.coupon,
641 item_category: item?.categoryLevel1 || item?.item_category, 642 item_list_name: item?.item_list_name, 643 item_variant: item?.variant, 644 quantity: 1 645 } 646 } 647 648 Tracker.push(elyEvent) 649 Tracker.push(gaEvent) 650 651 Tracker.sendPubSubEvent('AddToWishlist', { 652 event: { 653 type: 'wishlist_interaction_event', 654 subtype: 'initial_add', 655 items: [{ product_group: { id: parseInt(el) }, quantity: 1 }] 656 } 657 }) 658 } catch (error) { 659 Tracker.log(`Error in wishlistAdded handler: ${error.message}`, 'warn') 660 } 661 }, 662 663 // Simplified handlers for common patterns 664 filterApplied: (e) => { 665 Tracker.push(eventTemplates.elysium('filter_applied', '', { 666 eventCategory: 'responsiveFacets', 667 eventAction: 'add', 668 eventLabel: e.optionName, 669 filter: e.facetName 670 })) 671 }, 672 673 filterRemoved: (e) => { 674 Tracker.push(eventTemplates.elysium('filter_removed', '', { 675 eventCategory: 'responsiveFacets', 676 eventAction: 'Removes', 677 eventLabel: e.optionName, 678 filter: e.facetName 679 })) 680 }, 681 682 // Performance data handler (deferred) 683 performanceData: (CWVObject, perfDataObject) => { 684 requestIdleCallback(() => { 685 const perfData = { 686 request: { 687 server_timestamp: null, 688 client_timestamp: new Date(Date.now()).toISOString(), 689 url: window.location.href 690 }, 691 experiments: cache.experiments || [], 692 errors: [{ type: null, label: null }], 693 timing: { 694 backend_load_time: perfDataObject?.navigationTiming?.backendLoadTime, 695 cache_time: perfDataObject?.navigationTiming?.cacheTime, 696 connection_time: perfDataObject?.navigationTiming?.connectionTime, 697 dns_time: perfDataObject?.navigationTiming?.dnsTime, 698 dom_interactive_time: perfDataObject?.navigationTiming?.domInteractiveTime, 699 dom_parsing_time: perfDataObject?.navigationTiming?.domParsingTime, 700 dom_ready_time: perfDataObject?.navigationTiming?.domReadyTime, 701 first_paint_time: perfDataObject?.navigationTiming?.firstPaintTime, 702 frontend_load_time: perfDataObject?.navigationTiming?.frontendLoadTime, 703 load_event_time: perfDataObject?.navigationTiming?.loadEventTime, 704 navigation_time: perfDataObject?.navigationTiming?.navigationTime, 705 redirect_time: perfDataObject?.navigationTiming?.redirectTime, 706 first_contentful_paint_time: CWVObject?.fcp || null, 707 first_input_delay: CWVObject?.fid || null, 708 largest_contentful_paint_time: CWVObject?.lcp || null, 709 first_byte_time: CWVObject?.ttfb || null 710 }, 711 score: { cumulative_layout_shift: CWVObject?.cls || null }, 712 nonce: Tracker.nonce, 713 device: perfDataObject?.device, 714 page: perfDataObject?.page, 715 network: perfDataObject?.network, 716 abtasty: [{ 717 name: null, 718 time_to_first_byte: null, 719 transfer: null, 720 total: null, 721 encoded_file_size: null 722 }] 723 } 724 725 Tracker.pushToTrackAPI({ 726 operationName: 'perfData', 727 metaData: { destination: 'performanceData' }, 728 eventData: perfData 729 }) 730 }) 731 }, 732 733 // Add remaining handlers with similar optimizations... 734 // (For brevity, I'm including key handlers. The pattern can be applied to all) 735 736 enhancedPageLoad: () => { 737 requestIdleCallback(() => { 738 const searchParams = new URLSearchParams(window.location.search) 739 const searchTerm = searchParams.get('search') || searchParams.get('q') 740 741 const enhancedPageData = { 742 event: 'enhancedPageLoad', 743 pageLocation: window.location.href, 744 pageReferrer: document.referrer, 745 timestamp: new Date().toISOString(), 746 userAgent: navigator.userAgent, 747 screenResolution: `${screen.width}x${screen.height}`, 748 viewportSize: `${window.innerWidth}x${window.innerHeight}`, 749 connectionType: navigator.connection?.effectiveType || 'unknown', 750 language: navigator.language, 751 timezone: Intl.DateTimeFormat().resolvedOptions().timeZone 752 } 753 754 if (searchTerm) { 755 enhancedPageData.siteSearchTerm = searchTerm 756 enhancedPageData.siteSearchResults = document.querySelectorAll('[data-product-item]').length || 0 757 } 758 759 const productSku = document.querySelector('[data-product-sku]')?.getAttribute('data-product-sku') 760 if (productSku) { 761 enhancedPageData.productSKU = productSku 762 enhancedPageData.productStatus = document.querySelector('[data-product-status]')?.getAttribute('data-product-status') || 'Available' 763 } 764 765 Tracker.push(enhancedPageData) 766 }) 767 }, 768 769 // Product and page handlers 770 // Pushes only the GA4 view_item ecom_event (no ProductVisit Pub/Sub). 771 viewItem: (e) => { 772 const store = Tracker.translateTarget(e) 773 const pageData = Tracker.store.get(store) 774 const productData = Tracker.store.get('products')[pageData.sku] 775 const ecommerceEvent = createProductEvent('view_item', productData) 776 if (ecommerceEvent) Tracker.push(ecommerceEvent) 777 }, 778 productVisit: (e) => { 779 const store = Tracker.translateTarget(e) 780 const pageData = Tracker.store.get(store) 781 let productData = Tracker.store.get('products')[pageData.sku] 782 783 const currency = Tracker.store.get('currency')?.toString() || 'GBP' 784 const ecommerceEvent = createProductEvent('view_item', productData) 785 786 if (ecommerceEvent) { 787 Tracker.push(ecommerceEvent) 788 // product_group.id stays the master product; selected_variant.sku is the viewed variant. 789 const masterId = parseInt(pageData.masterSku ?? productData?.item_id) 790 Tracker.sendPubSubEvent('ProductVisit', { 791 event: { type: 'product_visit' }, 792 page: { 793 items: [{ 794 product_group: { 795 id: masterId, 796 selected_variant: { 797 price: { 798 currency: currency, 799 value: parseFloat(productData?.price) 800 }, 801 sku: parseInt(productData?.item_id) 802 } 803 } 804 }] 805 } 806 }, pageData.rayId) 807 } 808 }, 809 810 home: (e) => { 811 const store = Tracker.translateTarget(e) 812 const pageData = Tracker.store.get(store) 813 Tracker.sendPubSubEvent('Homepage', { event: { type: 'homepage' } }, pageData.rayId) 814 }, 815 816 plp: (e) => { 817 const store = Tracker.translateTarget(e) 818 const pageData = Tracker.store.get(store) 819 Tracker.sendPubSubEvent('ProductList', { 820 event: { type: 'product_list_visit' }, 821 page: { 822 items: pageData.products?.map((product) => ({ 823 product_group: { 824 selected_variant: { 825 price: { 826 currency: product?.defaultVariant?.price?.price?.currency || product?.cheapestVariant?.price?.price?.currency, 827 value: parseFloat(product?.defaultVariant?.price?.price?.amount || product?.cheapestVariant?.price?.price?.amount) 828 }, 829 sku: parseInt(product?.sku) 830 }, 831 total_variants: product?.variants?.length 832 } 833 })) 834 } 835 }, pageData.rayId) 836 }, 837 838 search: (e) => { 839 try { 840 const store = Tracker.translateTarget(e) 841 const pageData = Tracker.store.get(store) || {} 842 843 // Get search data from URL if pageData is empty 844 const searchParams = new URLSearchParams(window.location.search) 845 const searchQuery = pageData.query || searchParams.get('search') || searchParams.get('q') || '' 846 847 // Get product list data from tracker store 848 const productListData = Tracker.store.get('productList') || {} 849 const products = productListData.items || [] 850 851 Tracker.sendPubSubEvent('Search', { 852 event: { type: 'search' }, 853 page: { 854 search: { 855 query: searchQuery, 856 total_results: pageData.total || products.length || 0 857 }, 858 items: products.map((product) => ({ 859 product_group: { 860 selected_variant: { 861 price: { 862 currency: cache.currency || 'GBP', 863 value: parseFloat(product?.price || 0) 864 }, 865 sku: parseInt(product?.item_id || 0) 866 }, 867 total_variants: 1 // Default since we don't have variant count in list 868 } 869 })) 870 } 871 }, pageData.rayId || cache.rayId) 872 } catch (error) { 873 Tracker.log(`Error in search handler: ${error.message}`, 'warn') 874 } 875 }, 876 877 // Basket handlers 878 viewBasket: () => { 879 const basket = Tracker.store.get('viewBasket') 880 const currencyCode = basket?.currency || cache.currency || 'GBP' 881 const ecommerce = { 882 currencyCode, 883 value: basket?.value ?? 0, 884 items: basket?.items || [] 885 } 886
887 // GA4 ecom_event carrying the view_cart ecommerce payload. 888 const ecommerceEvent = eventTemplates.ecommerce('view_cart', ecommerce, ecommerce.value) 889 ecommerceEvent.ecommerce.currencyCode = currencyCode 890 Tracker.push(ecommerceEvent, { commerce: true }) 891 892 // Legacy v7 view_cart event; carries the same ecommerce payload for GTM triggers keyed on it. 893 const elyEvent = eventTemplates.elysium('view_cart', '', { 894 eventCategory: 'Navigation Header', 895 eventAction: 'Clicked View Basket' 896 }) 897 elyEvent.event = 'view_cart' 898 elyEvent.currencyCode = currencyCode 899 elyEvent.ecommerce = ecommerce 900 elyEvent.platformType = 'Altitude' 901 Tracker.push(elyEvent) 902 }, 903 904 removeFromCart: (e) => { 905 try { 906 let data = null; 907 908 // Try to get data from basket store first 909 const basketData = Tracker.store.get('basket'); 910 if (basketData && basketData[e]) { 911 data = basketData[e]; 912 } else if (typeof e === 'object' && e.item_id) { 913 // Handle direct data object 914 data = e; 915 } 916 917 if (!data) { 918 Tracker.log(`No product data found for remove from cart: ${JSON.stringify(e)}`, 'warn') 919 return 920 } 921 922 const ecommerceEvent = createProductEvent('remove_from_cart', data, { 923 basketItems: Tracker.store.get('basket')?.items?.map((item) => ({ 924 sku: item.product.sku, 925 quantity: item.quantity, 926 vipPrice: item.product.vipPrice, 927 price: item.product.price 928 })) 929 }) 930 931 // Override items array to match v7 structure 932 if (ecommerceEvent?.ecommerce?.items?.[0]) { 933 // Item price is single-unit; event value is unit price x quantity. 934 const removedUnitPrice = parseFloat(data?.value ?? data?.price ?? 0) 935 const removedQuantity = parseInt(data.quantity ?? 0) 936 ecommerceEvent.ecommerce.items[0] = { 937 item_id: data.item_id ?? '', 938 item_name: data.item_name ?? '', 939 item_brand: data.item_brand ?? '', 940 price: removedUnitPrice, 941 index: data.index, 942 discount: data?.discount, 943 affiliation: data?.affiliation, 944 coupon: data.coupon, 945 item_category: data?.categoryLevel1 || data?.item_category, 946 item_list_name: data?.item_list_name, 947 item_variant: data?.variant, 948 quantity: data.quantity ?? 0 949 } 950 ecommerceEvent.ecommerce.currencyCode = data?.currency ?? cache.currency 951 ecommerceEvent.ecommerce.value = removedUnitPrice * removedQuantity 952 } 953 954 const elyEvent = eventTemplates.elysium('', '', { 955 eventCategory: 'Product | Remove From Basket', 956 eventAction: 'success', 957 eventLabel: data?.item_id, 958 eventLabelValue: crypto.randomUUID() 959 }) 960 961 if (ecommerceEvent) { 962 Tracker.push(ecommerceEvent, { commerce: true }) 963 Tracker.push(elyEvent) 964 965 Tracker.sendPubSubEvent('RemoveFromCart', { 966 event: { 967 type: 'cart_interaction_event', 968 subtype: 'trash', 969 items: [{ 970 product_group: { 971 id: parseInt(data?.item_master) || null, 972 selected_variant: { 973 price: { 974 currency: data.currency || cache.currency, 975 value: parseFloat(data?.price || data?.value || 0) * parseInt(data?.quantity || 1) 976 }, 977 sku: parseInt(data?.item_id) 978 } 979 }, 980 quantity: parseInt(data?.quantity || 1) 981 }] 982 } 983 }) 984 } 985 } catch (error) { 986 Tracker.log(`Error in removeFromCart handler: ${error.message}`, 'warn') 987 } 988 }, 989 990 updateCart: (e) => { 991 try { 992 // Handle the data structure passed from UpdateQuantityModal 993 let basketData = null; 994 995 if (e?.store) { 996 // Data passed as { store: basketData } 997 basketData = e.store; 998 } else { 999 // Legacy behavior - try to get from store 1000 const store = Tracker.translateTarget(e); 1001 basketData = Tracker.store.get(store); 1002 } 1003 1004 if (!basketData) { 1005 Tracker.log('No basket data found for updateCart', 'warn'); 1006 return; 1007 } 1008 1009 // Extract items from basket data 1010 const items = basketData.items || []; 1011 if (!items.length) { 1012 Tracker.log('No items found in basket for updateCart', 'warn'); 1013 return; 1014 } 1015 1016 // Create GA4 ecommerce event for dataLayer 1017 const ecommerceEvent = eventTemplates.ecommerce('update_cart', { 1018 currency: cache.currency || 'GBP', 1019 value: parseFloat(basketData.chargePrice?.amount || 0), 1020 items: items.map(item => ({ 1021 item_id: item?.product?.sku || '', 1022 item_name: item?.product?.title || '', 1023 item_brand: item?.product?.product?.brand?.name || '', 1024 price: parseFloat(item?.product?.price?.price?.amount || 0), 1025 quantity: parseInt(item?.quantity || 1), 1026 currency: cache.currency || 'GBP' 1027 })) 1028 }, parseFloat(basketData.chargePrice?.amount || 0)); 1029 1030 // Push to dataLayer 1031 if (ecommerceEvent) { 1032 Tracker.push(ecommerceEvent, { commerce: true }); 1033 } 1034 1035 // Create Elysium event 1036 const elyEvent = eventTemplates.elysium('update_cart', 'quantity_change', { 1037 eventCategory: 'Basket', 1038 eventAction: 'Update Quantity', 1039 eventLabel: 'Cart Updated' 1040 }); 1041 Tracker.push(elyEvent); 1042 1043 // Create items array for PubSub event 1044 const pubSubItems = items.map(item => ({ 1045 product_group: { 1046 id: parseInt(item?.product?.product?.sku || 0) || null, 1047 selected_variant: { 1048 price: { 1049 currency: cache.currency || 'GBP', 1050 value: parseFloat(item?.product?.price?.price?.amount || 0) * parseInt(item?.quantity || 1) 1051 }, 1052 sku: parseInt(item?.product?.sku || 0) 1053 } 1054 }, 1055 quantity: parseInt(item?.quantity || 1) 1056 })); 1057
1058 Tracker.sendPubSubEvent('UpdateCart', { 1059 event: { 1060 type: 'cart_interaction_event', 1061 subtype: 'quantity_change', 1062 items: pubSubItems 1063 } 1064 }); 1065 1066 Tracker.log('UpdateCart event sent successfully', 'log'); 1067 } catch (error) { 1068 Tracker.log(`Error in updateCart handler: ${error.message}`, 'warn'); 1069 } 1070 }, 1071 1072 quickBuyClick: (e) => { 1073 try { 1074 // Handle both direct data objects and store lookups 1075 let data = null; 1076 1077 if (typeof e === 'object' && e.item_id) { 1078 // Direct data object passed from component 1079 data = e; 1080 } else { 1081 // Try to get from store (legacy behavior) 1082 data = getProductData(e); 1083 } 1084 1085 if (!data) { 1086 Tracker.log(`No product data found for quick buy: ${JSON.stringify(e)}`, 'warn') 1087 return 1088 } 1089 1090 const elyEvent = eventTemplates.elysium('', '', { 1091 eventCategory: 'Product | Quick Buy', 1092 eventAction: 'click', 1093 eventLabel: data?.item_id, 1094 eventLabelValue: crypto.randomUUID() 1095 }) 1096 1097 const ecommerceEvent = createProductEvent('select_item', data, { 1098 basketItems: Tracker.store.get('basket')?.items?.map((item) => ({ 1099 sku: item.product.sku, 1100 quantity: item.quantity, 1101 vipPrice: item.product.vipPrice, 1102 price: item.product.price 1103 })) 1104 }) 1105 1106 if (ecommerceEvent) { 1107 Tracker.push(ecommerceEvent, { commerce: true }) 1108 Tracker.push(elyEvent) 1109 1110 Tracker.sendPubSubEvent('QuickBuyClick', { 1111 event: { 1112 type: 'product_interaction_event', 1113 subtype: 'quick_buy_click', 1114 items: [{ 1115 product_group: { 1116 selected_variant: { 1117 price: { 1118 currency: data.currency || cache.currency, 1119 value: parseFloat(data?.price || data?.value || 0) * parseInt(data?.quantity || 1) 1120 }, 1121 sku: parseInt(data?.item_id) 1122 } 1123 }, 1124 quantity: parseInt(data?.quantity || 1) 1125 }] 1126 } 1127 }) 1128 } 1129 } catch (error) { 1130 Tracker.log(`Error in quickBuyClick handler: ${error.message}`, 'warn') 1131 } 1132 }, 1133 1134 // Wishlist handlers 1135 wishlistRemove: (el) => { 1136 try { 1137 const item = getProductData(el) 1138 if (!item) { 1139 Tracker.log(`No product data found for wishlist removal: ${el}`, 'warn') 1140 return 1141 } 1142 1143 const elyEvent = eventTemplates.elysium('wishlist_removed', '', { 1144 eventCategory: 'Wishlist Engagement', 1145 eventAction: 'removed from wishlist', 1146 eventLabel: item?.item_id 1147 }) 1148 elyEvent.product_id = item?.item_id 1149 elyEvent.product_name = item?.item_name 1150 1151 Tracker.push(elyEvent) 1152 Tracker.sendPubSubEvent('RemoveFromWishlist', { 1153 event: { 1154 type: 'wishlist_interaction_event', 1155 subtype: 'trash', 1156 items: [{ product_group: { id: parseInt(el) }, quantity: 1 }] 1157 } 1158 }) 1159 } catch (error) { 1160 Tracker.log(`Error in wishlistRemove handler: ${error.message}`, 'warn') 1161 } 1162 }, 1163 1164 wishlistLoggedOut: (el) => { 1165 Tracker.sendPubSubEvent('WishlistLoggedOut', { 1166 event: { 1167 type: 'wishlist_interaction_event', 1168 subtype: 'logged_out_attempt', 1169 items: [{ product_group: { id: parseInt(el) }, quantity: 1 }] 1170 } 1171 }) 1172 }, 1173 1174 // Widget handlers 1175 widgetTrack: (e) => { 1176 const widgetDescription = e.parentElement.dataset?.description 1177 const widgetKey = e.parentElement.dataset?.component 1178 const widgetId = e.parentElement.dataset?.id 1179 1180 const ecommerceEvent = eventTemplates.ecommerce('view_promotion', { 1181 creative_name: widgetKey ?? '', 1182 creative_slot: '', 1183 promotion_id: widgetId, 1184 promotion_name: widgetDescription, 1185 promotion_page: window?.location?.pathname 1186 }) 1187 Tracker.push(ecommerceEvent, { commerce: true }) 1188 }, 1189 1190 widgetClick: (el) => { 1191 const widgetContainer = el?.closest('.widgets') 1192 const widgetKey = widgetContainer?.dataset?.component 1193 const widgetDescription = widgetContainer?.dataset?.description 1194 const widgetId = widgetContainer?.dataset?.id 1195 1196 const excludedWidgetKeys = ['ProductListWidget', 'GlobalWaitListSignUpWidget'] 1197 if (excludedWidgetKeys.includes(widgetKey)) return 1198 1199 const ecommerceEvent = eventTemplates.ecommerce('select_promotion', { 1200 creative_name: widgetKey ?? '', 1201 creative_slot: '', 1202 promotion_id: widgetId, 1203 promotion_name: widgetDescription, 1204 promotion_page: window?.location?.pathname 1205 }) 1206 Tracker.push(ecommerceEvent, { commerce: true }) 1207 1208 Tracker.sendPubSubEvent('WidgetClick', { 1209 event: { 1210 type: 'widget_interaction_event', 1211 subtype: 'click', 1212 widget: { 1213 id: widgetId, 1214 description: widgetDescription, 1215 component: widgetKey 1216 } 1217 } 1218 }) 1219 }, 1220 1221 // Additional handlers for completeness 1222 sortOpened: () => { 1223 Tracker.push(eventTemplates.elysium('sort_by_clicked', '', { 1224 eventCategory: 'Facet Engagement', 1225 eventAction: 'Selects Sort By' 1226 })) 1227 }, 1228 1229 outOfStock: (el) => { 1230 const item = window.Tracker.store.get(el) 1231 Tracker.push(eventTemplates.elysium('sold_out_product', '', { 1232 eventLabel: 'Sold Out Product', 1233 eventLabelValue: item, 1234 path: window?.location?.pathname + window?.location?.search 1235 })) 1236 }, 1237 1238 pageNotFound: () => { 1239 Tracker.push(eventTemplates.elysium('page_not_found', '', { 1240 eventCategory: 'Errors', 1241 eventAction: 'Page Not Found', 1242 path: window?.location?.pathname 1243 })) 1244 }, 1245 1246 noSearchResult: (el) => { 1247 Tracker.push(eventTemplates.elysium('no_search_results', '', { 1248 eventLabel: 'No Search Results', 1249 eventLabelValue: el, 1250 path: window?.location?.pathname + window?.location?.search 1251 })) 1252 }, 1253 1254 // Newsletter and engagement handlers 1255 reEngagementOpen: () => { 1256 Tracker.push(eventTemplates.elysium('newsletter_signup_shown', '', { 1257 eventCategory: 'reEngagement Tooltip Modal', 1258 eventAction: 'Shown', 1259 eventLabel: 'reEngagement Modal Message' 1260 })) 1261 }, 1262 1263 reEngagementClicked: (el) => { 1264 Tracker.push(eventTemplates.elysium('newsletter_signup_clicked', '', { 1265 eventCategory: 'reEngagement Tooltip Modal', 1266 eventAction: 'Clicked', 1267 eventLabel: 'Continue Modal Button' 1268 })) 1269 }, 1270 1271 reEngagementComplete: (el) => { 1272 Tracker.push({ 1273 event: 'customEvent', 1274 event_name: 'newsletter_signup_complete', 1275 eventData: { 1276 eventCategory: 'newsletter', 1277 eventAction: 'successful sign-up', 1278 eventPage: window?.location?.pathname 1279 } 1280 }) 1281 }, 1282 1283 newsletterContentOpen: () => { 1284 Tracker.push(eventTemplates.elysium('newsletter_signup_shown', '', { 1285 eventCategory: 'Newsletter Content', 1286 eventAction: 'Shown', 1287 eventLabel: 'Newsletter Content Modal' 1288 })) 1289 }, 1290 1291 // Account and checkout handlers 1292 updateMarketingPreferences: () => { 1293 Tracker.sendPubSubEvent('MarketingPreferenceUpdated', { 1294 event: { 1295 type: 'account_update', 1296 subtype: 'communication_preferences' 1297 } 1298 }) 1299 }, 1300 1301 accountRegistrationSuccess: () => { 1302 Tracker.sendPubSubEvent('Login', { 1303 event: { type: 'login', subtype: 'account_creation' } 1304 }) 1305 }, 1306 1307 guestCheckoutStart: async (e) => { 1308 const store = Tracker.translateTarget(e) 1309 const data = Tracker.store.get(store) 1310 return Tracker.sendPubSubEvent('StartGuestCheckout', { 1311 event: { type: 'checkout_start', subtype: 'guest_checkout' } 1312 }, data.rayId) 1313 }, 1314 1315 // Discount code handlers 1316 applyDiscountCode: (e) => { 1317 Tracker.sendPubSubEvent('ApplyDiscountCodeToBasket', { 1318 event: { type: 'discount_code_event' } 1319 }, e.rayId) 1320 }, 1321 1322 applyDiscountSuccess: (e) => { 1323 Tracker.push({ 1324 event: 'apply_coupon_success', 1325 coupon_code: e.promoCode 1326 }) 1327 1328 Tracker.handlers.applyDiscountCode({ message: e.message, rayId: e.rayId }) 1329 }, 1330 1331 applyDiscountFailure: (e) => { 1332 1333 Tracker.push({ 1334 event: 'apply_coupon_fail', 1335 coupon_code: e.promoCode, 1336 error_message: e.errorMessage 1337 }) 1338 1339 Tracker.handlers.applyDiscountCode({ message: e.errorMessage, rayId: e.rayId }) 1340 }, 1341 1342 // Chatbot handler 1343 chatbotFeedback: (trackerParam, messageId) => { 1344 const { value } = Tracker.store.get('rayId') 1345 Tracker.sendPubSubEvent('ChatbotConversationFeedback', { 1346 event: { type: 'conversation', subtype: `${trackerParam}` }, 1347 ...(messageId && { origin: { widget_id: `${messageId}` } }) 1348 }, value) 1349 }, 1350 1351 // Newsletter signup complete handler 1352 newsletterSignupComplete: (el) => { 1353 Tracker.push(eventTemplates.elysium('', '', { 1354 eventAction: 'successful sign-up', 1355 eventCategory: 'newsletter', 1356 eventLabel: 'newsletter' 1357 })) 1358 }, 1359 1360 // Product item selection handler 1361 selectItem: (e) => { 1362 try { 1363 Tracker.log('Product item image/title clicked!', 'log') 1364 1365 // Get product data directly from the event (passed from ProductItem component) 1366 let data = e.productData 1367 1368 if (!data) { 1369 Tracker.log('No product data provided in event', 'warn') 1370 return 1371 } 1372 1373 Tracker.log('Product data from component:', data) 1374 1375 // Early return if no valid data 1376 if (!data.sku && !data.title) { 1377 Tracker.log('Invalid product data - missing sku and title', 'warn') 1378 return 1379 } 1380 1381 // Get the product list data for item_list_name 1382 const productListData = Tracker.store.get('productList') || {} 1383 const itemListName = productListData.title || 'Product List' 1384 1385 const ecommerceEvent = eventTemplates.ecommerce('select_item', { 1386 items: [{ 1387 item_id: data?.sku ?? '', 1388 item_name: data?.title ?? '', 1389 item_brand: data?.brand?.name ?? '', 1390 price: parseFloat(data?.defaultVariant?.price?.price?.amount) || 0, 1391 item_category: data?.categoryLevel1 ? (data?.categoryLevel2 ? `${data.categoryLevel1} / ${data.categoryLevel2}` : data.categoryLevel1) : '', 1392 item_list_name: itemListName 1393 }] 1394 }, parseFloat(data?.defaultVariant?.price?.price?.amount) || 0) 1395 1396 if (ecommerceEvent) { 1397 Tracker.push(ecommerceEvent, { commerce: true }) 1398 Tracker.log('Product item image select_item event sent', 'log') 1399 } 1400 } catch (error) { 1401 Tracker.log(`Error in selectItem handler: ${error.message}`, 'warn') 1402 } 1403 }, 1404 1405 // Checkout button click handler 1406 checkoutButtonClick: () => { 1407 Tracker.push(eventTemplates.elysium('', '', { 1408 eventAction: 'Go To Checkout', 1409 eventCategory: 'Link' 1410 })) 1411 } 1412 }, 1413 1414 trackApiConfig: null, 1415 config: null, 1416 store: { 1417 get(k) { 1418 return Tracker.store[k] 1419 }, 1420 set(k, v) { 1421 const result = Tracker.store[k] ? Object.assign(Tracker.store[k], v) : v 1422 Tracker.store[k] = result 1423 // Update cache for frequently accessed items 1424 if (k === 'currency') cache.currency = v 1425 if (k === 'locale') cache.locale = v?.value 1426 if (k === 'rayId') cache.rayId = v 1427 return result 1428 } 1429 } 1430 } 1431 1432 Tracker.assignTracker() 1433 1434 if (typeof window.Tracker.load !== 'function') { 1435 const cfg = window.__TRACKER_CONFIG__ || {}; 1436 Tracker.init({ 1437 debug: true, 1438 selectors: { 1439 track: 'data-track', 1440 type: 'data-track-type', 1441 store: 'data-track-push', 1442 currency: 'data-track-currency' 1443 }, 1444 dev: cfg.dev, 1445 watchers: cfg.watchers 1446 }) 1447 } else { 1448 window.Tracker.load() 1449 } 1450})()
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.