(function(){ const zoovuId = '4nvUXEZ/Yhz9ABT30AqBNlBgrgmAWuIAyyb2W5CTGTw='; const domainId = 'cbbe371d-cc20-4ace-acf8-278df97a749e'; const variables = [{"id":"1eec2be6-dc91-4c4e-a443-da2f051baf5b","name":"Add to cart","type":"CONSTANT","valueType":"TEXT","value":"Add to cart","scope":"GLOBAL"},{"id":"ad6080ea-719b-4732-952f-ef4f1ce45e02","name":"Event target","type":"FUNCTION","valueType":"EVENT","function":function(event){return event.target /* */},"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":"6c77635c-e95a-4eca-8086-f6be1b1193e8","name":"Named referral: Search","type":"CONSTANT","valueType":"TEXT","value":"Search","scope":"GLOBAL"},{"id":"fda8e609-5410-4707-b86b-01184d2af597","name":"No permission to track","type":"CONSTANT","valueType":"TEXT","value":"No permission to track","scope":"GLOBAL"},{"id":"75cf8fd1-fafe-44be-9806-23ff3017c9c1","name":"Page URL","type":"FUNCTION","valueType":"TEXT","function":function(event){return window.location.href; /* */},"scope":"GLOBAL"},{"id":"82d3f082-6cb8-453e-8f80-567200c2a4da","name":"Page visit","type":"CONSTANT","valueType":"TEXT","value":"Page visit","scope":"GLOBAL"},{"id":"b12f3744-de4e-487e-b45c-494d0ecd4b3b","name":"Product details page visit","type":"CONSTANT","valueType":"TEXT","value":"Product details page visit","scope":"GLOBAL"},{"id":"a7b55fae-30ce-4ae7-a0ac-1acd8748d66e","name":"Schaeffler_Category","type":"FUNCTION","valueType":"TEXT","function":function(event){var dl = window.dataLayer; for (var i = dl.length - 1; i >= 0; i--) { if (dl[i].PLP_productCategorization) return dl[i].PLP_productCategorization; } return ''; /* */},"scope":"LOCAL"},{"id":"52f168b0-8144-4003-b0ae-369b44aafd84","name":"Schaeffler_EUR","type":"CONSTANT","valueType":"TEXT","value":"EUR","scope":"LOCAL"},{"id":"15f5b778-7683-47f0-8cdc-31bfd8ffaebe","name":"Schaeffler_Locale","type":"FUNCTION","valueType":"TEXT","function":function(event){return document.documentElement.lang; /* */},"scope":"LOCAL"},{"id":"4b31aee9-fc63-4a96-b163-275d6cf0ff86","name":"Schaeffler_PDP_CurrencyCode","type":"FUNCTION","valueType":"TEXT","function":function(event){var dl = window.dataLayer; for (var i = dl.length - 1; i >= 0; i--) { if (dl[i].event === 'productDetailView' && dl[i].ecommerce) return dl[i].ecommerce.currencyCode; } return 'EUR'; /* */},"scope":"LOCAL"},{"id":"7dcc296b-86a4-458b-87b2-ea89054763e7","name":"Schaeffler_PDP_SKU","type":"FUNCTION","valueType":"TEXT","function":function(event){var dl = window.dataLayer; for (var i = dl.length - 1; i >= 0; i--) { if (dl[i].PDP_designation) return dl[i].PDP_designation; } return ''; /* */},"scope":"LOCAL"},{"id":"21a38378-140c-4082-b33e-31b26f828dbb","name":"Schaeffler_PLP_ATC_SKU","type":"FUNCTION","valueType":"TEXT","function":function(event){if (event && event.target) { var card = event.target.closest('.MuiCard-root'); if (card) { var links = card.querySelectorAll('a[href*="/p/"]'); for (var i = 0; i < links.length; i++) { var text = links[i].textContent.trim(); if (text) return text; } } } return ''; /* */},"scope":"LOCAL"},{"id":"4fc3fc04-adda-412a-9405-f992f6a3d5b2","name":"Schaeffler_Quantity","type":"CONSTANT","valueType":"INTEGER","value":"1","scope":"LOCAL"},{"id":"7eceb1cc-85b6-43d2-a14b-eab430cd191d","name":"Schaeffler_SearchIsEmpty","type":"FUNCTION","valueType":"BOOLEAN","function":function(event){return document.querySelectorAll('.jsSearchResultProduct, .jsSearchResultProd').length === 0; /* */},"scope":"LOCAL"},{"id":"8c2ed388-763b-410d-a183-f6fbe3c2d3aa","name":"Schaeffler_SearchKeyword","type":"FUNCTION","valueType":"TEXT","function":function(event){try { return new URLSearchParams(window.location.search).get('text') || ''; } catch(e) { return ''; } /* */},"scope":"LOCAL"},{"id":"b75f1863-85e0-47d8-b11a-934370b61d87","name":"Schaeffler_SearchSKUs","type":"FUNCTION","valueType":"LIST","function":function(event){var results = []; var cards = document.querySelectorAll('.jsSearchResultProduct, .jsSearchResultProd'); for (var i = 0; i < cards.length; i++) { var links = cards[i].querySelectorAll('a[href*="/p/"]'); for (var j = 0; j < links.length; j++) { var text = links[j].textContent.trim(); if (text && results.indexOf(text) === -1) results.push(text); } } return results; /* */},"scope":"LOCAL"},{"id":"f01382a0-755c-4e9c-902f-aee7c524ae7e","name":"Schaeffler_TargetUrl","type":"FUNCTION","valueType":"TEXT","function":function(event){if (event && event.target) { var a = event.target.closest('a'); if (a) return a.href; } return ''; /* */},"scope":"LOCAL"},{"id":"c0fe42da-f640-40d9-8e2e-9e39ea0c8dfe","name":"Search results","type":"CONSTANT","valueType":"TEXT","value":"Search results","scope":"GLOBAL"}]; const script = {"id":"a91628c3-c779-41e8-8fc6-ad6e2ea2f2aa","rows":[{"id":"64ebb5e4-9626-420b-b41f-0b9090843492","rowType":"EVENT","trigger":{"id":"13a72226-bc37-49ac-ae9b-c61aa1cf92d5","name":"Schaeffler_DeclineTracking_Click","type":"CLICK","groups":[{"id":"b3145da3-614c-4164-b9a8-768a5ed80eda","rows":[{"valueType":"EVENT","id":"16dc4b85-5606-46b4-896a-e00ca71573a7","variableId":"ad6080ea-719b-4732-952f-ef4f1ce45e02","value":".save-preference-btn-handler *, .save-preference-btn-handler","operator":"MATCHES_CSS_SELECTOR"}]}]},"action":{"type":"DECLINE_TRACKING"},"fields":[{"id":"3d14269a-506e-46d8-b56b-ca70ccd5909e","variableId":"15f5b778-7683-47f0-8cdc-31bfd8ffaebe","fieldName":"locale"},{"id":"9a0710ea-a95c-4f16-a440-648057eb00b6","variableId":"fda8e609-5410-4707-b86b-01184d2af597","fieldName":"eventLabel"}]},{"id":"6264d121-bdfa-47f8-9927-5af8b82f9154","rowType":"EVENT","trigger":{"id":"dda00627-0fec-4588-9305-7110a98aeb11","name":"Schaeffler_AllPages_PageLoad","type":"PAGE_LOAD","groups":[{"id":"be80d776-a22a-4487-ade0-21b50407f0fe","rows":[{"valueType":"TEXT","id":"93815524-fcd4-4829-8ac6-9184361259d9","variableId":"75cf8fd1-fafe-44be-9806-23ff3017c9c1","value":"/p/","operator":"DOES_NOT_CONTAIN"}]}]},"action":{"type":"PAGE_VISITED"},"fields":[{"id":"ed469982-1c7c-4782-9f00-c5e0cd6c2e2d","variableId":"15f5b778-7683-47f0-8cdc-31bfd8ffaebe","fieldName":"locale"},{"id":"6885ef3c-fca2-4ecf-bc0c-021adad8537c","variableId":"a7b55fae-30ce-4ae7-a0ac-1acd8748d66e","fieldName":"category"},{"id":"f50e224d-5949-4a2d-a9ad-980553cb109f","variableId":"82d3f082-6cb8-453e-8f80-567200c2a4da","fieldName":"eventLabel"}]},{"id":"3ea226e3-6376-48c0-9450-2d087d530a7b","rowType":"EVENT","trigger":{"id":"96ddb354-e8ce-474c-809e-0888e7a450ea","name":"Schaeffler_PDP_PageLoad","type":"PAGE_LOAD","groups":[{"id":"063790f5-a302-42c3-9b9d-ae5c37c86a9b","rows":[{"valueType":"TEXT","id":"4a4ada61-a1fb-43b0-a576-9a169ef54dfa","variableId":"75cf8fd1-fafe-44be-9806-23ff3017c9c1","value":"/p/","operator":"CONTAINS"}]}]},"action":{"type":"PDP_VISITED"},"fields":[{"id":"8f0aa0bf-840e-41ce-a32b-bdf9fe98c1bd","variableId":"7dcc296b-86a4-458b-87b2-ea89054763e7","fieldName":"sku"},{"id":"0b5823f6-a3e3-4bf8-88f5-7e08c880116e","variableId":"15f5b778-7683-47f0-8cdc-31bfd8ffaebe","fieldName":"locale"},{"id":"ce22bbe9-05f4-484e-9a40-45022df47fb4","variableId":"a7b55fae-30ce-4ae7-a0ac-1acd8748d66e","fieldName":"category"},{"id":"5478ff6f-aefa-4d78-a593-0ff9e41b265e","variableId":"4b31aee9-fc63-4a96-b163-275d6cf0ff86","fieldName":"currencyCode"},{"id":"dedcf22d-8318-4589-8560-534442d826e1","variableId":"4fc3fc04-adda-412a-9405-f992f6a3d5b2","fieldName":"quantity"},{"id":"2219979e-d7be-4f3a-b895-fc3143b5138c","variableId":"b12f3744-de4e-487e-b45c-494d0ecd4b3b","fieldName":"eventLabel"}]},{"id":"67a36d83-bcd8-4d96-9fdc-810bf6a7bbd9","rowType":"EVENT","trigger":{"id":"5dac1eab-13d0-4ad6-9c30-dd8b20a8b5ed","name":"Schaeffler_PDP_ATC_Click","type":"CLICK","delayMs":500,"groups":[{"id":"996d36d4-d500-4672-9174-2ca4cbd22cb4","rows":[{"valueType":"EVENT","id":"2f606317-0952-4933-aece-b0ea27f1de4a","variableId":"ad6080ea-719b-4732-952f-ef4f1ce45e02","value":".MuiButton-containedPrimary.MuiButton-sizeLarge *, .MuiButton-containedPrimary.MuiButton-sizeLarge","operator":"MATCHES_CSS_SELECTOR"},{"valueType":"TEXT","id":"98f2cbb7-6517-4034-9c0b-965cb1e24f82","variableId":"75cf8fd1-fafe-44be-9806-23ff3017c9c1","value":"/p/","operator":"CONTAINS"}]}]},"action":{"type":"ADD_TO_CART"},"fields":[{"id":"06af1659-bec6-454b-b70a-594e2245a409","variableId":"7dcc296b-86a4-458b-87b2-ea89054763e7","fieldName":"sku"},{"id":"95a254ff-e337-48a8-8902-1a93cdecbb25","variableId":"15f5b778-7683-47f0-8cdc-31bfd8ffaebe","fieldName":"locale"},{"id":"e599da20-0eb4-4587-8769-66a65ea3d684","variableId":"a7b55fae-30ce-4ae7-a0ac-1acd8748d66e","fieldName":"category"},{"id":"055aefdb-1a5b-48dc-80e2-28ebf27061df","variableId":"4b31aee9-fc63-4a96-b163-275d6cf0ff86","fieldName":"currencyCode"},{"id":"ff34f94b-a01c-44d1-8b3d-9ee562de9b55","variableId":"4fc3fc04-adda-412a-9405-f992f6a3d5b2","fieldName":"quantity"},{"id":"cf46d4fe-787e-4051-82da-ce05f2192401","variableId":"1eec2be6-dc91-4c4e-a443-da2f051baf5b","fieldName":"eventLabel"}]},{"id":"112c6996-be1c-4a24-a3b2-596ffe74f015","rowType":"EVENT","trigger":{"id":"72ba3353-4b52-40e6-99e7-ef4e6d2aeeb1","name":"Schaeffler_PLP_ATC_Click","type":"CLICK","delayMs":500,"groups":[{"id":"036d6a6a-8b6b-4891-98f2-960c4375c18b","rows":[{"valueType":"EVENT","id":"a98deab9-ade0-4e79-a44c-39e96bff61ab","variableId":"ad6080ea-719b-4732-952f-ef4f1ce45e02","value":".MuiButton-containedPrimary.MuiButton-sizeMedium *, .MuiButton-containedPrimary.MuiButton-sizeMedium","operator":"MATCHES_CSS_SELECTOR"},{"valueType":"TEXT","id":"3d15a936-80f0-468b-9bea-e366490e8410","variableId":"75cf8fd1-fafe-44be-9806-23ff3017c9c1","value":"/p/","operator":"DOES_NOT_CONTAIN"}]}]},"action":{"type":"ADD_TO_CART"},"fields":[{"id":"7add11a6-7fdb-46da-849c-461cf9438b91","variableId":"21a38378-140c-4082-b33e-31b26f828dbb","fieldName":"sku"},{"id":"7247bb57-abd1-4288-a88b-657e1ea31874","variableId":"15f5b778-7683-47f0-8cdc-31bfd8ffaebe","fieldName":"locale"},{"id":"db0774b9-a407-47bc-ad54-8ccb2ee604c5","variableId":"a7b55fae-30ce-4ae7-a0ac-1acd8748d66e","fieldName":"category"},{"id":"dcece60a-ecec-43a4-9ab1-7628001bcf0f","variableId":"52f168b0-8144-4003-b0ae-369b44aafd84","fieldName":"currencyCode"},{"id":"85f2623e-e4d4-41e1-8e89-89c10bd09bea","variableId":"4fc3fc04-adda-412a-9405-f992f6a3d5b2","fieldName":"quantity"},{"id":"41a1504e-191d-4ead-bae4-4c6c20bea75a","variableId":"1eec2be6-dc91-4c4e-a443-da2f051baf5b","fieldName":"eventLabel"}]},{"id":"8476123e-76da-474b-8cfd-d3ec29f22fdb","rowType":"EVENT","trigger":{"id":"993efb88-1105-4922-8f2b-35074306321f","name":"Schaeffler_SearchResults_PageLoad","type":"PAGE_LOAD","groups":[{"id":"7c567e7d-be7e-480a-9951-f633b4e0da48","rows":[{"valueType":"TEXT","id":"4ced9fe5-6668-4218-81fc-082198fe4cf0","variableId":"75cf8fd1-fafe-44be-9806-23ff3017c9c1","value":"/search/searchpage","operator":"CONTAINS"}]}]},"action":{"type":"SEARCH"},"fields":[{"id":"13d713a1-120b-4a83-8e52-fd7206f6bd97","variableId":"15f5b778-7683-47f0-8cdc-31bfd8ffaebe","fieldName":"locale"},{"id":"b51beb52-0fa8-4362-9288-a34e74f04a40","variableId":"8c2ed388-763b-410d-a183-f6fbe3c2d3aa","fieldName":"eventLabel"}]},{"id":"277f4fd8-dacc-4326-b307-54ce3118668b","rowType":"EVENT","trigger":{"id":"993efb88-1105-4922-8f2b-35074306321f","name":"Schaeffler_SearchResults_PageLoad","type":"PAGE_LOAD","groups":[{"id":"7c567e7d-be7e-480a-9951-f633b4e0da48","rows":[{"valueType":"TEXT","id":"4ced9fe5-6668-4218-81fc-082198fe4cf0","variableId":"75cf8fd1-fafe-44be-9806-23ff3017c9c1","value":"/search/searchpage","operator":"CONTAINS"}]}]},"action":{"type":"SEARCH_RESULT"},"fields":[{"id":"48044387-8a3e-41e3-a8a8-068c0d3aeed8","variableId":"15f5b778-7683-47f0-8cdc-31bfd8ffaebe","fieldName":"locale"},{"id":"fef06417-4572-483b-89ca-f85fa1f6900b","variableId":"7eceb1cc-85b6-43d2-a14b-eab430cd191d","fieldName":"isEmpty"},{"id":"9c9bb4d4-b820-4b7f-9924-762f8fa8bcda","variableId":"b75f1863-85e0-47d8-b11a-934370b61d87","fieldName":"skus"},{"id":"b67df662-92ee-4a3f-a28f-e557cb7b1271","variableId":"c0fe42da-f640-40d9-8e2e-9e39ea0c8dfe","fieldName":"eventLabel"}]},{"id":"6ac2c80c-c099-4f46-b1d3-ab87ea712749","rowType":"EVENT","trigger":{"id":"eacee6f9-282a-4e0e-aba6-5b53221ad6d8","name":"Schaeffler_SearchResults_Click","type":"CLICK","groups":[{"id":"7fc46a65-37cb-45fd-909e-b823da570bc9","rows":[{"valueType":"EVENT","id":"805f0cab-2117-429b-b0b7-d9758baf15a5","variableId":"ad6080ea-719b-4732-952f-ef4f1ce45e02","value":".jsSearchResultProduct a *, .jsSearchResultProduct a","operator":"MATCHES_CSS_SELECTOR"},{"valueType":"TEXT","id":"cf0e9070-58ab-40cc-82f7-24d2f567a6d8","variableId":"75cf8fd1-fafe-44be-9806-23ff3017c9c1","value":"/search/searchpage","operator":"CONTAINS"}]}]},"action":{"type":"CLICKOUT"},"fields":[{"id":"2b90ef6d-0835-4873-857f-9cefb2abd519","variableId":"15f5b778-7683-47f0-8cdc-31bfd8ffaebe","fieldName":"locale"},{"id":"67f3729f-3187-4338-916c-dbc4342fb63a","variableId":"f01382a0-755c-4e9c-902f-aee7c524ae7e","fieldName":"targetUrl"},{"id":"7a857340-808c-44e8-8437-1d3a620ad285","variableId":"6c77635c-e95a-4eca-8086-f6be1b1193e8","fieldName":"namedReferral"},{"id":"e1767bfb-7908-43eb-b648-16dfd972c932","variableId":"2e141436-b43c-40d6-bbed-64cfdca7f222","fieldName":"eventLabel"}]},{"id":"5e5208ff-b22d-4936-afd8-67326ae01e32","rowType":"EVENT","trigger":{"id":"630373ec-4c3a-4d2f-9e3f-f44551961879","name":"Schaeffler_SearchSuggest_Click","type":"CLICK","groups":[{"id":"d43841d5-37f9-4416-a25a-996b056669bb","rows":[{"valueType":"EVENT","id":"c6cf2ac2-2836-41f3-a438-c747b7baeaa6","variableId":"ad6080ea-719b-4732-952f-ef4f1ce45e02","value":".scha__search-box a[href*='/p/'] *, .scha__search-box a[href*='/p/']","operator":"MATCHES_CSS_SELECTOR"}]}]},"action":{"type":"CLICKOUT"},"fields":[{"id":"df26ddbd-639b-4f1d-84b1-06923bcd9a45","variableId":"15f5b778-7683-47f0-8cdc-31bfd8ffaebe","fieldName":"locale"},{"id":"48a8fc14-07d3-4c45-a75d-d22cd5661c42","variableId":"f01382a0-755c-4e9c-902f-aee7c524ae7e","fieldName":"targetUrl"},{"id":"ddc5efc9-7f44-49a1-ab24-5d71e8c12983","variableId":"6c77635c-e95a-4eca-8086-f6be1b1193e8","fieldName":"namedReferral"},{"id":"16a51911-d9a2-4dfd-ba89-8a8aa453af49","variableId":"2e141436-b43c-40d6-bbed-64cfdca7f222","fieldName":"eventLabel"}]}]}; const advancedCode = function(){}; const url = 'https://queue-propagator.zoovu.com'; const currentEnvironment = 'orca'; const currentAccountId = 250000469; const currencies = ['FJD', 'STD', 'MXN', 'SCR', 'LVL', 'CDF', 'BBD', 'HNL', 'UGX', 'MXV', 'ZAR', 'STN', 'CUC', 'SDD', 'BSD', 'SDG', 'IQD', 'GMD', 'CUP', 'TWD', 'RSD', 'UYI', 'MYR', 'FKP', 'XOF', 'UYU', 'CVE', 'OMR', 'SEK', 'KES', 'BTN', 'GNF', 'MZN', 'SVC', 'MZM', 'ARS', 'QAR', 'IRR', 'NLG', 'XPD', 'UZS', 'XPF', 'THB', 'BDT', 'LYD', 'KWD', 'XPT', 'RUB', 'ISK', 'BEF', 'MKD', 'RUR', 'DZD', 'PAB', 'SGD', 'KGS', 'XAD', 'XAF', 'XAG', 'CHF', 'ATS', 'HRK', 'ITL', 'CHE', 'DJF', 'TZS', 'ADP', 'VND', 'XAU', 'AUD', 'CHW', 'KHR', 'IDR', 'XBA', 'KYD', 'XBC', 'XBB', 'BWP', 'SHP', 'XBD', 'CYP', 'TJS', 'RWF', 'AED', 'DKK', 'BGL', 'ZWD', 'BGN', 'MMK', 'ZWG', 'NOK', 'SYP', 'ZWL', 'YUM', 'ZWN', 'LKR', 'ZWR', 'CZK', 'IEP', 'XCD', 'GRD', 'HTG', 'XSU', 'AFA', 'XCG', 'SIT', 'BHD', 'PTE', 'SZL', 'KZT', 'YER', 'AFN', 'BYB', 'NPR', 'AWG', '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', 'TOP', 'AZM', 'AZN', 'PGK', 'UAH', 'ERN', 'TPE', 'MRO', 'CNY', 'MRU', 'BMD', 'PHP', 'XXX', 'PYG', 'JMD', 'GWP', 'ESP', 'COP', 'USD', 'COU', 'USN', 'VEB', 'ETB', 'VED', 'USS', 'VUV', 'VEF', 'SOS', 'LAK', 'BND', 'ZMK', 'LRD', 'ALL', 'GHC', 'MTL', 'VES', 'ZMW', 'TRL', 'ILS', 'GHS', 'KPW', 'GYD', 'MDL', 'BOB', 'AMD', 'TRY', 'LBP', 'JOD', 'HKD', 'EUR', 'LSL', 'CAD', 'BOV', 'EEK', 'MUR', 'ROL', 'GIP', 'RON', 'NGN', 'CRC', 'PKR', 'ANG', 'SRD', 'SAR', 'LTL', 'TTD', 'MVR', 'SRG', 'INR', 'KRW', 'JPY', 'PLN', 'AOA', 'SBD', 'CSD', 'LUF', 'MWK', 'MGA', 'FIM', 'MGF', 'DEM', '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 = []; /** * @public PUBLIC API — externally consumed. * Renaming, changing the signature, or changing the return shape * will break callers. Do not modify without a deprecation plan. */ function disableTracking() { trackingEnabled = false; } /** * @public PUBLIC API — externally consumed. * Renaming, changing the signature, or changing the return shape * will break callers. Do not modify without a deprecation plan. */ function enableTracking() { trackingEnabled = true; } /** * @public PUBLIC API — externally consumed. * Renaming, changing the signature, or changing the return shape * will break callers. Do not modify without a deprecation plan. */ 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(/\.$/, ''); if (parsedPrice === '') { throw new Error(`getNumericPriceFromString: "${stringPrice}" is not a valid price (no numeric value found).`); } const numericPrice = Number(parsedPrice); if (Number.isNaN(numericPrice)) { throw new Error(`getNumericPriceFromString: "${stringPrice}" is not a valid price (parsed to NaN).`); } return numericPrice; } // helper for loading launcher from advancedmode /** * @public PUBLIC API — externally consumed. * Renaming, changing the signature, or changing the return shape * will break callers. Do not modify without a deprecation plan. */ 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); } } /** * @public PUBLIC API — externally consumed. * Renaming, changing the signature, or changing the return shape * will break callers. Do not modify without a deprecation plan. */ 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 domainRestrictedFieldNames = Object.freeze({ CURRENCY_CODE: 'currencyCode', NAMED_REFERRAL: 'namedReferral' }); // Fields whose value must belong to a fixed domain. `optional: true` means nullish is accepted; // otherwise nullish is itself an incompatible value. Add new entries here to extend. const domainRestrictedFields = Object.freeze({ [domainRestrictedFieldNames.CURRENCY_CODE]: { optional: true, isValid: value => currencies.includes(value) }, [domainRestrictedFieldNames.NAMED_REFERRAL]: { optional: false, isValid: value => value != null && !!namedReferrals[String(value).toUpperCase()] } }); 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 writeZoovuCidCookie(cid) { const hostname = window.location.hostname; const domain = getRegistrableDomain(hostname); const oneYearInSeconds = 365 * 24 * 60 * 60; let zoovuCid = `zoovu-cid=${cid}; path=/; domain=.${domain}; max-age=${oneYearInSeconds}`; if (window.isSecureContext) { zoovuCid += `; Secure`; } document.cookie = zoovuCid; } function addZoovuCidToCookies() { const cid = `${generateUuid()}_${Date.now()}`; writeZoovuCidCookie(cid); return cid; } /** * @public PUBLIC API — externally consumed. * Renaming, changing the signature, or changing the return shape * will break callers. Do not modify without a deprecation plan. */ function getCID() { removeMalformedZoovuCidCookies(); // Check if we have a valid cookie const existingCid = getCookieValue(`zoovu-cid`); if (existingCid !== null && isValidCidFormat(existingCid)) { writeZoovuCidCookie(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) { // Missing values are allowed here — required-field enforcement lives in ensureRequiredFieldsPresent. // Optional fields may legitimately return null/undefined (e.g. category, price). if (value === undefined || value === null) { return; } let errorMessage = ''; 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) { const validator = domainRestrictedFields[fieldName]; if (!validator) { return; } // Optional fields accept nullish — ensureRequiredFieldsPresent enforces presence where needed // (e.g. currencyCode on PURCHASED). For non-optional fields, nullish is itself incompatible. if (value == null && validator.optional) { return; } if (!validator.isValid(value)) { throw new TrackingExecutionError( `Value ${value} is not compatible with ${fieldName}.`, 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 (domainRestrictedFields[fieldName]) { 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); // Trigger variable returned no value — treat as "condition not met" rather than crashing the matcher. if (target === undefined || target === null) { return false; } 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 recordTrackingError(error) { try { const trackingExecutions = jsonParser(trackingFieldName); const isKnown = error instanceof TrackingExecutionError; const failedExecution = createTrackingFailedExecutionRecord( isKnown ? error.trackingEntityType : null, isKnown ? error.variableId : null, isKnown ? error.triggerId : null, isKnown ? error.scriptId : script.id, isKnown ? error.trackingErrorType : trackingErrorTypes.OTHER, (error && error.message) || String(error) ); trackingExecutions.push(failedExecution); localStorage.setItem(trackingFieldName, JSON.stringify(trackingExecutions)); } catch (recordingError) { console.debug('Failed to record tracking error', recordingError); } console.debug(error); } 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) { try { runAction(rule.action, rule.fields, event, rule.trigger); } catch (error) { recordTrackingError(error); } }, 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) { recordTrackingError(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 rawValue = localStorage.getItem(key) if (rawValue === null) { return [] } const foundValue = JSON.parse(rawValue) 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) { if (Number.isNaN(eventSpecificBody.price)) { throw new Error(`Invalid price for ${eventType} event: price resolved to NaN.`); } const body = { ...getBaseEventBody(eventType), // event label + custom per-event fields ...eventSpecificBody, }; sendEvent(body); }; /** * @public PUBLIC API — externally consumed. * Renaming, changing the signature, or changing the return shape * will break callers. Do not modify without a deprecation plan. */ function sendPageVisitedEvent({locale, category, eventLabel = 'Page visit'}) { const body = { locale, eventLabel, // optional category, }; if (ensureRequiredFieldsPresent({locale}, eventTypes.PAGE_VISITED)) { sendFactEventBase(eventTypes.PAGE_VISITED, body); } }; /** * @public PUBLIC API — externally consumed. * Renaming, changing the signature, or changing the return shape * will break callers. Do not modify without a deprecation plan. */ 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); } }; /** * @public PUBLIC API — externally consumed. * Renaming, changing the signature, or changing the return shape * will break callers. Do not modify without a deprecation plan. */ 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); } }; /** * @public PUBLIC API — externally consumed. * Renaming, changing the signature, or changing the return shape * will break callers. Do not modify without a deprecation plan. */ 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); } }; /** * @public PUBLIC API — externally consumed. * Renaming, changing the signature, or changing the return shape * will break callers. Do not modify without a deprecation plan. */ 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); } }; /** * @public PUBLIC API — externally consumed. * Renaming, changing the signature, or changing the return shape * will break callers. Do not modify without a deprecation plan. */ 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(); } }; /** * @public PUBLIC API — externally consumed. * Renaming, changing the signature, or changing the return shape * will break callers. Do not modify without a deprecation plan. */ function sendSearchEvent({ locale, eventLabel = "Search phrase typed", category }) { const body = { locale, eventLabel, // optional category, }; if (ensureRequiredFieldsPresent({locale}, eventTypes.SEARCH)) { sendFactEventBase(eventTypes.SEARCH, body); } }; /** * @public PUBLIC API — externally consumed. * Renaming, changing the signature, or changing the return shape * will break callers. Do not modify without a deprecation plan. */ 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); } }; /** * @public PUBLIC API — externally consumed. * Renaming, changing the signature, or changing the return shape * will break callers. Do not modify without a deprecation plan. */ 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); } }; /** * @public PUBLIC API — externally consumed. * Renaming, changing the signature, or changing the return shape * will break callers. Do not modify without a deprecation plan. */ 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); } }; /** * @public PUBLIC API — externally consumed. * Renaming, changing the signature, or changing the return shape * will break callers. Do not modify without a deprecation plan. */ 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, 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(); } })();