(function(){ const zoovuId = 'LRfPGXWWM436kbPVYhpheyJwrwYhn8ZIicL9AQ6dtSw='; const domainId = '5114d601-6478-4423-8740-3564e6ef0036'; const variables = [{"id":"b73b1d5c-2232-410e-be49-48ec6609f4d7","name":"Clickout, add product from search result","type":"CONSTANT","valueType":"TEXT","value":"Clickout, add product from search result","scope":"GLOBAL"},{"id":"3b881070-bf08-4869-be93-2b5d94a99c5b","name":"Clickout, category","type":"CONSTANT","valueType":"TEXT","value":"Clickout, category","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":"39c61b10-34bc-48df-9c7f-ec8bb6871209","name":"Midea_Category","type":"FUNCTION","valueType":"TEXT","function":function(event){var bc = document.querySelectorAll("[class*=\"breadcrumb\"] a"); if (bc.length > 1) return Array.from(bc).slice(1, -1).map(function(a) { return a.textContent.trim(); }).join(" > "); return ""; /* */},"scope":"LOCAL"},{"id":"ccd60d2c-7251-41d3-9194-7c9400aca134","name":"Midea_ClickedLinkIsProduct","type":"FUNCTION","valueType":"BOOLEAN","function":function(event){if (event && event.target) { var a = event.target.closest("a"); if (a && a.href) { var parts = a.href.split("."); if (parts.length > 1 && parts[parts.length-1].match(/^[a-z0-9]{5,}$/i)) return true; } } return false; /* */},"scope":"LOCAL"},{"id":"5cb3040e-cfba-4643-82f6-a01d7b9c60e0","name":"Midea_ClickoutReferral","type":"CONSTANT","valueType":"TEXT","value":"SEARCH","scope":"LOCAL"},{"id":"b74d9ceb-23bf-4183-a009-bd4238e28a6f","name":"Midea_ClickoutTargetUrl","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":"29a0f48a-5496-47d3-ba7a-3a710f533a33","name":"Midea_CurrencyCode","type":"CONSTANT","valueType":"TEXT","value":"USD","scope":"LOCAL"},{"id":"3f77499e-e1b5-4b6e-8c04-aea348d83ed5","name":"Midea_IsSearchPage","type":"FUNCTION","valueType":"BOOLEAN","function":function(event){return window.location.pathname.includes("/search"); /* */},"scope":"LOCAL"},{"id":"961c2d0f-d833-49f9-aae1-773794875cd4","name":"Midea_Locale","type":"FUNCTION","valueType":"TEXT","function":function(event){return document.documentElement.lang || "en-US"; /* */},"scope":"LOCAL"},{"id":"6412f860-2c21-47e5-ada3-2d3dcd4c3ea2","name":"Midea_Price","type":"FUNCTION","valueType":"DECIMAL","function":function(event){var scripts = document.querySelectorAll("script[type=\"application/ld+json\"]"); for (var i = 0; i < scripts.length; i++) { try { var d = JSON.parse(scripts[i].textContent); if (d["@type"] === "Product" && d.offers) return parseFloat(d.offers.price) || 0; } catch(e) {} } return 0; /* */},"scope":"LOCAL"},{"id":"9c0848f1-c2b5-46a3-9c66-e62f6e1d83c9","name":"Midea_Quantity","type":"FUNCTION","valueType":"INTEGER","function":function(event){var input = document.querySelector("input[type=\"number\"]"); return input ? parseInt(input.value) || 1 : 1; /* */},"scope":"LOCAL"},{"id":"96859b93-7cbe-45bf-a83a-03f29d3716f1","name":"Midea_SearchIsEmpty","type":"FUNCTION","valueType":"BOOLEAN","function":function(event){var m = document.body.innerText.match(/All \((\d+)\)/); return m ? m[1] === "0" : document.body.innerText.indexOf("No Relevant Product") !== -1; /* */},"scope":"LOCAL"},{"id":"2801f8c0-d44b-4f90-a1c4-6a58f7c2b502","name":"Midea_SearchQuery","type":"FUNCTION","valueType":"TEXT","function":function(event){return new URLSearchParams(window.location.search).get("q") || ""; /* */},"scope":"LOCAL"},{"id":"894c0121-b2a8-4b6b-ae6c-75721b71f1a3","name":"Midea_SearchSKUs","type":"FUNCTION","valueType":"LIST","function":function(event){var links = document.querySelectorAll("a[href*=\"/us/store/\"]"); var skus = []; Array.from(links).forEach(function(a) { var parts = a.href.split("."); if (parts.length > 1) { var sku = parts[parts.length - 1].toUpperCase(); if (sku.length > 3 && sku.match(/[A-Z0-9]+/) && skus.indexOf(sku) === -1) skus.push(sku); } }); return skus; /* */},"scope":"LOCAL"},{"id":"ee0543a5-4f94-4501-86fb-bab8389b1492","name":"Midea_SKU","type":"FUNCTION","valueType":"TEXT","function":function(event){var parts = window.location.pathname.split("."); if (parts.length > 1) return parts[parts.length - 1].toUpperCase(); return ""; /* */},"scope":"LOCAL"},{"id":"c0e4f3c4-66bd-4038-b575-ed15e6accbb7","name":"Midea_SuggestClickText","type":"FUNCTION","valueType":"TEXT","function":function(event){if (event && event.target) { var li = event.target.closest("li"); if (li) return li.textContent.trim(); } return ""; /* */},"scope":"LOCAL"},{"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":"74b0398b-b27b-47fc-8c2a-ab77a812d400","rows":[{"id":"ddfc60e3-f220-4b0e-8738-1eed768cd1c6","rowType":"EVENT","trigger":{"id":"65de4260-5169-43f0-a66d-aab655391e38","name":"Midea_ConsentSave_Click","type":"CLICK","groups":[{"id":"452d8eb7-6d98-4e80-9497-08f2c99b13c3","rows":[{"valueType":"EVENT","id":"5673a7f0-e91e-461a-a5ae-e4aa4cb093ed","variableId":"ad6080ea-719b-4732-952f-ef4f1ce45e02","value":".save-preference-btn-handler","operator":"MATCHES_CSS_SELECTOR"}]}]},"action":{"type":"DECLINE_TRACKING"},"fields":[{"id":"ba987c12-b70b-4e0d-8fea-30601dab5b34","variableId":"961c2d0f-d833-49f9-aae1-773794875cd4","fieldName":"locale"}]},{"id":"7c4d634f-59c7-4c72-97e3-c6ad6489d91b","rowType":"EVENT","trigger":{"id":"95d0eac1-5d11-4647-92be-554851a1254c","name":"Midea_ConsentReject_Click","type":"CLICK","groups":[{"id":"34684b16-baf0-4340-8f95-222c01d2ff40","rows":[{"valueType":"EVENT","id":"b0e18b7f-ce2c-42a9-bf52-b0e486ebd243","variableId":"ad6080ea-719b-4732-952f-ef4f1ce45e02","value":"#onetrust-reject-all-handler","operator":"MATCHES_CSS_SELECTOR"}]}]},"action":{"type":"DECLINE_TRACKING"},"fields":[{"id":"d7d39bc7-24f4-4527-a92e-0c08d5b3e243","variableId":"961c2d0f-d833-49f9-aae1-773794875cd4","fieldName":"locale"}]},{"id":"a6904f6b-b4a1-4f5f-bf73-807a9a36c9b5","rowType":"EVENT","trigger":{"id":"8bd46a8a-b8ad-441c-b423-3ade72b33bad","name":"Midea_NonPDP_PageLoad","type":"PAGE_LOAD","groups":[{"id":"f2b17c20-c707-41d6-a3cf-18a559fcee0e","rows":[{"valueType":"TEXT","id":"201805be-3e0d-4aae-a081-43e861191c11","variableId":"75cf8fd1-fafe-44be-9806-23ff3017c9c1","value":"/store/","operator":"DOES_NOT_CONTAIN"}]},{"id":"270dc0ec-c04e-4298-a564-a7a0bbef8c25","rows":[{"valueType":"TEXT","id":"c63e8cf3-c73a-4468-98de-ddbdce13117d","variableId":"75cf8fd1-fafe-44be-9806-23ff3017c9c1","value":"/store/","operator":"CONTAINS"},{"valueType":"TEXT","id":"3cf75602-9c46-41d4-8a25-8af0f1ba8d02","variableId":"ee0543a5-4f94-4501-86fb-bab8389b1492","value":"","operator":"EQUALS"}]}]},"action":{"type":"PAGE_VISITED"},"fields":[{"id":"e71f4d5d-3958-4f8c-8632-d4f19d4ca31d","variableId":"961c2d0f-d833-49f9-aae1-773794875cd4","fieldName":"locale"},{"id":"96bc91d0-7880-41c0-9812-da4f15bf4b62","variableId":"39c61b10-34bc-48df-9c7f-ec8bb6871209","fieldName":"category"}]},{"id":"e056fbd6-97d3-4e69-b448-4d779c1f4ffa","rowType":"EVENT","trigger":{"id":"276153d5-a638-45df-aa47-b4439e4ea045","name":"Midea_PDP_PageLoad","type":"PAGE_LOAD","groups":[{"id":"eefc90c2-1f18-479e-b355-8276e9bd6daf","rows":[{"valueType":"TEXT","id":"762f2eee-2738-4e58-a96d-64d2657d8e38","variableId":"75cf8fd1-fafe-44be-9806-23ff3017c9c1","value":"/store/","operator":"CONTAINS"},{"valueType":"TEXT","id":"0f5730c0-b9eb-4d7d-a090-d1acd37f8ffe","variableId":"ee0543a5-4f94-4501-86fb-bab8389b1492","value":"","operator":"DOES_NOT_EQUAL"}]}]},"action":{"type":"PDP_VISITED"},"fields":[{"id":"9fecbdca-3164-4949-ad19-7e9c9ef45702","variableId":"ee0543a5-4f94-4501-86fb-bab8389b1492","fieldName":"sku"},{"id":"f4fc7be8-e788-478a-bcc6-5c48da893abe","variableId":"961c2d0f-d833-49f9-aae1-773794875cd4","fieldName":"locale"},{"id":"6b7633a1-260d-4b41-8c98-110827c43b94","variableId":"6412f860-2c21-47e5-ada3-2d3dcd4c3ea2","fieldName":"price"},{"id":"55a70395-ce97-45e9-ae27-536bdce0ae0e","variableId":"29a0f48a-5496-47d3-ba7a-3a710f533a33","fieldName":"currencyCode"},{"id":"eb407fb5-5ea7-44ca-8c53-01b9bdebdd86","variableId":"39c61b10-34bc-48df-9c7f-ec8bb6871209","fieldName":"category"}]},{"id":"446af2e9-06b0-4fd2-8ba6-3af8b8f6d34c","rowType":"EVENT","trigger":{"id":"9ca33202-ecaf-44f5-b037-49d2986e28dd","name":"Midea_ATC_Click","type":"CLICK","delayMs":500,"groups":[{"id":"7f2d665f-b960-454c-af49-536e0a0c8d0e","rows":[{"valueType":"EVENT","id":"61f4a0c1-f751-48b1-8506-98d879e65483","variableId":"ad6080ea-719b-4732-952f-ef4f1ce45e02","value":".csr-cp-cart_btn *, .csr-cp-cart_btn","operator":"MATCHES_CSS_SELECTOR"}]}]},"action":{"type":"ADD_TO_CART"},"fields":[{"id":"fd233d3e-9381-4953-b4cc-732aa1856a21","variableId":"ee0543a5-4f94-4501-86fb-bab8389b1492","fieldName":"sku"},{"id":"b8b30793-8d74-4848-af38-27c5c0f10823","variableId":"961c2d0f-d833-49f9-aae1-773794875cd4","fieldName":"locale"},{"id":"18824eeb-d64c-4f51-9c3b-2b779a08ff20","variableId":"6412f860-2c21-47e5-ada3-2d3dcd4c3ea2","fieldName":"price"},{"id":"a76625d4-1577-4dfd-9b1b-a7ffca43bdf7","variableId":"29a0f48a-5496-47d3-ba7a-3a710f533a33","fieldName":"currencyCode"},{"id":"224166b6-4a0a-4af2-b615-2da0501954e1","variableId":"9c0848f1-c2b5-46a3-9c66-e62f6e1d83c9","fieldName":"quantity"},{"id":"01e623c1-62e3-4633-9432-b05e150f00b9","variableId":"39c61b10-34bc-48df-9c7f-ec8bb6871209","fieldName":"category"}]},{"id":"543bc104-8f8a-47ac-9937-75b18b3b6549","rowType":"EVENT","trigger":{"id":"558c0b33-658b-40e7-8b6f-1717a1d25945","name":"Midea_Search_PageLoad","type":"PAGE_LOAD","groups":[{"id":"45362785-2f84-4ca1-b73c-127795b1194a","rows":[{"valueType":"TEXT","id":"b09ea293-0499-43f9-87f8-e04f6352042b","variableId":"75cf8fd1-fafe-44be-9806-23ff3017c9c1","value":"/search","operator":"CONTAINS"}]}]},"action":{"type":"SEARCH"},"fields":[{"id":"430f4967-59e9-4793-8a63-7b76b061edcc","variableId":"961c2d0f-d833-49f9-aae1-773794875cd4","fieldName":"locale"},{"id":"6f36ce6e-5090-44a3-809c-41aeadfc5acb","variableId":"2801f8c0-d44b-4f90-a1c4-6a58f7c2b502","fieldName":"eventLabel"},{"id":"d1f27cd1-30e4-42b3-8706-e06fb4029fa2","variableId":"39c61b10-34bc-48df-9c7f-ec8bb6871209","fieldName":"category"}]},{"id":"75f78de7-f88b-44a9-9ecc-c7df45316b8a","rowType":"EVENT","trigger":{"id":"558c0b33-658b-40e7-8b6f-1717a1d25945","name":"Midea_Search_PageLoad","type":"PAGE_LOAD","groups":[{"id":"45362785-2f84-4ca1-b73c-127795b1194a","rows":[{"valueType":"TEXT","id":"b09ea293-0499-43f9-87f8-e04f6352042b","variableId":"75cf8fd1-fafe-44be-9806-23ff3017c9c1","value":"/search","operator":"CONTAINS"}]}]},"action":{"type":"SEARCH_RESULT"},"fields":[{"id":"10e25c7e-6f0e-42eb-aedd-8f8b44f8f31a","variableId":"961c2d0f-d833-49f9-aae1-773794875cd4","fieldName":"locale"},{"id":"2e486813-6b3e-4070-83d3-d800cc0cee82","variableId":"96859b93-7cbe-45bf-a83a-03f29d3716f1","fieldName":"isEmpty"},{"id":"01df2b7b-60f7-4f29-ab10-ea6d9fa578ee","variableId":"894c0121-b2a8-4b6b-ae6c-75721b71f1a3","fieldName":"skus"},{"id":"2a057d1e-3278-4551-bea5-00f8aef3c6a6","variableId":"39c61b10-34bc-48df-9c7f-ec8bb6871209","fieldName":"category"}]},{"id":"a139f163-4c3b-4701-b5f6-20829be8e38d","rowType":"EVENT","trigger":{"id":"e3afa0f1-9316-4ddb-a2d7-58d78140a45b","name":"Midea_Clickout_PDP_Click","type":"CLICK","groups":[{"id":"d23e6d2a-bde8-4316-8af0-551fa99d85c6","rows":[{"valueType":"EVENT","id":"0086e042-87eb-4e75-8669-90d526f808e2","variableId":"ad6080ea-719b-4732-952f-ef4f1ce45e02","value":"a[href*=\"/store/\"] *, a[href*=\"/store/\"]","operator":"MATCHES_CSS_SELECTOR"},{"valueType":"BOOLEAN","id":"7a2908b3-473b-41fc-8cd8-3e122ce5a0f6","variableId":"3f77499e-e1b5-4b6e-8c04-aea348d83ed5","value":"true","operator":"EQUALS"},{"valueType":"BOOLEAN","id":"3cb43fd8-1cb6-4db5-8c8c-51749bfe3037","variableId":"ccd60d2c-7251-41d3-9194-7c9400aca134","value":"true","operator":"EQUALS"}]}]},"action":{"type":"CLICKOUT"},"fields":[{"id":"b289b56d-fdea-44be-b0d9-ffdab6fd4285","variableId":"961c2d0f-d833-49f9-aae1-773794875cd4","fieldName":"locale"},{"id":"73aa5407-2b54-45ba-8314-a2d71506b07e","variableId":"b74d9ceb-23bf-4183-a009-bd4238e28a6f","fieldName":"targetUrl"},{"id":"93cb27c5-8c3b-4998-958c-980da189ec03","variableId":"5cb3040e-cfba-4643-82f6-a01d7b9c60e0","fieldName":"namedReferral"},{"id":"221efccf-f4ae-4130-b8e4-92638fed192f","variableId":"2e141436-b43c-40d6-bbed-64cfdca7f222","fieldName":"eventLabel"}]},{"id":"61ce344a-e651-4f83-9ebd-e8073c589147","rowType":"EVENT","trigger":{"id":"a9c010a7-adc0-43e6-b20e-bb69e291dc27","name":"Midea_Clickout_Category_Click","type":"CLICK","groups":[{"id":"0ddc1a76-7957-4db4-a938-017dd1691355","rows":[{"valueType":"EVENT","id":"728870b8-3570-4406-89ee-8511eac35b9a","variableId":"ad6080ea-719b-4732-952f-ef4f1ce45e02","value":"a[href*=\"/store/\"] *, a[href*=\"/store/\"]","operator":"MATCHES_CSS_SELECTOR"},{"valueType":"BOOLEAN","id":"10e0a12c-1403-4813-bd00-4c11330bf677","variableId":"3f77499e-e1b5-4b6e-8c04-aea348d83ed5","value":"true","operator":"EQUALS"},{"valueType":"BOOLEAN","id":"6051f20e-42a9-444c-9650-b956333f7677","variableId":"ccd60d2c-7251-41d3-9194-7c9400aca134","value":"false","operator":"EQUALS"}]}]},"action":{"type":"CLICKOUT"},"fields":[{"id":"e55cae87-07f5-4c45-b08f-4c1fc5bad910","variableId":"961c2d0f-d833-49f9-aae1-773794875cd4","fieldName":"locale"},{"id":"5ee1c368-61e7-459e-bc95-c67c56926775","variableId":"b74d9ceb-23bf-4183-a009-bd4238e28a6f","fieldName":"targetUrl"},{"id":"5118693f-e9f4-4921-ad48-01af6f3fab42","variableId":"5cb3040e-cfba-4643-82f6-a01d7b9c60e0","fieldName":"namedReferral"},{"id":"d5b1e17a-6160-4c38-9420-b9c11705d8ff","variableId":"3b881070-bf08-4869-be93-2b5d94a99c5b","fieldName":"eventLabel"}]},{"id":"408d98c7-fbb6-4d61-b6f8-31cae8f783a6","rowType":"EVENT","trigger":{"id":"b1983059-e172-4eaf-a89c-b6f9f28d1d0f","name":"Midea_Clickout_ATC_Search_Click","type":"CLICK","delayMs":500,"groups":[{"id":"81e2b485-1fa0-4c75-8289-f4b517856720","rows":[{"valueType":"EVENT","id":"03d1637d-c881-4fbe-ab90-efc4716cb787","variableId":"ad6080ea-719b-4732-952f-ef4f1ce45e02","value":".csr-cp-cart_btn *, .csr-cp-cart_btn","operator":"MATCHES_CSS_SELECTOR"},{"valueType":"BOOLEAN","id":"107ef6ac-cb64-4e86-8d39-b79a4827164b","variableId":"3f77499e-e1b5-4b6e-8c04-aea348d83ed5","value":"true","operator":"EQUALS"}]}]},"action":{"type":"CLICKOUT"},"fields":[{"id":"66d81a67-0421-4ca5-8e01-9c6ed0d49fef","variableId":"961c2d0f-d833-49f9-aae1-773794875cd4","fieldName":"locale"},{"id":"6df1746d-8aac-423e-8332-fd881e4be9ed","variableId":"b74d9ceb-23bf-4183-a009-bd4238e28a6f","fieldName":"targetUrl"},{"id":"f28ccbca-06fa-40fb-b340-12f37f4669e3","variableId":"5cb3040e-cfba-4643-82f6-a01d7b9c60e0","fieldName":"namedReferral"},{"id":"845b6819-4811-4d76-bef3-abe3e66566da","variableId":"b73b1d5c-2232-410e-be49-48ec6609f4d7","fieldName":"eventLabel"}]},{"id":"741e56f7-dfd5-445c-83ea-b75698254fd5","rowType":"EVENT","trigger":{"id":"85f757b7-48ec-462d-853a-de718f0c03dd","name":"Midea_Clickout_Suggest_Click","type":"CLICK","groups":[{"id":"0a66c193-f9f9-48c2-931f-53f5a520c960","rows":[{"valueType":"EVENT","id":"bb3de5cd-2185-45ae-85ed-9e1b6e3279f4","variableId":"ad6080ea-719b-4732-952f-ef4f1ce45e02","value":".search-result-v1_search_associations_ul li *, .search-result-v1_search_associations_ul li","operator":"MATCHES_CSS_SELECTOR"}]}]},"action":{"type":"CLICKOUT"},"fields":[{"id":"bf29205d-7717-4e6a-8df0-cd83b2b12171","variableId":"961c2d0f-d833-49f9-aae1-773794875cd4","fieldName":"locale"},{"id":"0793d114-4bf8-4c7d-a3f1-706ca51f1324","variableId":"c0e4f3c4-66bd-4038-b575-ed15e6accbb7","fieldName":"targetUrl"},{"id":"0f8e236e-c54d-4ffe-a957-ee7bfd591805","variableId":"5cb3040e-cfba-4643-82f6-a01d7b9c60e0","fieldName":"namedReferral"},{"id":"9d7ea2b4-9e20-4ef3-a0a9-695b86e230fa","variableId":"2e141436-b43c-40d6-bbed-64cfdca7f222","fieldName":"eventLabel"}]}]}; const advancedCode = function(){}; const url = 'https://queue-propagator.zoovu.com'; const currentEnvironment = 'barracuda'; const currentAccountId = 1869; const currencies = ['FJD', 'STD', 'MXN', 'LVL', 'SCR', 'CDF', 'BBD', 'HNL', 'UGX', 'MXV', 'ZAR', 'STN', 'CUC', 'SDD', 'BSD', 'SDG', 'IQD', 'GMD', 'CUP', 'TWD', 'RSD', 'UYI', 'MYR', 'FKP', 'XOF', 'UYU', 'CVE', 'OMR', 'KES', 'SEK', 'BTN', 'GNF', 'MZN', 'SVC', 'MZM', 'ARS', 'QAR', 'IRR', 'NLG', 'XPD', 'XPF', 'UZS', 'THB', 'BDT', 'LYD', 'KWD', 'XPT', 'RUB', 'ISK', 'BEF', 'MKD', 'RUR', 'DZD', 'PAB', 'SGD', 'KGS', 'XAD', 'XAF', 'XAG', 'CHF', 'ATS', 'HRK', 'ITL', 'CHE', 'DJF', 'TZS', 'VND', 'ADP', 'XAU', 'AUD', 'CHW', 'KHR', 'XBA', 'IDR', 'KYD', 'XBC', 'XBB', 'SHP', 'BWP', 'XBD', 'CYP', 'TJS', 'AED', 'RWF', 'DKK', 'ZWD', 'BGL', 'BGN', 'MMK', 'NOK', 'SYP', 'ZWG', 'ZWL', 'YUM', 'ZWN', 'LKR', 'ZWR', 'IEP', 'CZK', 'GRD', 'XCD', 'HTG', 'XSU', 'AFA', 'XCG', 'BHD', 'SIT', 'PTE', 'SZL', 'KZT', 'YER', 'AFN', 'BYB', 'AWG', 'NPR', 'MNT', 'GBP', 'XTS', 'BYN', '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', 'ETB', 'VEB', 'USS', 'VED', 'SOS', 'VEF', 'VUV', 'LAK', 'ZMK', 'BND', 'LRD', 'ALL', 'GHC', 'MTL', 'ZMW', 'VES', 'TRL', 'ILS', 'KPW', 'GYD', 'GHS', 'MDL', 'BOB', 'AMD', 'TRY', 'LBP', 'JOD', 'HKD', 'EUR', 'LSL', 'CAD', 'BOV', 'EEK', 'MUR', 'ROL', 'GIP', 'RON', 'NGN', 'CRC', 'PKR', 'ANG', 'SRD', 'TTD', 'LTL', 'SAR', 'MVR', 'SRG', 'INR', 'KRW', 'JPY', 'PLN', 'AOA', '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(); } })();