(function(){ const zoovuId = 'wNWhCtTlrKSTXiK/+VgEGwiieUOmXUpH'; const domainId = '05ab0b94-c821-4253-a7ad-e1aafaccb7ae'; const variables = [{"id":"6353c517-0b13-4612-8dfd-7a28a75d772b","name":"[Bosch PT] Are products missing in autosuggestions?","type":"FUNCTION","valueType":"BOOLEAN","function":function(event){// Different DOM for cart page if (window.location.href.includes("eshop")) { const products = document.querySelectorAll(`[data-test-id="complex-suggest-item"]`); if (products.length === 0) { return true; } else { return false; } } else { const isAlgolia = Boolean(document.querySelector("[class*=algolia]")); if (isAlgolia) { const noResults = Boolean(document.querySelector(".m-algolia__no-results")); if (noResults) { return true; } else { return false; } } else { const anyItems = Boolean(document.querySelector(".m-searchNavigationResults__itemsWrapper")); if (anyItems) { return false; } else { return true; } } } /* */},"scope":"LOCAL"},{"id":"7a089517-a9dd-4fb4-a822-f2086593e99b","name":"[Bosch PT] ATC was clicked","type":"FUNCTION","valueType":"BOOLEAN","function":function(event){if (event.target.closest("button.addToCart")) { return true; } else { return false; } /* */},"scope":"LOCAL"},{"id":"c648b7b3-561b-4aa3-a35e-cad90bd6aa87","name":"[Bosch PT] Cart quantity component was used","type":"FUNCTION","valueType":"BOOLEAN","function":function(event){const isQuantityButtonClick = Boolean( event.target.closest('[data-test-id="decrease-quantity-button"]') || event.target.closest('[data-test-id="increase-quantity-button"]') ); return isQuantityButtonClick; /* */},"scope":"LOCAL"},{"id":"5400d372-a64d-4866-9ea8-8311ca5616f1","name":"[Bosch PT] Currency","type":"FUNCTION","valueType":"TEXT","function":function(event){const COUNTRY_CURRENCY = { AD: "EUR", AE: "AED", AF: "AFN", AG: "XCD", AI: "XCD", AL: "ALL", AM: "AMD", AO: "AOA", AR: "ARS", AS: "USD", AT: "EUR", AU: "AUD", AW: "AWG", AX: "EUR", AZ: "AZN", BA: "BAM", BB: "BBD", BD: "BDT", BE: "EUR", BF: "XOF", BG: "BGN", BH: "BHD", BI: "BIF", BJ: "XOF", BL: "EUR", BM: "BMD", BN: "BND", BO: "BOB", BQ: "USD", BR: "BRL", BS: "BSD", BT: "BTN", BV: "NOK", BW: "BWP", BY: "BYN", BZ: "BZD", CA: "CAD", CC: "AUD", CD: "CDF", CF: "XAF", CG: "XAF", CH: "CHF", CI: "XOF", CK: "NZD", CL: "CLP", CM: "XAF", CN: "CNY", CO: "COP", CR: "CRC", CU: "CUP", CV: "CVE", CW: "ANG", CX: "AUD", CY: "EUR", CZ: "CZK", DE: "EUR", DJ: "DJF", DK: "DKK", DM: "XCD", DO: "DOP", DZ: "DZD", EC: "USD", EE: "EUR", EG: "EGP", EH: "MAD", ER: "ERN", ES: "EUR", ET: "ETB", FI: "EUR", FJ: "FJD", FK: "FKP", FM: "USD", FO: "DKK", FR: "EUR", GA: "XAF", GB: "GBP", GD: "XCD", GE: "GEL", GF: "EUR", GG: "GBP", GH: "GHS", GI: "GIP", GL: "DKK", GM: "GMD", GN: "GNF", GP: "EUR", GQ: "XAF", GR: "EUR", GT: "GTQ", GU: "USD", GW: "XOF", GY: "GYD", HK: "HKD", HM: "AUD", HN: "HNL", HR: "EUR", HT: "HTG", HU: "HUF", ID: "IDR", IE: "EUR", IL: "ILS", IM: "GBP", IN: "INR", IO: "USD", IQ: "IQD", IR: "IRR", IS: "ISK", IT: "EUR", JE: "GBP", JM: "JMD", JO: "JOD", JP: "JPY", KE: "KES", KG: "KGS", KH: "KHR", KI: "AUD", KM: "KMF", KN: "XCD", KP: "KPW", KR: "KRW", KW: "KWD", KY: "KYD", KZ: "KZT", LA: "LAK", LB: "LBP", LC: "XCD", LI: "CHF", LK: "LKR", LR: "LRD", LS: "LSL", LT: "EUR", LU: "EUR", LV: "EUR", LY: "LYD", MA: "MAD", MC: "EUR", MD: "MDL", ME: "EUR", MF: "EUR", MG: "MGA", MH: "USD", MK: "MKD", ML: "XOF", MM: "MMK", MN: "MNT", MO: "MOP", MP: "USD", MQ: "EUR", MR: "MRU", MS: "XCD", MT: "EUR", MU: "MUR", MV: "MVR", MW: "MWK", MX: "MXN", MY: "MYR", MZ: "MZN", NA: "NAD", NC: "XPF", NE: "XOF", NF: "AUD", NG: "NGN", NI: "NIO", NL: "EUR", NO: "NOK", NP: "NPR", NR: "AUD", NU: "NZD", NZ: "NZD", OM: "OMR", PA: "PAB", PE: "PEN", PF: "XPF", PG: "PGK", PH: "PHP", PK: "PKR", PL: "PLN", PM: "EUR", PN: "NZD", PR: "USD", PS: "ILS", PT: "EUR", PW: "USD", PY: "PYG", QA: "QAR", RE: "EUR", RO: "RON", RS: "RSD", RU: "RUB", RW: "RWF", SA: "SAR", SB: "SBD", SC: "SCR", SD: "SDG", SE: "SEK", SG: "SGD", SH: "SHP", SI: "EUR", SJ: "NOK", SK: "EUR", SL: "SLL", SM: "EUR", SN: "XOF", SO: "SOS", SR: "SRD", SS: "SSP", ST: "STN", SV: "USD", SX: "ANG", SY: "SYP", SZ: "SZL", TC: "USD", TD: "XAF", TF: "EUR", TG: "XOF", TH: "THB", TJ: "TJS", TK: "NZD", TL: "USD", TM: "TMT", TN: "TND", TO: "TOP", TR: "TRY", TT: "TTD", TV: "AUD", TW: "TWD", TZ: "TZS", UA: "UAH", UG: "UGX", UM: "USD", US: "USD", UY: "UYU", UZ: "UZS", VA: "EUR", VC: "XCD", VE: "VES", VG: "USD", VI: "USD", VN: "VND", VU: "VUV", WF: "XPF", WS: "WST", YE: "YER", YT: "EUR", ZA: "ZAR", ZM: "ZMW", ZW: "ZWL" }; const locales = window.location.pathname.split("/").filter(Boolean); const country = locales[0].toUpperCase(); return COUNTRY_CURRENCY[country]; /* */},"scope":"LOCAL"},{"id":"dafa3f7c-a241-4d3a-ac9c-389eb07d93bd","name":"[Bosch PT] Currency for purchase event","type":"FUNCTION","valueType":"TEXT","function":function(event){const isPurchasePayload = (value) => value && typeof value === 'object' && !Array.isArray(value) && typeof value.currency === 'string' && typeof value.transaction_id === 'string'; const purchaseEvent = window.dataLayer ?.map(entry => { if (!entry || typeof entry !== 'object') return null; for (const [, value] of Object.entries(entry)) { if (isPurchasePayload(value)) { return value; } } return null; }) .find(Boolean); const currency = purchaseEvent.currency; if (!currency) { const COUNTRY_CURRENCY = { AD: "EUR", AE: "AED", AF: "AFN", AG: "XCD", AI: "XCD", AL: "ALL", AM: "AMD", AO: "AOA", AR: "ARS", AS: "USD", AT: "EUR", AU: "AUD", AW: "AWG", AX: "EUR", AZ: "AZN", BA: "BAM", BB: "BBD", BD: "BDT", BE: "EUR", BF: "XOF", BG: "BGN", BH: "BHD", BI: "BIF", BJ: "XOF", BL: "EUR", BM: "BMD", BN: "BND", BO: "BOB", BQ: "USD", BR: "BRL", BS: "BSD", BT: "BTN", BV: "NOK", BW: "BWP", BY: "BYN", BZ: "BZD", CA: "CAD", CC: "AUD", CD: "CDF", CF: "XAF", CG: "XAF", CH: "CHF", CI: "XOF", CK: "NZD", CL: "CLP", CM: "XAF", CN: "CNY", CO: "COP", CR: "CRC", CU: "CUP", CV: "CVE", CW: "ANG", CX: "AUD", CY: "EUR", CZ: "CZK", DE: "EUR", DJ: "DJF", DK: "DKK", DM: "XCD", DO: "DOP", DZ: "DZD", EC: "USD", EE: "EUR", EG: "EGP", EH: "MAD", ER: "ERN", ES: "EUR", ET: "ETB", FI: "EUR", FJ: "FJD", FK: "FKP", FM: "USD", FO: "DKK", FR: "EUR", GA: "XAF", GB: "GBP", GD: "XCD", GE: "GEL", GF: "EUR", GG: "GBP", GH: "GHS", GI: "GIP", GL: "DKK", GM: "GMD", GN: "GNF", GP: "EUR", GQ: "XAF", GR: "EUR", GT: "GTQ", GU: "USD", GW: "XOF", GY: "GYD", HK: "HKD", HM: "AUD", HN: "HNL", HR: "EUR", HT: "HTG", HU: "HUF", ID: "IDR", IE: "EUR", IL: "ILS", IM: "GBP", IN: "INR", IO: "USD", IQ: "IQD", IR: "IRR", IS: "ISK", IT: "EUR", JE: "GBP", JM: "JMD", JO: "JOD", JP: "JPY", KE: "KES", KG: "KGS", KH: "KHR", KI: "AUD", KM: "KMF", KN: "XCD", KP: "KPW", KR: "KRW", KW: "KWD", KY: "KYD", KZ: "KZT", LA: "LAK", LB: "LBP", LC: "XCD", LI: "CHF", LK: "LKR", LR: "LRD", LS: "LSL", LT: "EUR", LU: "EUR", LV: "EUR", LY: "LYD", MA: "MAD", MC: "EUR", MD: "MDL", ME: "EUR", MF: "EUR", MG: "MGA", MH: "USD", MK: "MKD", ML: "XOF", MM: "MMK", MN: "MNT", MO: "MOP", MP: "USD", MQ: "EUR", MR: "MRU", MS: "XCD", MT: "EUR", MU: "MUR", MV: "MVR", MW: "MWK", MX: "MXN", MY: "MYR", MZ: "MZN", NA: "NAD", NC: "XPF", NE: "XOF", NF: "AUD", NG: "NGN", NI: "NIO", NL: "EUR", NO: "NOK", NP: "NPR", NR: "AUD", NU: "NZD", NZ: "NZD", OM: "OMR", PA: "PAB", PE: "PEN", PF: "XPF", PG: "PGK", PH: "PHP", PK: "PKR", PL: "PLN", PM: "EUR", PN: "NZD", PR: "USD", PS: "ILS", PT: "EUR", PW: "USD", PY: "PYG", QA: "QAR", RE: "EUR", RO: "RON", RS: "RSD", RU: "RUB", RW: "RWF", SA: "SAR", SB: "SBD", SC: "SCR", SD: "SDG", SE: "SEK", SG: "SGD", SH: "SHP", SI: "EUR", SJ: "NOK", SK: "EUR", SL: "SLL", SM: "EUR", SN: "XOF", SO: "SOS", SR: "SRD", SS: "SSP", ST: "STN", SV: "USD", SX: "ANG", SY: "SYP", SZ: "SZL", TC: "USD", TD: "XAF", TF: "EUR", TG: "XOF", TH: "THB", TJ: "TJS", TK: "NZD", TL: "USD", TM: "TMT", TN: "TND", TO: "TOP", TR: "TRY", TT: "TTD", TV: "AUD", TW: "TWD", TZ: "TZS", UA: "UAH", UG: "UGX", UM: "USD", US: "USD", UY: "UYU", UZ: "UZS", VA: "EUR", VC: "XCD", VE: "VES", VG: "USD", VI: "USD", VN: "VND", VU: "VUV", WF: "XPF", WS: "WST", YE: "YER", YT: "EUR", ZA: "ZAR", ZM: "ZMW", ZW: "ZWL" }; const locales = window.location.pathname.split("/").filter(Boolean); const country = locales[0].toUpperCase(); return COUNTRY_CURRENCY[country]; } else { return currency; } /* */},"scope":"LOCAL"},{"id":"d11d4343-6f33-4ffd-9207-aa43f2677671","name":"[Bosch PT] Delete button was clicked","type":"FUNCTION","valueType":"BOOLEAN","function":function(event){ const deleteButton = event.target.closest('[data-test-id="line-item-delete-button"]'); if (deleteButton) { return true; } return false; /* */},"scope":"LOCAL"},{"id":"cc08371f-f1bd-4513-9179-7ddb9dda4fd1","name":"[Bosch PT] Get all SKUs from search result","type":"FUNCTION","valueType":"LIST","function":function(event){const productSkus = []; const categoryTileLinks = document.querySelectorAll(".category-grid-tile__link-wrapper"); const algoliaProducts = document.querySelectorAll(".algolia-hit-item"); // Product on search result can have many variants and no "main sku", so I'm only passing SKU that is included in hyperlink. // To be checked later with product team if this makes sense. categoryTileLinks.forEach((tileLink) => { const href = tileLink.getAttribute("href"); const skus = tileLink.getAttribute("data-track_dyn_productid").split(","); skus.forEach((sku) => { if (href.includes(sku)) { productSkus.push(sku); } }) }) algoliaProducts.forEach((product) => { const sku = product.getAttribute("data-track_dyn_productid"); if (sku) { productSkus.push(sku); } }) return productSkus; /* */},"scope":"LOCAL"},{"id":"f864d134-858a-41fb-a69e-c8c1bf80556b","name":"[Bosch PT] Get closest hyperlink","type":"FUNCTION","valueType":"TEXT","function":function(event){if (event.target.closest(`.m-algolia button[aria-label="Go to search results page"]`)) { return "/"; } return event.target.closest("a").href; /* */},"scope":"LOCAL"},{"id":"a4097d75-887f-4947-a618-415b4f4ef370","name":"[Bosch PT] Get price from ATC button","type":"FUNCTION","valueType":"DECIMAL","function":function(event){const atcButton = event.target.closest("button.addToCart"); const sku = atcButton.getAttribute("data-sku"); const product = productCartMap[sku]; return Number(product.price); /* */},"scope":"LOCAL"},{"id":"bf72883a-4663-41ed-baf6-584175490328","name":"[Bosch PT] Get quantity for REMOVE_FROM_CART","type":"FUNCTION","valueType":"INTEGER","function":function(event){const productCard = event.target.closest("[class*=lineItemContainer]"); const quantity = productCard.querySelector(`[data-test-id="product-quantity"]`).value; return Number(quantity); /* */},"scope":"LOCAL"},{"id":"0f6a3dfb-49ad-486c-baa4-bb55bb2b1a30","name":"[Bosch PT] isPDP","type":"FUNCTION","valueType":"BOOLEAN","function":function(event){const isPdp = document.body.classList.contains("t-productdetailpage--eshop") || document.body.classList.contains("t-pdp"); return isPdp; /* */},"scope":"LOCAL"},{"id":"c381b7a3-3dd3-4100-9c41-e85911343723","name":"[BoschPT] isSearchInput","type":"FUNCTION","valueType":"BOOLEAN","function":function(event){if (event.target.closest("input[id*='Search']")) { return true; } if (event.target.id === "search") { return true; } // cart page if (event.target.closest(`[name="searchQuery"]`)) { return true; } return false; /* */},"scope":"LOCAL"},{"id":"90055979-6dde-4e58-919a-7e7db009aae3","name":"[Bosch PT] It's pdp but product is not necessarily selected","type":"FUNCTION","valueType":"BOOLEAN","function":function(event){const firstPdp = document.querySelector("#product-detail-stage"); const secondPdp = document.querySelector(".o-gt-stage"); if (firstPdp || secondPdp) { return true; } return false; /* */},"scope":"LOCAL"},{"id":"e7fe10dc-2909-4f60-974c-a1ed812a0506","name":"[Bosch PT] Label for search results on SERP","type":"FUNCTION","valueType":"TEXT","function":function(event){const isEmpty = !!(document.querySelector("[class*=no-search-results]")); const anyResultOnAlgolia = document.querySelector("[class*=algolia-hit-item]"); if (isEmpty && !anyResultOnAlgolia) { return "Empty search result page"; } else { return "Search results" } /* */},"scope":"LOCAL"},{"id":"97966e49-2e80-42f2-ab9b-65a4d909d336","name":"[Bosch PT] Locale formatted","type":"FUNCTION","valueType":"TEXT","function":function(event){const locales = window.location.pathname.split("/").filter(Boolean); const country = locales[0].toUpperCase(); const language = locales[1].toLowerCase(); const localeFormatted = `${language}-${country}`; return localeFormatted; /* */},"scope":"LOCAL"},{"id":"ce635408-b5eb-48e4-9b48-949d83c92b29","name":"[Bosch PT] Page is PDP","type":"FUNCTION","valueType":"BOOLEAN","function":function(event){const mainSectionInFirstPDP = document.querySelector("#product-detail-stage"); const mainSectionInSecondPDP = document.querySelector(".o-gt-stage"); if (mainSectionInFirstPDP || mainSectionInSecondPDP) { return true; } return false; /* */},"scope":"LOCAL"},{"id":"4d44ede1-271d-47d1-a400-2c20e70deaf6","name":"[Bosch PT] Price for PDP_VISITED","type":"FUNCTION","valueType":"DECIMAL","function":function(event){// there are multiple price elements, but we only care about first one here at both PDPs const priceText = document.querySelector(".a-gt-price__price")?.textContent; if (!priceText) { return 0; } function parsePrice(str) { // Extract only digits, commas, dots const numberPart = str.match(/[\d.,]+/g)?.join('') || ""; // If the number uses comma as decimal separator (European format) // Detect by checking last comma position const hasCommaAsDecimal = numberPart.includes(',') && numberPart.lastIndexOf(',') > numberPart.lastIndexOf('.'); let normalized = numberPart; if (hasCommaAsDecimal) { // Remove thousand separators and convert final comma to dot normalized = normalized.replace(/\./g, ""); // remove thousand dots normalized = normalized.replace(/,/g, "."); // convert decimal comma } else { // Normal format: remove thousand separators (commas) normalized = normalized.replace(/,/g, ""); } return parseFloat(normalized); } const price = parsePrice(priceText); return price; /* */},"scope":"LOCAL"},{"id":"b3d67dce-a9a9-468f-9360-d80eae8d665b","name":"[Bosch PT] PricePerUnit for UPDATE_CART","type":"FUNCTION","valueType":"DECIMAL","function":function(event){const products = JSON.parse(sessionStorage.getItem("zoovu-cart-product-prices") || "[]"); const cartItem = event.target.closest('[class*=lineItemContainer]'); const skuFormatted = cartItem.querySelector('[data-test-id="product-sku"]').textContent; const sku = skuFormatted.split(":")[1].trim(); const product = products.find(product => product.sku === sku); return product.pricePerUnit; // old version in case new one needs a fallback for some reason // function getCurrentPrice() // { // const productCard = event.target.closest("[class*=lineItemContainer]"); // const priceText = productCard.querySelector(`[data-test-id="product-price-block"] div p`).textContent; // function parsePrice(str) { // // Extract only digits, commas, dots // const numberPart = str.match(/[\d.,]+/g)?.join('') || ""; // // If the number uses comma as decimal separator (European format) // // Detect by checking last comma position // const hasCommaAsDecimal = numberPart.includes(',') && // numberPart.lastIndexOf(',') > numberPart.lastIndexOf('.'); // let normalized = numberPart; // if (hasCommaAsDecimal) { // // Remove thousand separators and convert final comma to dot // normalized = normalized.replace(/\./g, ""); // remove thousand dots // normalized = normalized.replace(/,/g, "."); // convert decimal comma // } else { // // Normal format: remove thousand separators (commas) // normalized = normalized.replace(/,/g, ""); // } // return parseFloat(normalized); // } // return parsePrice(priceText); // } // function getCurrentQuantity() // { // const productCard = event.target.closest("[class*=lineItemContainer]"); // const currentQuantity = Number(productCard.querySelector(`[data-test-id="product-quantity"]`).value); // return currentQuantity; // } // const currentPrice = getCurrentPrice(); // const currentQuantity = getCurrentQuantity(); // const pricePerUnit = currentPrice / currentQuantity; /* */},"scope":"LOCAL"},{"id":"3194c5d8-d8f5-42c7-b90a-d38b1446b4b1","name":"[Bosch PT] Products for purchase event","type":"FUNCTION","valueType":"LIST","function":function(event){const isPurchasePayload = (value) => value && typeof value === 'object' && !Array.isArray(value) && typeof value.currency === 'string' && typeof value.transaction_id === 'string'; const purchaseEvent = window.dataLayer ?.map(entry => { if (!entry || typeof entry !== 'object') return null; for (const [, value] of Object.entries(entry)) { if (isPurchasePayload(value)) { return value; } } return null; }) .find(Boolean); const products = []; purchaseEvent.items.forEach((item) => { products.push({ sku: item.item_id, name: item.item_name, pricePerUnit: item.price, quantity: item.quantity, }); }); return products; /* */},"scope":"LOCAL"},{"id":"d3e5945a-8e55-4b96-b963-ccbc71ad4352","name":"[Bosch PT] Product was clicked on autosuggestions","type":"FUNCTION","valueType":"BOOLEAN","function":function(event){if (event.target.closest(".search [class*=ais-Hits-item]")) { return true; } // Different DOM for cart page if (window.location.href.includes("eshop")) { return Boolean(event.target.closest(`[data-test-id="complex-suggest-item"]`)); } else { return (Boolean(event.target.closest(`.m-searchNavigationResults__product-suggestion a:not([href*=search]`))); } /* */},"scope":"LOCAL"},{"id":"1330574d-7910-45d3-b50c-e0e82bfc11f8","name":"[Bosch PT] Quantity for ATC button","type":"FUNCTION","valueType":"INTEGER","function":function(event){const atcButton = event.target.closest("button.addToCart"); const sku = atcButton.getAttribute("data-sku"); const product = productCartMap[sku]; // In case ATC is connected to quantity input const quantityContainer = event.target.parentElement.querySelector("input"); if (quantityContainer) { return Number(quantityContainer.value); } return Number(product.quantity); /* */},"scope":"LOCAL"},{"id":"f2c8b929-18d6-4fa1-9c48-53dcdfe223d5","name":"[Bosch PT] Search result is empty","type":"FUNCTION","valueType":"BOOLEAN","function":function(event){const isEmpty = !!(document.querySelector("[class*=no-search-results]")); const anyResultOnAlgolia = document.querySelector("[class*=algolia-hit-item]"); if (isEmpty && !anyResultOnAlgolia) { return true; } else { return false; } /* */},"scope":"LOCAL"},{"id":"9219e29f-45ce-4e95-884b-c97151c7ee88","name":"[Bosch PT] Search result was clicked","type":"FUNCTION","valueType":"BOOLEAN","function":function(event){const product = event.target.closest(".algolia-hit-item") || event.target.closest(".category-grid-tile"); return Boolean(product); /* */},"scope":"LOCAL"},{"id":"28432573-d25f-474a-9e55-c457f58bb150","name":"[Bosch PT] Show all results button was clicked","type":"FUNCTION","valueType":"BOOLEAN","function":function(event){const showAllResultsButton = event.target.closest(`[data-test-id="search-show-all-products-link"]`) || event.target.closest("[class*=viewallresults]") || event.target.closest(`.m-algolia button[aria-label="Go to search results page"]`); return Boolean(showAllResultsButton); /* */},"scope":"LOCAL"},{"id":"af1f6746-9b0a-467c-9eb8-fab74479a700","name":"[Bosch PT] SKU from any cart item action (delete/modify)","type":"FUNCTION","valueType":"TEXT","function":function(event){const cartItem = event.target.closest('[class*=lineItemContainer]'); const skuFormatted = cartItem.querySelector('[data-test-id="product-sku"]').textContent; const sku = skuFormatted.split(":")[1].trim(); return sku; /* */},"scope":"LOCAL"},{"id":"5a950c76-96c5-41f3-8f9b-d83de69cd69b","name":"[Bosch PT] SKU from ATC","type":"FUNCTION","valueType":"TEXT","function":function(event){const sku = event.target.closest("button.addToCart").getAttribute('data-sku'); return sku; /* */},"scope":"LOCAL"},{"id":"f3dd91b8-0939-4ecf-bb28-32c8759d2f4d","name":"[Bosch PT] SKU from PDP","type":"FUNCTION","valueType":"TEXT","function":function(event){return window.ZoovuDIR.getCurrentSku(); /* */},"scope":"LOCAL"},{"id":"59c718f9-cd2c-4f44-9755-2609e78a2812","name":"[Bosch PT] SKU on PDP has changed","type":"FUNCTION","valueType":"BOOLEAN","function":function(event){const currentSku = window.ZoovuDIR.getCurrentSku(); const previousSku = window.ZoovuDIR.getPreviousSku(); if (currentSku !== previousSku) { return true; } else { return false; } /* */},"scope":"LOCAL"},{"id":"e22afd9c-c6f7-4e8f-8413-31ded27641a3","name":"[Bosch PT] Target is category link in autosuggestions","type":"FUNCTION","valueType":"BOOLEAN","function":function(event){// Different DOM for cart page if (window.location.href.includes("eshop")) { if (event.target.closest(`a[data-test-id="more-suggestions-link"]`)) { return true; } // const isInsideLinksContainer = Boolean(event.target.closest(`ul[class*=headerSearchResultsMoreSuggestionsBlock]`)); // const isLink = Boolean(event.target.closest(`[data-test-id="more-suggestions-link"]`)); // if (isInsideLinksContainer && isLink) // { // const link = event.target.closest(`[data-test-id="more-suggestions-link"]`); // const href = link.getAttribute("href"); // if (href.includes("www")) // { // return true; // } // } } else { if (event.target.closest(".m-searchNavigationResults__linksItems__link ")) { return true; } // const isInsideLinksContainer = Boolean(event.target.closest(`.m-searchNavigationResults__linksItems`)); // const isLink = Boolean(event.target.closest(`.m-searchNavigationResults__linksItems__link`)); // if (isInsideLinksContainer && isLink) // { // const link = event.target.closest(`.m-searchNavigationResults__linksItems__link`); // const href = link.getAttribute("href"); // if (href.includes("www")) // { // return true; // } // } } return false; /* */},"scope":"LOCAL"},{"id":"98de71ca-516e-4a2b-a7af-32fd8de785e5","name":"[Bosch PT] TargetURL for clickout in autosuggestions","type":"FUNCTION","valueType":"TEXT","function":function(event){return event.target.closest("a").getAttribute("href"); /* */},"scope":"LOCAL"},{"id":"baa79840-995a-45a3-8f9d-1d2a1983b109","name":"[Bosch PT] TargetURL for search clickout","type":"FUNCTION","valueType":"TEXT","function":function(event){return event.target.closest("a").href; /* */},"scope":"LOCAL"},{"id":"b6d9960e-8afc-4d54-b00f-80bfb9409f8b","name":"[Bosch PT] TransactionID for purchase event","type":"FUNCTION","valueType":"TEXT","function":function(event){const isPurchasePayload = (value) => value && typeof value === 'object' && !Array.isArray(value) && typeof value.currency === 'string' && typeof value.transaction_id === 'string'; const purchaseEvent = window.dataLayer ?.map(entry => { if (!entry || typeof entry !== 'object') return null; for (const [, value] of Object.entries(entry)) { if (isPurchasePayload(value)) { return value; } } return null; }) .find(Boolean); return purchaseEvent.transaction_id; /* */},"scope":"LOCAL"},{"id":"f684a5b4-3a9b-469d-a4d8-dfc15bb2c33e","name":"[Bosch PT] Updated quantity for UPDATE_CART","type":"FUNCTION","valueType":"INTEGER","function":function(event){const productCard = event.target.closest("[class*=lineItemContainer]"); const currentQuantity = Number(productCard.querySelector(`[data-test-id="product-quantity"]`).value); const shouldSubtract = Boolean(event.target.closest(`[data-test-id="decrease-quantity-button"]`)); const shouldAdd = Boolean(event.target.closest(`[data-test-id="increase-quantity-button"]`)); if (shouldAdd) { return currentQuantity + 1; } else if (shouldSubtract) { return currentQuantity - 1; } /* */},"scope":"LOCAL"},{"id":"3b881070-bf08-4869-be93-2b5d94a99c5b","name":"Clickout, category","type":"CONSTANT","valueType":"TEXT","value":"Clickout, category","scope":"GLOBAL"},{"id":"d2d8bad9-06f7-4f78-9184-8c38a9a469a1","name":"Clickout, view all products","type":"CONSTANT","valueType":"TEXT","value":"Clickout, view all products","scope":"GLOBAL"},{"id":"f3b453cf-520a-4785-b040-5093e047a9a7","name":"Empty array","type":"FUNCTION","valueType":"LIST","function":function(event){return []; /* */},"scope":"GLOBAL"},{"id":"2e141436-b43c-40d6-bbed-64cfdca7f222","name":"Go to PDP - search clickout","type":"CONSTANT","valueType":"TEXT","value":"Go to PDP - search clickout","scope":"GLOBAL"},{"id":"2429c8a0-dc9c-4a08-9286-809e837843b4","name":"Label for search result (popup)","type":"FUNCTION","valueType":"TEXT","function":function(event){const isAlgolia = Boolean(document.querySelector("[class*=algolia]")); if (isAlgolia) { const noResults = Boolean(document.querySelector("[class*=algoliaNoResultsView_noResultsViewWrapper]")); if (noResults) { return "Empty search result popup"; } else { return "Search results"; } } else { const anyItems = Boolean(document.querySelector(".m-searchNavigationResults__itemsWrapper")); if (anyItems) { return "Search results"; } else { return "Empty search result popup"; } } /* */},"scope":"LOCAL"},{"id":"6c77635c-e95a-4eca-8086-f6be1b1193e8","name":"Named referral: Search","type":"CONSTANT","valueType":"TEXT","value":"Search","scope":"GLOBAL"},{"id":"75cf8fd1-fafe-44be-9806-23ff3017c9c1","name":"Page URL","type":"FUNCTION","valueType":"TEXT","function":function(event){return window.location.href; /* */},"scope":"GLOBAL"}]; const script = {"id":"fe90ed05-cecf-479b-81fa-2b96f5dd904d","rows":[{"id":"7fb4c435-0f9c-425e-9577-6e107450222f","rowType":"CUSTOM_ACTION","trigger":{"id":"bf1c2b81-9d97-4c51-89c0-3fb9d0e7cd49","name":"[Bosch PT] Visiting any page","type":"PAGE_LOAD","groups":[{"id":"143ecc59-02c2-42bc-ac96-24ac3de37239","rows":[{"valueType":"BOOLEAN","id":"a6bd8539-c2d2-4eda-bc01-2e62a52cd7b1","variableId":"ce635408-b5eb-48e4-9b48-949d83c92b29","value":"true","operator":"EQUALS"}]},{"id":"13260e13-b9ff-4683-a0eb-ef1ef1882b39","rows":[{"valueType":"BOOLEAN","id":"4165791a-1e33-41d5-a099-c948e81ef760","variableId":"ce635408-b5eb-48e4-9b48-949d83c92b29","value":"false","operator":"EQUALS"}]}]},"action":{"id":"235043b1-d7dc-40da-88b4-62cd4d0bae75","type":"CUSTOM","code":function(event){function getAnalysisConsentFromCookie() { const name = "privacy-consents-v4"; const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); if (parts.length !== 2) return false; const cookieValue = parts.pop().split(';').shift(); const decoded = decodeURIComponent(cookieValue); const parsed = JSON.parse(decoded); return parsed?.consent?.analysis === true; } function onConsentChange() { const consent = getAnalysisConsentFromCookie(); if (consent) { enableTracking(); } else { disableTracking(); } } window.addEventListener("consent_updated", () => { onConsentChange(); }); window.addEventListener("consent_loaded", () => { onConsentChange(); }); onConsentChange(); /* */}},"fields":[]},{"id":"cdff5ee1-5348-4618-8722-c0dbc695123e","rowType":"EVENT","trigger":{"id":"b2c1f180-f75a-4b57-9c22-764b192c9833","name":"[Bosch PT] Show all results button was clicked","type":"CLICK","groups":[{"id":"0ff4367d-5622-4668-bfc4-c30c6164cafa","rows":[{"valueType":"BOOLEAN","id":"675e4ef5-1800-4bc9-aa8e-49c1ccb10f4f","variableId":"28432573-d25f-474a-9e55-c457f58bb150","value":"true","operator":"EQUALS"}]}]},"action":{"type":"CLICKOUT"},"fields":[{"id":"51c057c4-ab7f-4210-8abd-235c5e899876","variableId":"f864d134-858a-41fb-a69e-c8c1bf80556b","fieldName":"targetUrl"},{"id":"58496f18-9967-4285-a5dd-eb56994867c0","variableId":"97966e49-2e80-42f2-ab9b-65a4d909d336","fieldName":"locale"},{"id":"87cbab16-61b7-4eb0-b3fc-c7d66ce753ce","variableId":"d2d8bad9-06f7-4f78-9184-8c38a9a469a1","fieldName":"eventLabel"},{"id":"5461695d-6e17-43d0-ba1a-697cadb65aa6","variableId":"6c77635c-e95a-4eca-8086-f6be1b1193e8","fieldName":"namedReferral"}]},{"id":"284a08ea-09d7-474d-8686-48b6a6789c2b","rowType":"EVENT","trigger":{"id":"dbb640fb-b6ae-4376-8766-1d852b69fa2f","name":"[Bosch PT] Search input was used","type":"INPUT","delayMs":2500,"groups":[{"id":"84d8547b-4787-4929-b7fc-4367e276f70e","rows":[{"valueType":"BOOLEAN","id":"03f8d18f-db03-404e-8101-d7d3d1ca1355","variableId":"c381b7a3-3dd3-4100-9c41-e85911343723","value":"true","operator":"EQUALS"}]}]},"action":{"type":"SEARCH_RESULT"},"fields":[{"id":"74f61e09-046a-4ea7-9103-e2877bef4031","variableId":"97966e49-2e80-42f2-ab9b-65a4d909d336","fieldName":"locale"},{"id":"6387b35d-e322-4cda-9202-9bca41fb71b4","variableId":"6353c517-0b13-4612-8dfd-7a28a75d772b","fieldName":"isEmpty"},{"id":"8010d021-304b-4502-81b6-f6415f61c57c","variableId":"2429c8a0-dc9c-4a08-9286-809e837843b4","fieldName":"eventLabel"},{"id":"371f8cbb-6b76-49c9-8282-5b601a85fdad","variableId":"f3b453cf-520a-4785-b040-5093e047a9a7","fieldName":"skus"}]},{"id":"c3511da7-4e1f-4ea7-95d2-7e59090a80ec","rowType":"CUSTOM_ACTION","trigger":{"id":"1379104d-1c5f-49eb-b892-57e2b0bce5dc","name":"[Bosch PT] Product on PDP has changed","type":"CLICK","groups":[{"id":"ded30c69-a26e-4822-a482-1187058579af","rows":[{"valueType":"BOOLEAN","id":"130714f2-dbac-4897-9aba-223c67eec540","variableId":"ce635408-b5eb-48e4-9b48-949d83c92b29","value":"true","operator":"EQUALS"},{"valueType":"BOOLEAN","id":"3eb131f4-735b-414c-ae3d-5f8a09211341","variableId":"59c718f9-cd2c-4f44-9755-2609e78a2812","value":"true","operator":"EQUALS"}]}]},"action":{"id":"1921ef2a-35e4-4215-b511-0f1dfe6e1e3b","type":"CUSTOM","code":function(event){// The reason setTimeout is necessary is because otherwise this SKU will be checked after a new one is saved // This way ensures we first check the SKU in PDP_VISITED and only afterwards we save new SKU setTimeout(() => { const sku = window.ZoovuDIR.getCurrentSku(); sessionStorage.setItem("zoovu-last-selected-sku", sku); }, 500) /* */}},"fields":[]},{"id":"1d5a3f93-8567-4bcc-8b4c-1501af61919f","rowType":"EVENT","trigger":{"id":"1379104d-1c5f-49eb-b892-57e2b0bce5dc","name":"[Bosch PT] Product on PDP has changed","type":"CLICK","groups":[{"id":"ded30c69-a26e-4822-a482-1187058579af","rows":[{"valueType":"BOOLEAN","id":"130714f2-dbac-4897-9aba-223c67eec540","variableId":"ce635408-b5eb-48e4-9b48-949d83c92b29","value":"true","operator":"EQUALS"},{"valueType":"BOOLEAN","id":"3eb131f4-735b-414c-ae3d-5f8a09211341","variableId":"59c718f9-cd2c-4f44-9755-2609e78a2812","value":"true","operator":"EQUALS"}]}]},"action":{"type":"PDP_VISITED"},"fields":[{"id":"be5e7463-165c-4fe6-bf80-274e822dff1c","variableId":"97966e49-2e80-42f2-ab9b-65a4d909d336","fieldName":"locale"},{"id":"34a212ba-712e-42c5-9d22-e096a25179a1","variableId":"f3dd91b8-0939-4ecf-bb28-32c8759d2f4d","fieldName":"sku"},{"id":"855ec87b-a760-4f36-8c06-342dafa40a5f","variableId":"4d44ede1-271d-47d1-a400-2c20e70deaf6","fieldName":"price"},{"id":"a4c8e3de-5771-4d36-a3f0-e5c9917c78d9","variableId":"5400d372-a64d-4866-9ea8-8311ca5616f1","fieldName":"currencyCode"}]},{"id":"d04f46dd-f974-41c1-b741-acd58409eb4d","rowType":"CUSTOM_ACTION","trigger":{"id":"d85bd256-0569-4817-bfc1-83dff99a410a","name":"[Bosch PT] Visiting cart page","type":"PAGE_LOAD","groups":[{"id":"b8115873-32e3-4a73-a7d4-35949980c8fc","rows":[{"valueType":"TEXT","id":"ff80f2b3-5b42-441f-a823-1a20be469c33","variableId":"75cf8fd1-fafe-44be-9806-23ff3017c9c1","value":"cart","operator":"CONTAINS"}]}]},"action":{"id":"a050c1bb-ca8e-43ff-9e50-6b029f054d76","type":"CUSTOM","code":function(event){// Workaround for https://zoovu.atlassian.net/browse/COE-7137 function waitForLoaderToDisappear(selector = "[class*=pageLoader]") { return new Promise(resolve => { const check = () => { const loader = document.querySelector(selector); if (!loader) { resolve(); } else { setTimeout(check, 1000); } }; check(); }); } waitForLoaderToDisappear().then(() => { main(); }); function main() { const products = []; const productContainers = document.querySelectorAll("[class*=lineItemContainer]"); function getSKU(productContainer) { const skuFormatted = productContainer.querySelector('[data-test-id="product-sku"]').textContent; const sku = skuFormatted.split(":")[1].trim(); return sku; } function getPricePerUnit(productContainer) { function getCurrentPrice() { const productCard = productContainer; const priceText = productCard.querySelector(`[class*=productPrice_mainPriceWrapper] p`).textContent; return getNumericPriceFromString(priceText); // function parsePrice(str) { // // Extract only digits, commas, dots // const numberPart = str.match(/[\d.,]+/g)?.join('') || ""; // // If the number uses comma as decimal separator (European format) // // Detect by checking last comma position // const hasCommaAsDecimal = numberPart.includes(',') && // numberPart.lastIndexOf(',') > numberPart.lastIndexOf('.'); // let normalized = numberPart; // if (hasCommaAsDecimal) { // // Remove thousand separators and convert final comma to dot // normalized = normalized.replace(/\./g, ""); // remove thousand dots // normalized = normalized.replace(/,/g, "."); // convert decimal comma // } else { // // Normal format: remove thousand separators (commas) // normalized = normalized.replace(/,/g, ""); // } // return parseFloat(normalized); // } // return parsePrice(priceText); } function getCurrentQuantity() { const productCard = productContainer; const currentQuantity = Number(productCard.querySelector(`[data-test-id="product-quantity"]`).value); return currentQuantity; } const currentPrice = getCurrentPrice(); const currentQuantity = getCurrentQuantity(); const pricePerUnit = currentPrice / currentQuantity; return Number(pricePerUnit.toFixed(2)); } Array.from(productContainers).forEach(productContainer => { // https://zoovu.atlassian.net/browse/COE-7225 const productIsAGift = Boolean(productContainer.querySelector("[class*=gift]")); if (!productIsAGift) { products.push({ sku: getSKU(productContainer), pricePerUnit: getPricePerUnit(productContainer) }) } }) sessionStorage.setItem("zoovu-cart-product-prices", JSON.stringify(products)); } /* */}},"fields":[]},{"id":"353e6fc3-b2fc-4125-905f-ab995c9af0a3","rowType":"EVENT","trigger":{"id":"8a545127-e6ee-4e47-96b4-4c990422f08b","name":"[Bosch PT] Category link was clicked on search autosuggestions","type":"CLICK","groups":[{"id":"4e525821-47a3-4bbf-9af9-27a16918d7f2","rows":[{"valueType":"BOOLEAN","id":"116c3f5a-225c-4d29-b2fd-ef8a8f4e478a","variableId":"e22afd9c-c6f7-4e8f-8413-31ded27641a3","value":"true","operator":"EQUALS"}]}]},"action":{"type":"CLICKOUT"},"fields":[{"id":"7adf4a5c-b6c6-4fad-8557-a9309394ecc2","variableId":"97966e49-2e80-42f2-ab9b-65a4d909d336","fieldName":"locale"},{"id":"6b558d96-200b-404c-8acf-44a8fb64df1b","variableId":"98de71ca-516e-4a2b-a7af-32fd8de785e5","fieldName":"targetUrl"},{"id":"d09e22a3-9641-47b3-a7da-f721a7f01725","variableId":"6c77635c-e95a-4eca-8086-f6be1b1193e8","fieldName":"namedReferral"},{"id":"961db7bb-6842-4595-b68a-6bd35c574f40","variableId":"3b881070-bf08-4869-be93-2b5d94a99c5b","fieldName":"eventLabel"}]},{"id":"02a82123-f971-44fe-ba5b-3f8aac14d541","rowType":"EVENT","trigger":{"id":"e108d62c-bbec-4725-8dab-ca6542e25c4e","name":"[Bosch PT] On a thank-you page","type":"PAGE_LOAD","groups":[{"id":"527cb3bb-c529-4bc7-ac64-39e29f2282c3","rows":[{"valueType":"TEXT","id":"b51ed31d-a090-4e3d-bbb5-cb46b2162111","variableId":"75cf8fd1-fafe-44be-9806-23ff3017c9c1","value":"confirmation","operator":"CONTAINS"}]}]},"action":{"type":"PURCHASED"},"fields":[{"id":"d43ff7e8-7fe3-44ee-ba50-ff67d50d071b","variableId":"3194c5d8-d8f5-42c7-b90a-d38b1446b4b1","fieldName":"products"},{"id":"5277fbce-8dbb-4df2-9c90-b43824ef4853","variableId":"b6d9960e-8afc-4d54-b00f-80bfb9409f8b","fieldName":"transactionId"},{"id":"4df98d57-0a1b-4789-85d6-f22373b96016","variableId":"dafa3f7c-a241-4d3a-ac9c-389eb07d93bd","fieldName":"currencyCode"}]},{"id":"2db68159-9e79-4705-9a17-47246536a891","rowType":"EVENT","trigger":{"id":"f73f4b15-91b3-4f67-ba02-db598f03e5c8","name":"[Bosch PT] Product was clicked on autosuggestions","type":"CLICK","groups":[{"id":"d1b6fdcf-227d-44b2-ada3-ae3716822519","rows":[{"valueType":"BOOLEAN","id":"a9fd50de-7834-4c9b-b920-611acf521591","variableId":"d3e5945a-8e55-4b96-b963-ccbc71ad4352","value":"true","operator":"EQUALS"}]}]},"action":{"type":"CLICKOUT"},"fields":[{"id":"5730e810-8027-43a6-be94-0cfaf1ae855f","variableId":"97966e49-2e80-42f2-ab9b-65a4d909d336","fieldName":"locale"},{"id":"225fad75-a298-47a9-97a5-9f2b10a1fc17","variableId":"98de71ca-516e-4a2b-a7af-32fd8de785e5","fieldName":"targetUrl"},{"id":"e30408de-df81-42cc-b142-95d055edb35d","variableId":"6c77635c-e95a-4eca-8086-f6be1b1193e8","fieldName":"namedReferral"},{"id":"e2e6fa8b-9b48-4968-9600-9072354e704f","variableId":"2e141436-b43c-40d6-bbed-64cfdca7f222","fieldName":"eventLabel"}]},{"id":"f84cea58-0521-40b3-aafa-2bd98b9d7ebc","rowType":"EVENT","trigger":{"id":"dbb640fb-b6ae-4376-8766-1d852b69fa2f","name":"[Bosch PT] Search input was used","type":"INPUT","delayMs":2500,"groups":[{"id":"84d8547b-4787-4929-b7fc-4367e276f70e","rows":[{"valueType":"BOOLEAN","id":"03f8d18f-db03-404e-8101-d7d3d1ca1355","variableId":"c381b7a3-3dd3-4100-9c41-e85911343723","value":"true","operator":"EQUALS"}]}]},"action":{"type":"SEARCH"},"fields":[{"id":"3850d6c6-d10a-4be7-bbbf-e0ca7e0604f0","variableId":"97966e49-2e80-42f2-ab9b-65a4d909d336","fieldName":"locale"}]},{"id":"4fd2e46e-1ab8-4af5-b7b1-282d0b042d43","rowType":"EVENT","trigger":{"id":"93db58bf-0211-444e-b5c4-c28d2136bb1b","name":"[Bosch PT] Visiting search result page","type":"PAGE_LOAD","delayMs":3000,"groups":[{"id":"6e059508-74e1-4102-b4ce-91a33187b1ac","rows":[{"valueType":"TEXT","id":"03eaa643-b3ae-42db-bfae-4123e31ed0d3","variableId":"75cf8fd1-fafe-44be-9806-23ff3017c9c1","value":"?q=","operator":"CONTAINS"}]}]},"action":{"type":"SEARCH_RESULT"},"fields":[{"id":"568bcf2e-0737-45cd-9935-3cb233d0f252","variableId":"97966e49-2e80-42f2-ab9b-65a4d909d336","fieldName":"locale"},{"id":"d94b3a76-31de-4d54-83b9-9ee395e6a321","variableId":"f2c8b929-18d6-4fa1-9c48-53dcdfe223d5","fieldName":"isEmpty"},{"id":"8b0e8945-2ec3-4ac9-8936-0695755e0bb8","variableId":"cc08371f-f1bd-4513-9179-7ddb9dda4fd1","fieldName":"skus"},{"id":"7547cf92-e4f8-456e-90c7-95522a61cf90","variableId":"e7fe10dc-2909-4f60-974c-a1ed812a0506","fieldName":"eventLabel"}]},{"id":"2d9ba673-df54-40d0-b167-2f9451193472","rowType":"EVENT","trigger":{"id":"2abd9f72-cd80-4a47-b2c8-20687a7efc44","name":"[Bosch PT] Search clickout","type":"CLICK","groups":[{"id":"d2c7c44d-80f3-491c-b41e-04eb1f7be402","rows":[{"valueType":"BOOLEAN","id":"adf82313-e403-429b-bef3-14fc8aa5fa18","variableId":"9219e29f-45ce-4e95-884b-c97151c7ee88","value":"true","operator":"EQUALS"},{"valueType":"TEXT","id":"dc0986f0-402c-48dc-907f-57d63fc251cd","variableId":"75cf8fd1-fafe-44be-9806-23ff3017c9c1","value":"?q=","operator":"CONTAINS"}]}]},"action":{"type":"CLICKOUT"},"fields":[{"id":"5b56d130-7d0f-4fb2-b830-d610a43fca44","variableId":"97966e49-2e80-42f2-ab9b-65a4d909d336","fieldName":"locale"},{"id":"446818ab-bb0d-4b80-8f46-0488da191c8b","variableId":"baa79840-995a-45a3-8f9d-1d2a1983b109","fieldName":"targetUrl"},{"id":"c8809ffb-9539-4b2c-8b2a-9e1774284a31","variableId":"6c77635c-e95a-4eca-8086-f6be1b1193e8","fieldName":"namedReferral"},{"id":"dc8a1bd7-e327-4554-916d-c98a70f6fd22","variableId":"2e141436-b43c-40d6-bbed-64cfdca7f222","fieldName":"eventLabel"}]},{"id":"4451b93e-507b-4956-9213-74e330388f8d","rowType":"CUSTOM_ACTION","trigger":{"id":"91fac70f-18cc-497f-b3f6-4632951f77e6","name":"[Bosch PT] Visiting PDP (whether it has product selected or not)","type":"PAGE_LOAD","groups":[{"id":"b95b1078-b8e6-4dbd-b3a2-1bffce166e60","rows":[{"valueType":"BOOLEAN","id":"50b60b3c-f79d-4359-a748-51617a5d39f0","variableId":"90055979-6dde-4e58-919a-7e7db009aae3","value":"true","operator":"EQUALS"}]}]},"action":{"id":"1921ef2a-35e4-4215-b511-0f1dfe6e1e3b","type":"CUSTOM","code":function(event){// The reason setTimeout is necessary is because otherwise this SKU will be checked after a new one is saved // This way ensures we first check the SKU in PDP_VISITED and only afterwards we save new SKU setTimeout(() => { const sku = window.ZoovuDIR.getCurrentSku(); sessionStorage.setItem("zoovu-last-selected-sku", sku); }, 500) /* */}},"fields":[]},{"id":"7139da3b-9353-4ca3-be6a-3265744f4853","rowType":"EVENT","trigger":{"id":"510ea37f-195f-4c9f-ad94-9f4aa528c519","name":"[Bosch PT] Visiting PDP","type":"PAGE_LOAD","groups":[{"id":"9da0cc36-5ff8-44da-9f9c-1febbb673e56","rows":[{"valueType":"BOOLEAN","id":"e3c43f55-edc6-4fc5-8875-54f9a048619d","variableId":"ce635408-b5eb-48e4-9b48-949d83c92b29","value":"true","operator":"EQUALS"}]}]},"action":{"type":"PDP_VISITED"},"fields":[{"id":"a29ad3fb-c3fa-4690-8b1c-78a703d59a81","variableId":"97966e49-2e80-42f2-ab9b-65a4d909d336","fieldName":"locale"},{"id":"6142ae71-54d9-4ac1-8a20-b5cc316e8a61","variableId":"f3dd91b8-0939-4ecf-bb28-32c8759d2f4d","fieldName":"sku"},{"id":"69a651bf-4c7e-4fba-9673-3e1038281c8f","variableId":"4d44ede1-271d-47d1-a400-2c20e70deaf6","fieldName":"price"},{"id":"1b84507c-82ea-417f-8146-22acf4f0d674","variableId":"5400d372-a64d-4866-9ea8-8311ca5616f1","fieldName":"currencyCode"}]},{"id":"b9a3852b-48f8-44cc-9b7f-176df2955412","rowType":"EVENT","trigger":{"id":"bb7e66ca-d6f4-44e5-b738-6c458a7406b3","name":"[Bosch PT] Cart quantity was changed","type":"CLICK","groups":[{"id":"c6833a3a-f2b7-4caf-83ab-dd6db1004525","rows":[{"valueType":"BOOLEAN","id":"8fe7a961-df94-49a9-ba91-5fa0b70b693d","variableId":"c648b7b3-561b-4aa3-a35e-cad90bd6aa87","value":"true","operator":"EQUALS"}]}]},"action":{"type":"UPDATE_CART"},"fields":[{"id":"4c70a68b-b29d-466d-a19f-1e5de44e47e0","variableId":"97966e49-2e80-42f2-ab9b-65a4d909d336","fieldName":"locale"},{"id":"e54c6420-9461-4d61-8654-4daea3a7cd99","variableId":"af1f6746-9b0a-467c-9eb8-fab74479a700","fieldName":"sku"},{"id":"6685f663-3704-4795-a113-8244c05d39dc","variableId":"b3d67dce-a9a9-468f-9360-d80eae8d665b","fieldName":"price"},{"id":"3868a80a-995e-4b4f-908b-032f3c00246e","variableId":"5400d372-a64d-4866-9ea8-8311ca5616f1","fieldName":"currencyCode"},{"id":"c12d7d49-fa01-4b2a-bdf9-1a837fae8aaf","variableId":"f684a5b4-3a9b-469d-a4d8-dfc15bb2c33e","fieldName":"quantity"}]},{"id":"428d3333-4f05-42a1-820d-48ff9335afa0","rowType":"EVENT","trigger":{"id":"0bb302ba-438f-47a0-8260-4575e8fb6127","name":"[Bosch PT] Product was removed from cart","type":"CLICK","groups":[{"id":"ad9a3be4-1ae8-4ee2-8ff2-83936a349eb8","rows":[{"valueType":"BOOLEAN","id":"38927020-e7f3-4e58-8b31-75655f76dfe1","variableId":"d11d4343-6f33-4ffd-9207-aa43f2677671","value":"true","operator":"EQUALS"}]}]},"action":{"type":"REMOVE_FROM_CART"},"fields":[{"id":"f7e44848-60ca-4069-a48c-23b6fbc9c5df","variableId":"97966e49-2e80-42f2-ab9b-65a4d909d336","fieldName":"locale"},{"id":"f8f0e687-f5ff-4833-81d2-a9fa5fc00c37","variableId":"af1f6746-9b0a-467c-9eb8-fab74479a700","fieldName":"sku"},{"id":"9557859c-3833-485d-a2c5-af70f93e967a","variableId":"b3d67dce-a9a9-468f-9360-d80eae8d665b","fieldName":"price"},{"id":"633b5874-bf24-44bb-8034-7f1dccc480c2","variableId":"5400d372-a64d-4866-9ea8-8311ca5616f1","fieldName":"currencyCode"},{"id":"a5273b82-c665-4127-b731-e0cb8b0454df","variableId":"bf72883a-4663-41ed-baf6-584175490328","fieldName":"quantity"}]},{"id":"6a9cddec-c081-4be8-b6a3-8460d2b173d2","rowType":"EVENT","trigger":{"id":"4ee86f61-bdc2-4358-a320-002b0cc0286d","name":"[Bosch PT] ATC button on PDP clicked","type":"CLICK","groups":[{"id":"f8b1df8b-94d3-4150-87e8-4d5112fa55cd","rows":[{"valueType":"BOOLEAN","id":"5ac7d40a-6750-4e3c-a076-82965644cc60","variableId":"7a089517-a9dd-4fb4-a822-f2086593e99b","value":"true","operator":"EQUALS"}]}]},"action":{"type":"ADD_TO_CART"},"fields":[{"id":"3f94274e-8498-45b7-b026-6e617e50cae3","variableId":"97966e49-2e80-42f2-ab9b-65a4d909d336","fieldName":"locale"},{"id":"0580e106-722e-4dcf-93df-9d07506c3772","variableId":"5a950c76-96c5-41f3-8f9b-d83de69cd69b","fieldName":"sku"},{"id":"cac2be8f-d00b-425f-8702-b3d2fb3838fa","variableId":"a4097d75-887f-4947-a618-415b4f4ef370","fieldName":"price"},{"id":"5f797945-d7e0-4907-89a5-e5f99785615a","variableId":"5400d372-a64d-4866-9ea8-8311ca5616f1","fieldName":"currencyCode"},{"id":"0b2d60f0-5aea-4938-baaf-47af43cd5aee","variableId":"1330574d-7910-45d3-b50c-e0e82bfc11f8","fieldName":"quantity"}]},{"id":"90258029-541e-448e-895a-e2347d8ddf05","rowType":"EVENT","trigger":{"id":"60d5d091-48ed-4b41-b029-fc5b70e81a32","name":"[Bosch PT] Visiting a page that isn't PDP","type":"PAGE_LOAD","groups":[{"id":"649644e3-2f06-480e-880e-685f270cf839","rows":[{"valueType":"BOOLEAN","id":"b5151fb5-6b77-4b36-aa1e-014551a10cdb","variableId":"0f6a3dfb-49ad-486c-baa4-bb55bb2b1a30","value":"false","operator":"EQUALS"}]}]},"action":{"type":"PAGE_VISITED"},"fields":[{"id":"8f0530e8-de43-411e-ba3c-16551fd50416","variableId":"97966e49-2e80-42f2-ab9b-65a4d909d336","fieldName":"locale"}]}]}; const advancedCode = function(){function getCookieValue(cookieName) { const cookies = document.cookie.split('; '); for (let i = 0; i < cookies.length; i++) { const cookie = cookies[i].split('='); // Skip malformed cookies with multiple '=' signs if (cookie.length !== 2) { continue; } if (cookie[0] === cookieName) { return cookie[1]; } } return null; } function isValidCidFormat(cid) { // UUID (36 chars) or UUID_timestamp (36 + 1 + 13 = 50 chars) // Max 60 chars to allow some buffer return cid && cid.length <= 60; } function getAllCookieValues(cookieName) { const cookies = document.cookie.split("; "); const values = []; for (let i = 0; i < cookies.length; i++) { const cookie = cookies[i].split("="); // Skip malformed cookies with multiple '=' signs if (cookie.length !== 2) { continue; } if (cookie[0] === cookieName) { values.push(cookie[1]); } } return values; } function removeZoovuCidCookie() { const hostname = window.location.hostname; const domain = hostname; // Set expiration to past date to remove cookie document.cookie = `zoovu-cid=; path=/; domain=.${domain}; expires=Thu, 01 Jan 1970 00:00:00 GMT`; document.cookie = `zoovu-cid=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`; } function removeMalformedZoovuCidCookies() { const cookies = document.cookie.split("; "); let hasMalformed = false; // Check for structurally malformed cookies (multiple '=' signs) for (let i = 0; i < cookies.length; i++) { const parts = cookies[i].split("="); if (parts[0] === "zoovu-cid" && parts.length !== 2) { hasMalformed = true; break; } } // Check for invalid CID formats in properly structured cookies if (!hasMalformed) { const allCids = getAllCookieValues("zoovu-cid"); hasMalformed = allCids.some((cid) => !isValidCidFormat(cid)); } if (hasMalformed) { console.debug("[Zoovu Tracking] Found malformed CID cookies, removing them"); removeZoovuCidCookie(); } } removeMalformedZoovuCidCookies(); function getCurrentSku() { const firstPdp = document.querySelector("#product-detail-stage"); const secondPdp = document.querySelector(".o-gt-stage"); const secondPdpFilter = document.querySelector(".o-gt-stage__filter[data-sku]"); const secondPdpSelectedRow = document.querySelector(".variant-table__wrapper .row-selected:not(.hidden)"); if (firstPdp) { return firstPdp.getAttribute("data-sku") || "VARIANT_NOT_SELECTED"; } else if (secondPdp) { if (secondPdpSelectedRow) { return secondPdpSelectedRow.getAttribute("id").split("-")[1]; } if (secondPdpFilter) { return secondPdpFilter.getAttribute("data-sku"); } return secondPdp.getAttribute("data-sku") || "VARIANT_NOT_SELECTED"; } } function getPreviousSku() { return sessionStorage.getItem("zoovu-last-selected-sku"); } // Ensure ZoovuDIR exists window.ZoovuDIR = window.ZoovuDIR || {}; window.ZoovuDIR.getCurrentSku = getCurrentSku; window.ZoovuDIR.getPreviousSku = getPreviousSku;}; const url = 'https://queue-propagator.zoovu.com'; const currentEnvironment = 'orca'; const currentAccountId = 2729; const currencies = ['FJD', 'MXN', 'STD', 'SCR', 'LVL', 'CDF', 'BBD', 'HNL', 'UGX', 'ZAR', 'MXV', 'STN', 'CUC', 'BSD', 'SDD', 'SDG', 'IQD', 'CUP', 'GMD', 'TWD', 'RSD', 'UYI', 'MYR', 'FKP', 'XOF', 'UYU', 'CVE', 'OMR', 'SEK', 'KES', 'BTN', 'GNF', 'MZN', 'MZM', 'SVC', 'ARS', 'QAR', 'NLG', 'IRR', 'XPD', 'XPF', 'THB', 'UZS', 'BDT', 'LYD', 'KWD', 'XPT', 'RUB', 'ISK', 'BEF', 'MKD', 'RUR', 'DZD', 'PAB', 'SGD', 'KGS', 'XAD', 'XAF', 'XAG', 'HRK', 'ITL', 'CHF', 'ATS', 'CHE', 'DJF', 'TZS', 'XAU', 'ADP', 'VND', 'AUD', 'CHW', 'KHR', 'XBA', 'IDR', 'KYD', 'XBC', 'XBB', 'SHP', 'BWP', 'CYP', 'XBD', 'TJS', 'AED', 'RWF', 'DKK', 'ZWD', 'BGL', 'BGN', 'MMK', 'SYP', 'NOK', 'ZWG', 'ZWL', 'ZWN', 'YUM', 'LKR', 'ZWR', 'CZK', 'IEP', 'XCD', 'GRD', 'HTG', 'XSU', 'AFA', 'XCG', 'BHD', 'SIT', 'PTE', 'KZT', 'SZL', 'YER', 'AFN', 'BYB', 'AWG', 'NPR', 'MNT', 'GBP', 'BYN', 'XTS', 'HUF', 'BYR', 'BIF', 'XUA', 'XDR', 'BZD', 'MOP', 'NAD', 'SKK', 'TMM', 'PEN', 'WST', 'TMT', 'FRF', 'CLF', 'GTQ', 'CLP', 'TND', 'SLE', 'SLL', 'AYM', 'XFO', 'DOP', 'KMF', 'XFU', 'GEL', 'MAD', 'AZM', 'TOP', 'AZN', 'PGK', 'UAH', 'ERN', 'TPE', 'MRO', 'CNY', 'MRU', 'BMD', 'PHP', 'XXX', 'PYG', 'JMD', 'GWP', 'ESP', 'COP', 'USD', 'COU', 'USN', 'ETB', 'VEB', 'USS', 'VED', 'VEF', 'SOS', 'VUV', 'LAK', 'BND', 'ZMK', 'LRD', 'ALL', 'GHC', 'MTL', 'VES', 'ZMW', 'TRL', 'ILS', 'GHS', 'KPW', 'GYD', 'BOB', 'MDL', 'AMD', 'TRY', 'LBP', 'JOD', 'HKD', 'EUR', 'LSL', 'CAD', 'BOV', 'EEK', 'MUR', 'ROL', 'GIP', 'RON', 'NGN', 'CRC', 'PKR', 'ANG', 'SRD', 'SAR', 'TTD', 'LTL', 'MVR', 'SRG', 'INR', 'KRW', 'JPY', 'AOA', 'PLN', 'SBD', 'CSD', 'LUF', 'MWK', 'MGA', 'FIM', 'DEM', 'MGF', 'BAM', 'EGP', 'SSP', 'NIO', 'NZD', 'BRL'] // ------------------------- API ------------------------- let trackingEnabled = true; let hasLauncher = typeof advancedCode === 'function' && advancedCode.toString().includes('/behavioral-launchers-script'); let signalReady = false; let signalReadyPromise = null; let signalCheckAttempts = 0; const MAX_SIGNAL_CHECK_ATTEMPTS = 20; // 20 attempts const SIGNAL_CHECK_INTERVAL_MS = 200; // Check every 200ms (total 4 seconds max wait) let eventQueue = []; function disableTracking() { trackingEnabled = false; } function enableTracking() { trackingEnabled = true; } function getNumericPriceFromString(stringPrice) { const parsedPrice = stringPrice .replace(/\.$/, '') .replace(/([^.',\s\d])*/g, '') .replace(/([.',\s](?=\d{3}))/g, '') .replace(/([.',](?=\d{2}))/g, '.') .trim() .replace(/\s+/g, '') .replace(/\.+(?=\.)/g, '') .replace(/\.$/, ''); return Number(parsedPrice); } // helper for loading launcher from advancedmode function loadLauncher(launcherUrl, callback, id) { if (launcherUrl) { const script = document.createElement('script'); script.type = 'text/javascript'; script.src = launcherUrl; script.id = id || 'zv-launcher'; script.onload = callback; document.head.appendChild(script); } } function setSignalReady() { signalReady = true; if (eventQueue.length > 0) { console.debug(`Signal ready - flushing ${eventQueue.length} queued events`); const eventsToFlush = [...eventQueue]; eventQueue = []; eventsToFlush.forEach(queuedEvent => { sendEventImmediately(queuedEvent); }); } } if (typeof window !== 'undefined') { if (typeof window.ZoovuTrackingManager === 'undefined') { window.ZoovuTrackingManager = {}; } window.ZoovuTrackingManager.setSignalReady = setSignalReady; } class TrackingExecutionError extends Error { constructor(message, trackingEntityType, variableId, triggerId, scriptId, trackingErrorType) { super(message); this.trackingEntityType = trackingEntityType; this.variableId = variableId; this.triggerId = triggerId; this.scriptId = scriptId; this.trackingErrorType = trackingErrorType; } } // ------------------------- helpers ------------------------- const eventTypes = Object.freeze({ PAGE_VISITED: "PAGE_VISITED", ADD_TO_CART: "ADD_TO_CART", REMOVE_FROM_CART: "REMOVE_FROM_CART", PDP_VISITED: "PDP_VISITED", PURCHASED: "PURCHASED", UPDATE_CART: "UPDATE_CART", DECLINE_TRACKING: "DECLINE_TRACKING", SEARCH: "SEARCH", SEARCH_RESULT: "SEARCH_RESULT", CLICKOUT: "CLICKOUT", LEAD_GEN: "LEAD_GEN" }); const trackingEventTypes = Object.freeze({ TRACKING_SUCCESSFUL_EXECUTION: 'TRACKING_SUCCESSFUL_EXECUTION', TRACKING_EXECUTION_FAILURE: 'TRACKING_EXECUTION_FAILURE' }) const actionTypes = Object.freeze({ ...eventTypes, CUSTOM: 'CUSTOM' }) const triggerTypes = Object.freeze({ CLICK: 'CLICK', INPUT: 'INPUT', PAGE_LOAD: 'PAGE_LOAD' }); const variableTypes = Object.freeze({ FUNCTION: 'FUNCTION', CONSTANT: 'CONSTANT' }); const trackingEntityTypes = Object.freeze({ VARIABLE: 'VARIABLE', TRIGGER: 'TRIGGER', CUSTOM_ACTION: 'CUSTOM_ACTION' }); const trackingErrorTypes = Object.freeze({ MISSING_VALUE: 'MISSING_VALUE', TYPE_MISMATCH: 'TYPE_MISMATCH', UNSUPPORTED_VARIABLE: 'UNSUPPORTED_VARIABLE', MISSING_VARIABLE: 'MISSING_VARIABLE', OTHER: 'OTHER' }); const trackingVariableScopes = Object.freeze({ GLOBAL: 'GLOBAL', LOCAL: 'LOCAL' }); const namedReferrals = Object.freeze({ SEARCH: 'SEARCH' }) const getElementAttribute = (target, attributeName) => { const attribute = target[attributeName]; if (typeof attribute === 'string') { return attribute; } if (attribute && attribute.baseVal !== undefined) { return attribute.baseVal; } return target.getAttribute(attributeName === 'className' ? 'class' : attributeName) || ''; }; const getElementId = (target) => { return getElementAttribute(target, 'id'); }; const getElementClassName = (target) => { return getElementAttribute(target, 'className'); }; const matchers = Object.freeze({ EQUALS: (a, b) => a === b, DOES_NOT_EQUAL: (a, b) => a !== b, CONTAINS: (a, b) => a.indexOf(b) >= 0, DOES_NOT_CONTAIN: (a, b) => a.indexOf(b) === -1, GREATER_THAN: (a, b) => a > b, LESS_THAN: (a, b) => a < b, GREATER_THAN_OR_EQUAL: (a, b) => a >= b, LESS_THAN_OR_EQUAL: (a, b) => a <= b, MATCHES_CSS_SELECTOR: (target, test) => target.matches(test), MATCHES_ID: (target, test) => getElementId(target) === test, CONTAINS_ID: (target, test) => getElementId(target).includes(test), DOES_NOT_CONTAIN_ID: (target, test) => !getElementId(target).includes(test), MATCHES_CLASS: (target, test) => getElementClassName(target) === test, CONTAINS_CLASS: (target, test) => getElementClassName(target).includes(test), DOES_NOT_CONTAIN_CLASS: (target, test) => !getElementClassName(target).includes(test), MATCHES_REGEX: (a, b) => parseRegexString(b).test(a), }); const trackingFieldName = `${domainId}_${zoovuId}_trackingExecutions` const MAX_RECORDS = 100 // helper to retrieve variableId later let eventFields = [] if (!JSON.parse(localStorage.getItem(trackingFieldName))) { localStorage.setItem(trackingFieldName, JSON.stringify([])) } function parseRegexString(input) { const trimmed = input.trim(); // check if it starts and ends with slashes (e.g., /^abc$/gi) const match = trimmed.match(/^\/(.+)\/([a-z]*)$/i); if (match) { const pattern = match[1]; const flags = match[2]; return new RegExp(pattern, flags); } // no flags return new RegExp(trimmed); } function checkValue(type, value) { switch (type) { case 'BOOLEAN': return value === true || value === 'true'; case 'TEXT': return value; case 'DECIMAL': return typeof value === 'string' ? parseFloat(value) : value; case 'INTEGER': return typeof value === 'string' ? parseInt(value) : value; default: return value; } } function cast(type, value) { switch (type) { case 'BOOLEAN': return value === true || value === 'true'; case 'TEXT': return value; case 'DECIMAL': return typeof value === 'string' ? parseFloat(value) : value; case 'INTEGER': return typeof value === 'string' ? parseInt(value) : value; default: return value; } } function debounce(fn, delay, useTrailing = false) { if (useTrailing) { // Trailing debounce for INPUT triggers - only fires once after delay using requestAnimationFrame let lastCallTime = 0; let animationFrameId = null; let hasScheduledExecution = false; function checkAndExecute(context, args) { const currentTime = performance.now(); const timeSinceLastCall = currentTime - lastCallTime; if (timeSinceLastCall >= delay) { // Delay has passed, execute the function fn.apply(context, args); hasScheduledExecution = false; } else { // Keep checking until delay has passed animationFrameId = requestAnimationFrame(() => { checkAndExecute(context, args); }); } } return function () { const context = this, args = arguments; lastCallTime = performance.now(); if (!hasScheduledExecution) { hasScheduledExecution = true; animationFrameId = requestAnimationFrame(() => { checkAndExecute(context, args); }); } }; } // Original requestAnimationFrame-based debounce for other triggers let lastCallTime = 0; let animationFrameId = null; let isScheduled = false; function execute(context, args, currentTime) { if (currentTime - lastCallTime >= delay) { fn.apply(context, args); isScheduled = false; } else { animationFrameId = requestAnimationFrame((newTime) => { execute(context, args, newTime); }); } } return function () { const context = this, args = arguments; lastCallTime = performance.now(); if (!isScheduled) { isScheduled = true; animationFrameId = requestAnimationFrame((currentTime) => { execute(context, args, currentTime); }); } }; } function getOrganization() { return zoovuId; } function getDomainId() { return domainId; } function getTimezone() { return Intl.DateTimeFormat().resolvedOptions().timeZone; } function getReferrer() { const referrer = document.referrer; if (/^(https?|android-app):\/\//i.test(referrer)) { return referrer; } else { return undefined; } } function getPath() { return window.location.href.replace(window.location.origin, ''); } function getCookieValue(cookieName) { const cookies = document.cookie.split('; '); for (let i = 0; i < cookies.length; i++) { const cookie = cookies[i].split('='); // Skip malformed cookies with multiple '=' signs if (cookie.length !== 2) { continue; } if (cookie[0] === cookieName) { return cookie[1]; } } return null; } function getAllCookieValues(cookieName) { const cookies = document.cookie.split('; '); const values = []; for (let i = 0; i < cookies.length; i++) { const cookie = cookies[i].split('='); // Skip malformed cookies with multiple '=' signs if (cookie.length !== 2) { continue; } if (cookie[0] === cookieName) { values.push(cookie[1]); } } return values; } function generateUuid() { return self.crypto.randomUUID(); } function isValidCidFormat(cid) { // UUID (36 chars) or UUID_timestamp (36 + 1 + 13 = 50 chars) // Max 60 chars to allow some buffer return cid && cid.length <= 60; } let cachedRegistrableDomain = null; function getRegistrableDomain(hostname) { if (cachedRegistrableDomain) { return cachedRegistrableDomain; } const parts = hostname.split('.'); if (parts.length <= 2) { cachedRegistrableDomain = hostname; return hostname; } // Probe from broadest to narrowest domain level. // Browsers refuse to set cookies on public suffixes (e.g. "co.uk"), // so the first level that accepts a cookie is the registrable domain. const probe = '__zoovu_dt'; for (let i = 2; i <= parts.length; i++) { const candidate = parts.slice(-i).join('.'); document.cookie = `${probe}=1; domain=.${candidate}; path=/`; if (getCookieValue(probe) !== null) { // Clean up the probe cookie document.cookie = `${probe}=; domain=.${candidate}; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`; cachedRegistrableDomain = candidate; return candidate; } } // Fallback – should rarely be reached cachedRegistrableDomain = parts.slice(-2).join('.'); return cachedRegistrableDomain; } function removeZoovuCidCookie() { const hostname = window.location.hostname; const domain = getRegistrableDomain(hostname); // Set expiration to past date to remove cookie document.cookie = `zoovu-cid=; path=/; domain=.${domain}; expires=Thu, 01 Jan 1970 00:00:00 GMT`; document.cookie = `zoovu-cid=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`; } function removeMalformedZoovuCidCookies() { const cookies = document.cookie.split('; '); let hasMalformed = false; // Check for structurally malformed cookies (multiple '=' signs) for (let i = 0; i < cookies.length; i++) { const parts = cookies[i].split('='); if (parts[0] === 'zoovu-cid' && parts.length !== 2) { hasMalformed = true; break; } } // Check for invalid CID formats in properly structured cookies if (!hasMalformed) { const allCids = getAllCookieValues('zoovu-cid'); hasMalformed = allCids.some(cid => !isValidCidFormat(cid)); } if (hasMalformed) { console.debug('[Zoovu Tracking] Found malformed CID cookies, removing them'); removeZoovuCidCookie(); } } function addZoovuCidToCookies() { const uuid = generateUuid(); const hostname = window.location.hostname; const domain = getRegistrableDomain(hostname); const zoovuCid = `zoovu-cid=${uuid}; path=/; domain=.${domain}`; document.cookie = zoovuCid; return uuid; } function getCID() { removeMalformedZoovuCidCookies(); // Check if we have a valid cookie const existingCid = getCookieValue(`zoovu-cid`); if (existingCid !== null && isValidCidFormat(existingCid)) { return existingCid; } // No valid cookie exists, create a new one return addZoovuCidToCookies(); } function getEnvironment() { return currentEnvironment; } function getAccountId() { return currentAccountId; } function getPropertyTypeError(propertyName, expectedType, currentType) { return `TYPE MISMATCH: ${propertyName}: should be ${expectedType} but is ${currentType}` } function typeCheckValue(value, variableType, propertyName, variableId, triggerId) { let errorMessage = ''; if (value === undefined || value === null) { throw new TrackingExecutionError(`${propertyName}: value is not defined.`, trackingEntityTypes.VARIABLE, variableId, triggerId, script.id, trackingErrorTypes.MISSING_VALUE) } switch (variableType) { case "TEXT": if (typeof value !== 'string') errorMessage = getPropertyTypeError(propertyName, 'string', typeof value); break; case "INTEGER": case "DECIMAL": if (typeof value !== 'number') errorMessage = getPropertyTypeError(propertyName, 'number', typeof value); break; case "BOOLEAN": if (typeof value !== 'boolean') errorMessage = getPropertyTypeError(propertyName, 'boolean', typeof value); break; case 'LIST': if (!Array.isArray(value)) errorMessage = getPropertyTypeError(propertyName, 'list', typeof value); break; case 'EVENT': break; default: throw new TrackingExecutionError(`Not supported variable type: ${variableType}`, trackingEntityTypes.VARIABLE, variableId, triggerId, script.id, trackingErrorTypes.UNSUPPORTED_VARIABLE) } if (errorMessage) { throw new TrackingExecutionError(errorMessage, trackingEntityTypes.VARIABLE, variableId, triggerId, script.id, trackingErrorTypes.TYPE_MISMATCH) } } function ensureRequiredFieldsPresent(fields, eventType) { return Object.entries(fields).every(([key, value]) => { if (!value) { const variableId = eventFields.find(field => field.fieldName === key).variableId; // empty eventFields eventFields = []; throw new TrackingExecutionError( `Required property ${key} missing value for ${eventType}.`, trackingEntityTypes.VARIABLE, variableId, null, script.id, trackingErrorTypes.MISSING_VALUE ) } return true; }); } function getBaseEventBody(eventType) { return { origin: 'CLIENT', queryParams: {}, organization: getOrganization(), domainId: getDomainId(), path: getPath(), referrer: getReferrer(), cid: getCID(), timezone: getTimezone(), // dynamic eventType: eventType ?? '', eventLabel: '', }; } function getEventExecutable(actionType) { switch (actionType) { case actionTypes.PAGE_VISITED: return sendPageVisitedEvent; case actionTypes.ADD_TO_CART: return sendAddToCartEvent; case actionTypes.CLICKOUT: return sendClickoutEvent; case actionTypes.REMOVE_FROM_CART: return sendRemoveFromCartEvent; case actionTypes.PDP_VISITED: return sendPdpVisitedEvent; case actionTypes.PURCHASED: return sendPurchaseEvent; case actionTypes.DECLINE_TRACKING: return sendDeclineTrackingEvent; case actionTypes.UPDATE_CART: return sendUpdateCartEvent; case actionTypes.SEARCH: return sendSearchEvent; case actionTypes.SEARCH_RESULT: return sendSearchResultEvent; case actionTypes.LEAD_GEN: return sendLeadGenEvent; } } function checkSpecificFieldValues(fieldName, value, variableId) { if (fieldName === 'currencyCode' && !currencies.includes(value)) { throw new TrackingExecutionError( `Value ${value} is not compatible with currencyCode.`, trackingEntityTypes.VARIABLE, variableId, null, script.id, trackingErrorTypes.OTHER ) } if (fieldName === 'namedReferral' && !namedReferrals[value.toUpperCase()]) { throw new TrackingExecutionError( `Value ${value} is not compatible with namedReferral.`, trackingEntityTypes.VARIABLE, variableId, null, script.id, trackingErrorTypes.OTHER ) } } function getVariableValueById(variableId, event, fieldName, triggerId) { const foundVariable = variables.find(v => v.id === variableId); let variableValue = null; if (foundVariable) { if (foundVariable.type === variableTypes.CONSTANT) { variableValue = cast(foundVariable.valueType, foundVariable.value); } else if (foundVariable.type === variableTypes.FUNCTION && typeof foundVariable.function === 'function') { try { variableValue = foundVariable.function(event); } catch (e) { throw new TrackingExecutionError( e.message, trackingEntityTypes.VARIABLE, variableId, triggerId, script.id, trackingErrorTypes.OTHER ) } } else { throw new TrackingExecutionError( `${foundVariable.type} is not supported.`, trackingEntityTypes.VARIABLE, variableId, triggerId, script.id, trackingErrorTypes.UNSUPPORTED_VARIABLE ) } typeCheckValue(variableValue, foundVariable.valueType, fieldName ? fieldName : foundVariable.name, variableId, triggerId); if (fieldName === 'currencyCode' || fieldName === 'namedReferral') { checkSpecificFieldValues(fieldName, variableValue, variableId) } return variableValue; } else { throw new TrackingExecutionError( `No variable with ID ${variableId}`, trackingEntityTypes.VARIABLE, variableId, triggerId, script.id, trackingErrorTypes.MISSING_VARIABLE ) } } function evaluateSingleTriggerRow(row, event, triggerId) { const target = getVariableValueById(row.variableId, event, undefined, triggerId); const matcher = matchers[row.operator]; checkValue(row.valueType, row.value); const value = cast(row.valueType, row.value); if (matcher && typeof matcher === 'function') { return matcher(target, value); } return false; } function preparePayloadFromFields(fields, event) { const payload = {}; fields.forEach(f => { payload[f.fieldName] = getVariableValueById(f.variableId, event, f.fieldName) }) return payload; } function evaluateTriggerConditions(trigger, event) { // OR between groups return trigger.groups.some(group => // AND between rows group.rows.every(row => evaluateSingleTriggerRow(row, event, trigger.id))) } function runAction(action, fields, event, trigger) { const trackingExecutions = jsonParser(trackingFieldName) if (action.type === actionTypes.CUSTOM && typeof action.code === 'function') { // add script custom action trackingExecutions.push(createTrackingSuccessfulExecutionRecord(trackingEntityTypes.CUSTOM_ACTION, action.id)) try { action.code(event); } catch (e) { throw new TrackingExecutionError( e.message, trackingEntityTypes.CUSTOM_ACTION, action.id, null, script.id, trackingErrorTypes.OTHER ) } localStorage.setItem(trackingFieldName, JSON.stringify(trackingExecutions)); } else { const sendEventFunction = getEventExecutable(action.type); eventFields = [...fields]; if (sendEventFunction && typeof sendEventFunction === 'function') { const payload = preparePayloadFromFields(fields, event) sendEventFunction(payload); addSuccessfulExecutions(trigger, fields) } } } // Store debounced functions per rule to reuse them const debouncedRuleActions = new Map(); let ruleCounter = 0; function evaluateSingleRule(rule, event) { // Assign unique ID to rule if it doesn't have one if (!rule._uniqueId) { rule._uniqueId = ruleCounter++; } if (evaluateTriggerConditions(rule.trigger, event)) { // Set default delay for INPUT triggers if delayMs is undefined const delay = rule.trigger.type === triggerTypes.INPUT && rule.trigger.delayMs === undefined ? 1500 : rule.trigger.delayMs; if (delay) { // Get or create debounced function for this rule using unique rule ID const ruleKey = rule._uniqueId; if (!debouncedRuleActions.has(ruleKey)) { const useTrailing = rule.trigger.type === triggerTypes.INPUT; const debounced = debounce(function (event) { runAction(rule.action, rule.fields, event, rule.trigger); }, delay, useTrailing); debouncedRuleActions.set(ruleKey, debounced); } debouncedRuleActions.get(ruleKey)(event); } else { runAction(rule.action, rule.fields, event, rule.trigger); } } } function evaluateRules(rules, event) { try { rules.forEach(rule => { evaluateSingleRule(rule, event) }); } catch (error) { const trackingExecutions = jsonParser(trackingFieldName); const {message, trackingEntityType, variableId, triggerId, scriptId, trackingErrorType} = error; const failedExecution = createTrackingFailedExecutionRecord( trackingEntityType, variableId, triggerId, scriptId, trackingErrorType, message ) trackingExecutions.push(failedExecution) localStorage.setItem(trackingFieldName, JSON.stringify(trackingExecutions)); console.debug(error) } } function observeAndReactOnPageChange(callback) { let oldHref; if (oldHref === undefined) { callback(); oldHref = document.location.href; } const body = document.querySelector('body'); const observer = new MutationObserver(() => { if (oldHref !== document.location.href) { oldHref = document.location.href; callback(); } }); observer.observe(body, {childList: true, subtree: true}); } function executeAdvancedCode() { if (typeof advancedCode === 'function') { advancedCode(); } } function jsonParser(key) { const foundValue = JSON.parse(localStorage.getItem(key)) if (!Array.isArray(foundValue)) { throw Error('Invalid value in localStorage for key ' + key) } return foundValue } function createTrackingSuccessfulExecutionRecord(entityType, entityId) { const executionTime = Date.now(); return { organization: getOrganization(), domainId: getDomainId(), eventType: trackingEventTypes.TRACKING_SUCCESSFUL_EXECUTION, trackingEntityType: entityType, trackingEntityId: entityId, executionTime } } function createTrackingFailedExecutionRecord(entityType, entityId, triggerId, scriptId, errorType, message) { const executionTime = Date.now(); return { organization: getOrganization(), domainId: getDomainId(), eventType: trackingEventTypes.TRACKING_EXECUTION_FAILURE, trackingEntityType: entityType, trackingEntityId: entityId, executionTime, triggerId: triggerId || null, scriptId: scriptId || null, error: { type: errorType, message } } } function addSuccessfulExecutions(trigger, fields) { const successfulExecutions = jsonParser(trackingFieldName) // add script trigger successfulExecutions.push(createTrackingSuccessfulExecutionRecord(trackingEntityTypes.TRIGGER, trigger.id)) // add script function variables fields.forEach(field => { const foundVariableInField = variables.find(variable => variable.id === field.variableId) if (foundVariableInField.type === variableTypes.FUNCTION && foundVariableInField.scope === trackingVariableScopes.LOCAL) { successfulExecutions.push(createTrackingSuccessfulExecutionRecord(trackingEntityTypes.VARIABLE, foundVariableInField.id)) } }) // add trigger function variables trigger.groups.forEach(groups => { groups.rows.forEach(row => { const foundVariable = variables.find(variable => variable.id === row.variableId) if (foundVariable.type === variableTypes.FUNCTION && foundVariable.scope === trackingVariableScopes.LOCAL) { successfulExecutions.push(createTrackingSuccessfulExecutionRecord(trackingEntityTypes.VARIABLE, foundVariable.id)) } }) }) localStorage.setItem(trackingFieldName, JSON.stringify(successfulExecutions)); } // ------------------------- events ------------------------- async function sendEventImmediately(body) { if (trackingEnabled) { await fetch(`${url}/fact`, { method: 'POST', mode: 'cors', cache: 'no-cache', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', }, redirect: 'follow', referrerPolicy: 'no-referrer', body: JSON.stringify(body), }); } else { console.debug("Tracking disabled - no permission to track"); } } function getSignalReadyPromise() { // Return existing promise if already polling if (signalReadyPromise) { return signalReadyPromise; } // Signal already ready, return resolved promise if (signalReady) { return Promise.resolve(); } // Create new polling promise signalReadyPromise = new Promise((resolve) => { signalCheckAttempts = 0; const checkInterval = setInterval(() => { signalCheckAttempts++; if (signalReady) { clearInterval(checkInterval); signalReadyPromise = null; resolve(); } else if (signalCheckAttempts >= MAX_SIGNAL_CHECK_ATTEMPTS) { clearInterval(checkInterval); signalReadyPromise = null; console.debug('Signal connection timeout - proceeding without signal'); resolve(); } }, SIGNAL_CHECK_INTERVAL_MS); }); return signalReadyPromise; } async function sendEvent(body) { if (trackingEnabled) { // Check if we should wait for signal connection // Only wait if launcher exists (hasLauncher) and signal is not ready yet if (hasLauncher && !signalReady && signalCheckAttempts < MAX_SIGNAL_CHECK_ATTEMPTS) { // Queue the event - don't block, just queue it eventQueue.push(body); // Trigger polling in background (fire and forget) getSignalReadyPromise().then(() => { // Flush happens in setSignalReady, but handle timeout case if (!signalReady && eventQueue.length > 0) { const eventsToFlush = [...eventQueue]; eventQueue = []; eventsToFlush.forEach(queuedEvent => { sendEventImmediately(queuedEvent); }); } }); } else { // Send immediately if no launcher or signal already ready await sendEventImmediately(body); } } else { console.debug("Tracking disabled - no permission to track"); } } async function sendSSTRPurchaseEvent(body) { if (trackingEnabled) { const purchaseUrl = 'https://ev-co.zoovu.com/v1/CollectSalesEvent'; await fetch(purchaseUrl, { method: 'POST', mode: 'cors', cache: 'no-cache', headers: { 'Content-Type': 'application/json', }, redirect: 'follow', referrerPolicy: 'no-referrer', body: JSON.stringify(body), }); } else { console.debug("Tracking disabled - no permission to track"); } } async function sendTrackingSuccessfulExecution(body) { if (trackingEnabled) { await fetch(`${url}/tracking/executions`, { method: 'POST', mode: 'cors', cache: 'no-cache', headers: { 'Content-Type': 'application/json', }, redirect: 'follow', referrerPolicy: 'no-referrer', body: JSON.stringify(body), }); } else { console.debug("Tracking disabled - no permission to track"); } } function sendFactEventBase(eventType, eventSpecificBody) { const body = { ...getBaseEventBody(eventType), // event label + custom per-event fields ...eventSpecificBody, }; sendEvent(body); }; function sendPageVisitedEvent({locale, category, eventLabel = 'Page visit'}) { const body = { locale, eventLabel, // optional category, }; if (ensureRequiredFieldsPresent({locale}, eventTypes.PAGE_VISITED)) { sendFactEventBase(eventTypes.PAGE_VISITED, body); } }; function sendPdpVisitedEvent({ locale, sku, quantity = 1, currencyCode, eventLabel = "Product details page visit", price, category }) { const body = { locale, sku, eventLabel, // optional category, price, currencyCode, quantity, }; if (ensureRequiredFieldsPresent({locale, sku}, eventTypes.PDP_VISITED)) { sendFactEventBase(eventTypes.PDP_VISITED, body); } }; function sendAddToCartEvent({ locale, sku, quantity = 1, currencyCode, eventLabel = "Add to cart", price, category }) { const body = { locale, sku, eventLabel, // optional category, price, currencyCode, quantity, }; if (ensureRequiredFieldsPresent({locale, sku}, eventTypes.ADD_TO_CART)) { sendFactEventBase(eventTypes.ADD_TO_CART, body); } }; function sendRemoveFromCartEvent({ locale, sku, quantity = 1, currencyCode, eventLabel = "Remove from cart", price, category }) { const body = { locale, sku, eventLabel, // optional category, price, currencyCode, quantity, }; if (ensureRequiredFieldsPresent({locale, sku}, eventTypes.REMOVE_FROM_CART)) { sendFactEventBase(eventTypes.REMOVE_FROM_CART, body); } }; function sendUpdateCartEvent({ locale, sku, quantity = 1, currencyCode, eventLabel = "Update cart", price, category }) { const body = { locale, sku, eventLabel, // optional category, price, currencyCode, quantity, }; if (ensureRequiredFieldsPresent({locale, sku}, eventTypes.UPDATE_CART)) { sendFactEventBase(eventTypes.UPDATE_CART, body); } }; function sendDeclineTrackingEvent({ locale, eventLabel = "No permission to track", category }) { const body = { locale, eventLabel, // optional category }; if (ensureRequiredFieldsPresent({locale}, eventTypes.DECLINE_TRACKING)) { sendFactEventBase(eventTypes.DECLINE_TRACKING, body); disableTracking(); } }; function sendSearchEvent({ locale, eventLabel = "Search phrase typed", category }) { const body = { locale, eventLabel, // optional category, }; if (ensureRequiredFieldsPresent({locale}, eventTypes.SEARCH)) { sendFactEventBase(eventTypes.SEARCH, body); } }; function sendSearchResultEvent({ locale, isEmpty = false, eventLabel = "Search results", skus, category }) { const body = { locale, isEmpty, skus, eventLabel, // optional category }; if (ensureRequiredFieldsPresent({locale, skus}, eventTypes.SEARCH_RESULT)) { sendFactEventBase(eventTypes.SEARCH_RESULT, body); } }; function sendClickoutEvent({ locale, targetUrl, namedReferral, eventLabel = "Clickout", category }) { const body = { locale, targetUrl, namedReferral, eventLabel, // optional category }; if (ensureRequiredFieldsPresent({locale, targetUrl, namedReferral}, eventTypes.CLICKOUT)) { sendFactEventBase(eventTypes.CLICKOUT, body); } }; function sendPurchaseEvent({ currencyCode, transactionId, products }) { const sstrBody = { transactionId, products, currency: currencyCode, browserTimestamp: Date.now(), env: getEnvironment(), accountId: getAccountId(), clientId: getCID(), }; if (ensureRequiredFieldsPresent({products, currencyCode,}, eventTypes.PURCHASED)) { sendSSTRPurchaseEvent(sstrBody); } }; function sendLeadGenEvent({ locale, currencyCode, products, leadId, leadType, category, eventLabel = "Lead gen sent" }) { const body = { locale, eventLabel, // optional currencyCode, products, leadId, leadType, category }; if (ensureRequiredFieldsPresent({locale}, eventTypes.LEAD_GEN)) { sendFactEventBase(eventTypes.LEAD_GEN, body); } }; function sendSuccessfulExecutionEvent(forceSend) { const successfulExecutions = jsonParser(trackingFieldName) if (successfulExecutions.length === MAX_RECORDS || (forceSend && successfulExecutions.length > 0)) { sendTrackingSuccessfulExecution(successfulExecutions) localStorage.setItem(trackingFieldName, JSON.stringify([])) } } function exposeTrackingApi() { if (typeof window !== 'undefined') { if (typeof window.ZoovuTrackingManager === 'undefined') { window.ZoovuTrackingManager = {}; } // Public API with stable contract - these signatures won't change const sendPageVisitedEventPublic = (event) => sendPageVisitedEvent(event); const sendPdpVisitedEventPublic = (event) => sendPdpVisitedEvent(event); const sendAddToCartEventPublic = (event) => sendAddToCartEvent(event); const sendRemoveFromCartEventPublic = (event) => sendRemoveFromCartEvent(event); const sendUpdateCartEventPublic = (event) => sendUpdateCartEvent(event); const sendDeclineTrackingEventPublic = (event) => sendDeclineTrackingEvent(event); const sendSearchEventPublic = (event) => sendSearchEvent(event); const sendSearchResultEventPublic = (event) => sendSearchResultEvent(event); const sendClickoutEventPublic = (event) => sendClickoutEvent(event); const sendPurchaseEventPublic = (event) => sendPurchaseEvent(event); const sendLeadGenEventPublic = (event) => sendLeadGenEvent(event); window.ZoovuTrackingManager = { // Public API - stable contract sendPageVisitedEvent: sendPageVisitedEventPublic, sendPdpVisitedEvent: sendPdpVisitedEventPublic, sendAddToCartEvent: sendAddToCartEventPublic, sendRemoveFromCartEvent: sendRemoveFromCartEventPublic, sendUpdateCartEvent: sendUpdateCartEventPublic, sendDeclineTrackingEvent: sendDeclineTrackingEventPublic, sendSearchEvent: sendSearchEventPublic, sendSearchResultEvent: sendSearchResultEventPublic, sendClickoutEvent: sendClickoutEventPublic, sendPurchaseEvent: sendPurchaseEventPublic, sendLeadGenEvent: sendLeadGenEventPublic, // Utility methods disableTracking, enableTracking, getNumericPriceFromString, // private setSignalReady, }; } } // ------------------------- core ------------------------- function reactOnLoad() { const pageLoadRules = script.rows.filter(row => row.trigger.type === triggerTypes.PAGE_LOAD); const handler = (event) => { observeAndReactOnPageChange(() => { evaluateRules(pageLoadRules, event); sendSuccessfulExecutionEvent(); }); }; // Check if page already loaded if (document.readyState === "complete") { handler(new Event('load')); } else { window.addEventListener("load", handler, {capture: true}); } } function reactOnClick() { const clickRules = script.rows.filter(row => row.trigger.type === triggerTypes.CLICK); // click via mouse document.querySelector('body').addEventListener('mousedown', (event) => { evaluateRules(clickRules, event); sendSuccessfulExecutionEvent() }, {capture: true}); // click via enter button document.querySelector('body').addEventListener('keydown', (event) => { if (event.code === "Enter" || event.code === "NumpadEnter") { evaluateRules(clickRules, event); sendSuccessfulExecutionEvent() } }, {capture: true}); } function reactOnInput() { const inputRules = script.rows.filter(row => row.trigger.type === triggerTypes.INPUT); document.querySelector('body').addEventListener('input', function (event) { evaluateRules(inputRules, event); sendSuccessfulExecutionEvent() }, {capture: true}); } function reactOnMouseLeave() { document.addEventListener("mouseleave", function (event) { if (event.clientY <= 0 || event.clientX <= 0 || (event.clientX >= window.innerWidth || event.clientY >= window.innerHeight)) { sendSuccessfulExecutionEvent(true) } }); } // Expose API immediately - doesn't require DOM or event listeners exposeTrackingApi(); function track() { try { executeAdvancedCode(); reactOnLoad(); reactOnClick(); reactOnInput(); reactOnMouseLeave(); } catch (error) { console.debug("Tracking disabled", error); } } // RUN SCRIPT AFTER PAGE LOAD if (document.readyState === "loading" || document.readyState === "interactive") { // Loading hasn't finished yet document.addEventListener("readystatechange", (event) => { if (event.target.readyState === "complete") { track(); } }); } else { track(); } })();