(function(){ const zoovuId = 'n0swUGLZmSC/szEA2SGUJdlZq5tRvmphDvuUsdvubn0='; const domainId = '3507f2c2-6cc1-401a-b1f6-cd6c47c76348'; const variables = [{"id":"f51a1d5b-6bc1-4814-9140-af9516bcbd0d","name":"Clickout, add product from PLP search result","type":"CONSTANT","valueType":"TEXT","value":"Clickout, add product from PLP search result","scope":"GLOBAL"},{"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":"d2d8bad9-06f7-4f78-9184-8c38a9a469a1","name":"Clickout, view all products","type":"CONSTANT","valueType":"TEXT","value":"Clickout, view all products","scope":"GLOBAL"},{"id":"555e5793-ef14-46c2-9a45-d01e45f1c492","name":"Empty search result page","type":"CONSTANT","valueType":"TEXT","value":"Empty search result page","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":"3143b69e-2887-40db-b044-5f4368f0a545","name":"Moen_ATC_Price","type":"FUNCTION","valueType":"DECIMAL","function":function(event){ if (!event || !event.target || !event.target.closest) return null; var btn = event.target.closest("button"); var form = btn && btn.form; var id = form && form.elements.id ? String(form.elements.id.value) : ""; var root = event.target.closest("[data-analytics-card]") || (form && form.closest("main-product")) || event.target.closest("main-product"); if (!root) return null; var list = root.querySelectorAll("script[data-selected-variant]"); for (var i = 0; i < list.length; i++) { try { var v = JSON.parse(list[i].textContent); if (v && typeof v.price === "number" && (!id || String(v.id) === id)) return v.price / 100; } catch (e) {} } var items = root.querySelectorAll("script[data-analytics-item]"); for (var j = 0; j < items.length; j++) { try { var it = JSON.parse(items[j].textContent); if (it && typeof it.price === "number" && (!id || String(it.variant_id) === id)) return it.price; } catch (e) {} } if (!id) return null; var all = document.querySelectorAll("script[data-analytics-item]"); for (var k = 0; k < all.length; k++) { try { var o = JSON.parse(all[k].textContent); if (o && typeof o.price === "number" && String(o.variant_id) === id) return o.price; } catch (e) {} } return null; /* */},"scope":"LOCAL"},{"id":"0959c32f-c08a-48bf-b8eb-06da13edb4f5","name":"Moen_ATC_Quantity","type":"FUNCTION","valueType":"INTEGER","function":function(event){ var btn = event && event.target && event.target.closest ? event.target.closest("button") : null; var form = btn && btn.form; var q = form && form.elements.quantity ? parseInt(form.elements.quantity.value, 10) : 1; return q > 0 ? q : 1; /* */},"scope":"LOCAL"},{"id":"2a4f7212-161c-472f-b762-fbc0c6bea50d","name":"Moen_ATC_SKU","type":"FUNCTION","valueType":"TEXT","function":function(event){ if (!event || !event.target || !event.target.closest) return ""; var btn = event.target.closest("button"); var form = btn && btn.form; var id = form && form.elements.id ? String(form.elements.id.value) : ""; var root = event.target.closest("[data-analytics-card]") || (form && form.closest("main-product")) || event.target.closest("main-product"); if (!root) return ""; var list = root.querySelectorAll("script[data-selected-variant]"); for (var i = 0; i < list.length; i++) { try { var v = JSON.parse(list[i].textContent); if (v && v.sku && (!id || String(v.id) === id)) return String(v.sku); } catch (e) {} } var bv = root.querySelector("[data-bv-product-id]"); return bv ? String(bv.getAttribute("data-bv-product-id")) : ""; /* */},"scope":"LOCAL"},{"id":"e65a7537-b1c9-40cc-9d3a-1e210c273c82","name":"Moen_Cart_LineQuantity","type":"FUNCTION","valueType":"INTEGER","function":function(event){ if (!event || !event.target || !event.target.closest) return null; var line = event.target.closest("[id^=\"CartDrawer-Item-\"]"); var input = line ? line.querySelector("input[name=\"updates[]\"]") : null; var q = input ? parseInt(input.value, 10) : NaN; return q > 0 ? q : 1; /* */},"scope":"LOCAL"},{"id":"15393d83-66dd-4b82-a4ba-73522aac17f9","name":"Moen_Cart_Price","type":"FUNCTION","valueType":"DECIMAL","function":function(event){ if (!event || !event.target || !event.target.closest) return null; var line = event.target.closest("[id^=\"CartDrawer-Item-\"]"); var k = line ? line.querySelector("[key]") : null; var key = k ? k.getAttribute("key") : ""; var items = (window.__CART__ && window.__CART__.items) || []; for (var i = 0; i < items.length; i++) { if (items[i].key === key && typeof items[i].final_price === "number") return items[i].final_price / 100; } return null; /* */},"scope":"LOCAL"},{"id":"6b5421d1-f27d-4e97-aa61-11b2cf5ea340","name":"Moen_Cart_QuantityDelta","type":"FUNCTION","valueType":"INTEGER","function":function(event){ var b = event && event.target && event.target.closest ? event.target.closest("button") : null; if (b && b.name === "minus") return -1; return 1; /* */},"scope":"LOCAL"},{"id":"2b2b8a95-38bf-4d84-b02c-c2d0f4cda128","name":"Moen_Cart_SKU","type":"FUNCTION","valueType":"TEXT","function":function(event){ if (!event || !event.target || !event.target.closest) return ""; var line = event.target.closest("[id^=\"CartDrawer-Item-\"]"); if (!line) return ""; var q = line.querySelector("quantity-input[data-sku]"); if (q) return String(q.getAttribute("data-sku")); var k = line.querySelector("[key]"); var key = k ? k.getAttribute("key") : ""; var items = (window.__CART__ && window.__CART__.items) || []; for (var i = 0; i < items.length; i++) { if (items[i].key === key && items[i].sku) return String(items[i].sku); } return ""; /* */},"scope":"LOCAL"},{"id":"5ae2ce17-ec1b-40cf-b721-e6572f739188","name":"Moen_Click_ButtonEnabled","type":"FUNCTION","valueType":"BOOLEAN","function":function(event){ var b = event && event.target && event.target.closest ? event.target.closest("button") : null; return !!b && !b.disabled; /* */},"scope":"LOCAL"},{"id":"af88b22b-7ed9-4d2a-b899-70e97aa3afa8","name":"Moen_Click_InSearchResults","type":"FUNCTION","valueType":"BOOLEAN","function":function(event){ if (!event || !event.target || !event.target.closest) return false; return !!event.target.closest("ul[data-analytics-list-id=search_results]"); /* */},"scope":"LOCAL"},{"id":"bab9bd1e-7bb7-451e-ab7e-0d81ed7fe567","name":"Moen_Click_InSuggestResults","type":"FUNCTION","valueType":"BOOLEAN","function":function(event){ if (!event || !event.target || !event.target.closest) return false; var s = document.querySelector("bloomreach-suggest"); return !!event.target.closest("ul[data-suggest-grid]") && !!s && s.getAttribute("data-suggest-state") === "results"; /* */},"scope":"LOCAL"},{"id":"51cff814-c9ad-4c05-849c-485c16b9887e","name":"Moen_Click_IsProductLink","type":"FUNCTION","valueType":"BOOLEAN","function":function(event){ var a = event && event.target && event.target.closest ? event.target.closest("a[href]") : null; return !!a && a.pathname.indexOf("/products/") !== -1; /* */},"scope":"LOCAL"},{"id":"cde6ed7d-25e3-4abc-9c7e-51767ce7c319","name":"Moen_Clickout_TargetUrl","type":"FUNCTION","valueType":"TEXT","function":function(event){ if (!event || !event.target || !event.target.closest) return ""; var a = event.target.closest("a[href]"); if (a) return String(a.href); var c = event.target.closest("[data-analytics-card][product-url]") || event.target.closest("main-product[product-url]"); var u = c ? c.getAttribute("product-url") : ""; return u ? new URL(u, window.location.origin).href : ""; /* */},"scope":"LOCAL"},{"id":"491f0feb-e334-45f0-8552-9a4c2a603f09","name":"Moen_CurrencyCode","type":"FUNCTION","valueType":"TEXT","function":function(event){ var s = window.Shopify; if (s && s.currency && s.currency.active) return String(s.currency.active).toUpperCase(); if (window.__CART__ && window.__CART__.currency) return String(window.__CART__.currency).toUpperCase(); return ""; /* */},"scope":"LOCAL"},{"id":"1c7a71fc-0089-4293-b2d8-7a5b4b801ea7","name":"Moen_IsNewPageView","type":"FUNCTION","valueType":"BOOLEAN","function":function(event){ var u = new URL(window.location.href); u.searchParams.delete("modals"); var key = u.pathname + u.search; var g = window.__zvMoenPageView; if (!g || g.key !== key) { g = window.__zvMoenPageView = { key: key, open: true }; setTimeout(function () { g.open = false; }, 0); } return g.open; /* */},"scope":"LOCAL"},{"id":"f3fe367e-749e-4e7b-95da-89b5a597523c","name":"Moen_IsSearchPage","type":"FUNCTION","valueType":"BOOLEAN","function":function(event){ return /^\/search\/?$/.test(window.location.pathname); /* */},"scope":"LOCAL"},{"id":"fa5086fa-000f-4f0c-9fa2-395f222ddc6b","name":"Moen_Locale","type":"FUNCTION","valueType":"TEXT","function":function(event){ var tag = (document.documentElement.lang || "").trim(); if (tag.indexOf("-") !== -1 || tag.indexOf("_") !== -1) return tag; var s = window.Shopify || {}; var lang = tag || String(s.locale || "").split(/[-_]/)[0]; if (lang && s.country) return lang + "-" + s.country; return ""; /* */},"scope":"LOCAL"},{"id":"a4786aee-901d-497c-b4e5-b18536985331","name":"Moen_NamedReferral","type":"CONSTANT","valueType":"TEXT","value":"SEARCH","scope":"LOCAL"},{"id":"8d319105-c0ac-4d23-923c-fb2d8fd8247a","name":"Moen_PDP_Price","type":"FUNCTION","valueType":"DECIMAL","function":function(event){ var list = document.querySelectorAll("main-product script[data-selected-variant]"); for (var i = 0; i < list.length; i++) { if (list[i].closest("[data-analytics-card], #quick-shop-modal")) continue; try { var v = JSON.parse(list[i].textContent); if (v && typeof v.price === "number") return v.price / 100; } catch (e) {} } var pdp = document.querySelectorAll("main-product script[data-analytics-pdp]"); for (var j = 0; j < pdp.length; j++) { if (pdp[j].closest("[data-analytics-card], #quick-shop-modal")) continue; try { var p = JSON.parse(pdp[j].textContent); if (p && typeof p.price === "number") return p.price; } catch (e) {} } return null; /* */},"scope":"LOCAL"},{"id":"d6272954-281f-4c2f-921e-d1b31a7827e1","name":"Moen_PDP_SKU","type":"FUNCTION","valueType":"TEXT","function":function(event){ var list = document.querySelectorAll("main-product script[data-selected-variant]"); for (var i = 0; i < list.length; i++) { if (list[i].closest("[data-analytics-card], #quick-shop-modal")) continue; try { var v = JSON.parse(list[i].textContent); if (v && v.sku) return String(v.sku); } catch (e) {} } var pdp = document.querySelectorAll("main-product script[data-analytics-pdp]"); for (var j = 0; j < pdp.length; j++) { if (pdp[j].closest("[data-analytics-card], #quick-shop-modal")) continue; try { var p = JSON.parse(pdp[j].textContent); if (p && p.sku) return String(p.sku); } catch (e) {} } return ""; /* */},"scope":"LOCAL"},{"id":"607294f5-03e3-4ace-af6a-009657bb0bcc","name":"Moen_Purchase_CurrencyCode","type":"FUNCTION","valueType":"TEXT","function":function(event){ var dl = window.dataLayer || []; var found = { id: "", currency: "", items: null }; for (var i = dl.length - 1; i >= 0; i--) { var e = dl[i], p = null; if (!e) continue; if (e[0] === "event" && (e[1] === "purchase" || e[1] === "checkout_completed")) p = e[2]; else if (e.event === "purchase" || e.event === "checkout_completed" || e.event === "dl_purchase") p = e.ecommerce || e.data || e; else if (e.ecommerce && e.ecommerce.purchase) p = e.ecommerce; if (!p) continue; if (p.checkout) p = p.checkout; var ua = p.purchase || {}; var af = ua.actionField || {}; if (!found.id) found.id = String(p.transaction_id || p.transactionId || p.orderId || p.order_id || af.id || (p.order && p.order.id) || ""); if (!found.currency) found.currency = String(p.currency || p.currencyCode || ""); if (!found.items) { var list = p.items || ua.products || p.products || p.lineItems; if (list && list.length) found.items = list; } if (found.id && found.currency && found.items) break; } return found.currency.toUpperCase(); /* */},"scope":"LOCAL"},{"id":"81b4167e-9da1-4709-b573-9fb9ce87a80a","name":"Moen_Purchase_Products","type":"FUNCTION","valueType":"LIST","function":function(event){ var dl = window.dataLayer || []; var found = { id: "", currency: "", items: null }; for (var i = dl.length - 1; i >= 0; i--) { var e = dl[i], p = null; if (!e) continue; if (e[0] === "event" && (e[1] === "purchase" || e[1] === "checkout_completed")) p = e[2]; else if (e.event === "purchase" || e.event === "checkout_completed" || e.event === "dl_purchase") p = e.ecommerce || e.data || e; else if (e.ecommerce && e.ecommerce.purchase) p = e.ecommerce; if (!p) continue; if (p.checkout) p = p.checkout; var ua = p.purchase || {}; var af = ua.actionField || {}; if (!found.id) found.id = String(p.transaction_id || p.transactionId || p.orderId || p.order_id || af.id || (p.order && p.order.id) || ""); if (!found.currency) found.currency = String(p.currency || p.currencyCode || ""); if (!found.items) { var list = p.items || ua.products || p.products || p.lineItems; if (list && list.length) found.items = list; } if (found.id && found.currency && found.items) break; } if (!found.items) return null; var out = []; for (var j = 0; j < found.items.length; j++) { var it = found.items[j]; if (!it) continue; var v = it.variant && typeof it.variant === "object" ? it.variant : null; var sku = it.sku || it.item_sku || (v ? v.sku : (it.item_id || it.id)); if (!sku) continue; var raw = it.price !== undefined ? it.price : (it.pricePerUnit !== undefined ? it.pricePerUnit : (v && v.price)); if (raw && typeof raw === "object") raw = raw.amount; if (raw === null || raw === undefined || raw === "") continue; var price = Number(raw); if (!isFinite(price) || price < 0) continue; var qty = parseInt(it.quantity, 10); if (!isFinite(qty) || qty <= 0) qty = 1; out.push({ sku: String(sku), name: String(it.item_name || it.name || it.title || ""), pricePerUnit: price, quantity: qty }); } return out.length ? out : null; /* */},"scope":"LOCAL"},{"id":"2294aa65-973a-494b-834f-ed5a87965b2c","name":"Moen_Purchase_TransactionId","type":"FUNCTION","valueType":"TEXT","function":function(event){ var dl = window.dataLayer || []; var found = { id: "", currency: "", items: null }; for (var i = dl.length - 1; i >= 0; i--) { var e = dl[i], p = null; if (!e) continue; if (e[0] === "event" && (e[1] === "purchase" || e[1] === "checkout_completed")) p = e[2]; else if (e.event === "purchase" || e.event === "checkout_completed" || e.event === "dl_purchase") p = e.ecommerce || e.data || e; else if (e.ecommerce && e.ecommerce.purchase) p = e.ecommerce; if (!p) continue; if (p.checkout) p = p.checkout; var ua = p.purchase || {}; var af = ua.actionField || {}; if (!found.id) found.id = String(p.transaction_id || p.transactionId || p.orderId || p.order_id || af.id || (p.order && p.order.id) || ""); if (!found.currency) found.currency = String(p.currency || p.currencyCode || ""); if (!found.items) { var list = p.items || ua.products || p.products || p.lineItems; if (list && list.length) found.items = list; } if (found.id && found.currency && found.items) break; } var id = found.id; if (id.indexOf("gid://") === 0) id = id.split("/").pop(); return id; /* */},"scope":"LOCAL"},{"id":"e14c1fcd-e035-4433-9024-29718b3ef8c3","name":"Moen_QuickShop_Origin","type":"FUNCTION","valueType":"TEXT","function":function(event){ var m = document.querySelector("#quick-shop-modal main-product[product-url]"); if (!m) return ""; var path = String(m.getAttribute("product-url")).split("?")[0].toLowerCase(); var s = document.querySelector("bloomreach-suggest"); var popup = document.getElementById("bloomreach-search-dialog"); var cards = document.querySelectorAll("[data-analytics-card][product-url]"); var inGrid = false, inSuggest = false; for (var i = 0; i < cards.length; i++) { if (String(cards[i].getAttribute("product-url")).split("?")[0].toLowerCase() !== path) continue; if (cards[i].closest("ul[data-suggest-grid]")) inSuggest = true; if (cards[i].closest("ul[data-analytics-list-id=search_results]")) inGrid = true; } if (inSuggest && popup && popup.hasAttribute("open") && s && s.getAttribute("data-suggest-state") === "results") return "popup"; if (inGrid) return "results"; return ""; /* */},"scope":"LOCAL"},{"id":"06f4c1d3-fea8-4a9a-a222-13c3e1a38702","name":"Moen_Search_InputHasText","type":"FUNCTION","valueType":"BOOLEAN","function":function(event){ return !!(event && event.target && String(event.target.value || "").trim()); /* */},"scope":"LOCAL"},{"id":"272ee6ed-b0f3-440a-b56c-525d4fd6987e","name":"Moen_Search_Phrase","type":"FUNCTION","valueType":"TEXT","function":function(event){ var m = /[?&]q=([^&]*)/.exec(window.location.search); if (!m) return ""; try { return decodeURIComponent(m[1].replace(/\+/g, " ")).trim(); } catch (e) { return m[1]; } /* */},"scope":"LOCAL"},{"id":"7a2f747d-1427-4fbb-aac0-fe7f6a2913fa","name":"Moen_SR_Page_HasResults","type":"FUNCTION","valueType":"BOOLEAN","function":function(event){ return !!document.querySelector("ul[data-analytics-list-id=search_results] [data-analytics-card]"); /* */},"scope":"LOCAL"},{"id":"4b48ed68-4fae-46b8-9ddf-67179203f824","name":"Moen_SR_Page_IsEmpty","type":"FUNCTION","valueType":"BOOLEAN","function":function(event){ var e = document.querySelector("bloomreach-listing-grid [data-listing-empty]"); return !!e && !e.hidden; /* */},"scope":"LOCAL"},{"id":"04723323-c718-4040-b751-3a3f48d219a2","name":"Moen_SR_Page_SKUs","type":"FUNCTION","valueType":"LIST","function":function(event){ var out = []; var cards = document.querySelectorAll("ul[data-analytics-list-id=search_results] [data-analytics-card]"); for (var i = 0; i < cards.length; i++) { var b = cards[i].querySelector("[data-bv-product-id]"); if (b) out.push(String(b.getAttribute("data-bv-product-id"))); } return out; /* */},"scope":"LOCAL"},{"id":"be729b94-605b-46ba-9831-59f43fbd9249","name":"Moen_SR_Popup_IsEmpty","type":"FUNCTION","valueType":"BOOLEAN","function":function(event){ var s = document.querySelector("bloomreach-suggest"); return !!s && s.getAttribute("data-suggest-state") === "empty"; /* */},"scope":"LOCAL"},{"id":"0d8b881a-b8ce-476d-9e98-b9bf390d2cb3","name":"Moen_SR_Popup_Label","type":"FUNCTION","valueType":"TEXT","function":function(event){ var s = document.querySelector("bloomreach-suggest"); return s && s.getAttribute("data-suggest-state") === "empty" ? "Empty search result popup" : undefined; /* */},"scope":"LOCAL"},{"id":"5bfcdbdf-84f6-4ed3-a635-90cfb1755634","name":"Moen_SR_Popup_SKUs","type":"FUNCTION","valueType":"LIST","function":function(event){ var out = []; var s = document.querySelector("bloomreach-suggest"); var state = s && s.getAttribute("data-suggest-state"); if (state === "empty") return out; if (state !== "results") return null; var cards = document.querySelectorAll("ul[data-suggest-grid] [data-analytics-card]"); for (var i = 0; i < cards.length; i++) { var b = cards[i].querySelector("[data-bv-product-id]"); if (b) out.push(String(b.getAttribute("data-bv-product-id"))); } return out.length ? out : null; /* */},"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":"72872401-1a85-464d-a4bd-1725b18aa2ba","rows":[{"id":"5d73a31f-9da5-4c5e-93b1-70621a277d0a","rowType":"EVENT","trigger":{"id":"daf1341d-0ae3-4fb3-9544-27b5f0e31210","name":"Moen_NonPDP_PageLoad","type":"PAGE_LOAD","delayMs":0,"groups":[{"id":"2b2e5451-236e-4e86-a1d4-252e4cb75534","rows":[{"valueType":"TEXT","id":"594d9dff-3e57-46c3-b174-fab89c339c14","variableId":"75cf8fd1-fafe-44be-9806-23ff3017c9c1","value":"/products/","operator":"DOES_NOT_CONTAIN"},{"valueType":"BOOLEAN","id":"520aa84c-13f8-4e1a-875d-e0a6f0687b40","variableId":"1c7a71fc-0089-4293-b2d8-7a5b4b801ea7","value":"true","operator":"EQUALS"}]}]},"action":{"type":"PAGE_VISITED"},"fields":[{"id":"8052008a-58f9-4e2e-966d-11402fda162a","variableId":"fa5086fa-000f-4f0c-9fa2-395f222ddc6b","fieldName":"locale"}]},{"id":"a5f34bbd-789b-49b2-adae-74fb53e8c937","rowType":"EVENT","trigger":{"id":"c2764daa-d58c-4764-a2fa-be5250ba0de5","name":"Moen_PDP_PageLoad","type":"PAGE_LOAD","delayMs":0,"groups":[{"id":"cc09a59b-01af-44e4-b11f-e99f51cb6e99","rows":[{"valueType":"TEXT","id":"5f7b5c3a-18c4-4986-b5d5-cfda3b3b40db","variableId":"75cf8fd1-fafe-44be-9806-23ff3017c9c1","value":"/products/","operator":"CONTAINS"},{"valueType":"BOOLEAN","id":"5f207f3c-ef6e-450f-beac-47c66ade864e","variableId":"1c7a71fc-0089-4293-b2d8-7a5b4b801ea7","value":"true","operator":"EQUALS"}]}]},"action":{"type":"PDP_VISITED"},"fields":[{"id":"c80841a3-bb87-4150-bca3-20fbc4cd77b4","variableId":"fa5086fa-000f-4f0c-9fa2-395f222ddc6b","fieldName":"locale"},{"id":"0815f646-edd9-4231-b60e-1576a2d3dcfd","variableId":"d6272954-281f-4c2f-921e-d1b31a7827e1","fieldName":"sku"},{"id":"9c095615-d062-4d97-8085-1c627e408fc1","variableId":"8d319105-c0ac-4d23-923c-fb2d8fd8247a","fieldName":"price"},{"id":"7bd924c0-49bb-429c-bda1-594f2e01e4d4","variableId":"491f0feb-e334-45f0-8552-9a4c2a603f09","fieldName":"currencyCode"}]},{"id":"c0657830-bed8-413c-819a-87745818a210","rowType":"EVENT","trigger":{"id":"b4479eed-e29d-4e00-a5e6-0634c010a8ae","name":"Moen_ATC_ProductForm_Click","type":"CLICK","delayMs":0,"groups":[{"id":"9330a0db-b20a-4abd-af3b-e621ffcaa667","rows":[{"valueType":"EVENT","id":"d121393b-c5fd-4485-a793-d7cf2c2ba85f","variableId":"ad6080ea-719b-4732-952f-ef4f1ce45e02","value":"button[name=add][id*=\"-template--\"], button[name=add][id*=\"-template--\"] *","operator":"MATCHES_CSS_SELECTOR"},{"valueType":"BOOLEAN","id":"b89c13f7-e84c-4bf4-a100-0003380f0835","variableId":"5ae2ce17-ec1b-40cf-b721-e6572f739188","value":"true","operator":"EQUALS"}]}]},"action":{"type":"ADD_TO_CART"},"fields":[{"id":"fde223ee-501c-4361-989c-054243bd7421","variableId":"fa5086fa-000f-4f0c-9fa2-395f222ddc6b","fieldName":"locale"},{"id":"9adde536-481c-405c-b509-565392bf04e7","variableId":"2a4f7212-161c-472f-b762-fbc0c6bea50d","fieldName":"sku"},{"id":"e28cac3f-6889-46bd-9f24-1e906141fcbb","variableId":"3143b69e-2887-40db-b044-5f4368f0a545","fieldName":"price"},{"id":"2cd07779-45cd-4ebf-96b0-138e7bca5e92","variableId":"491f0feb-e334-45f0-8552-9a4c2a603f09","fieldName":"currencyCode"},{"id":"a24cac60-236a-4f95-a217-505ef32f0086","variableId":"0959c32f-c08a-48bf-b8eb-06da13edb4f5","fieldName":"quantity"}]},{"id":"e59bbaa9-3b93-4e54-a7ca-5c07e5b3433b","rowType":"EVENT","trigger":{"id":"d3e6cd52-b303-402a-a81f-fda28495b3ce","name":"Moen_ATC_ProductCard_Click","type":"CLICK","delayMs":0,"groups":[{"id":"fbdc6750-1883-447e-a4f1-e237adcef51f","rows":[{"valueType":"EVENT","id":"8bc57004-c73b-4f9d-8d44-ccfe52253a30","variableId":"ad6080ea-719b-4732-952f-ef4f1ce45e02","value":"[data-analytics-card] button[name=add], [data-analytics-card] button[name=add] *","operator":"MATCHES_CSS_SELECTOR"},{"valueType":"BOOLEAN","id":"4e9174aa-ac2c-43aa-9595-4d0793bb9571","variableId":"5ae2ce17-ec1b-40cf-b721-e6572f739188","value":"true","operator":"EQUALS"}]}]},"action":{"type":"ADD_TO_CART"},"fields":[{"id":"4c1acd27-cca3-4970-b7a9-88b1e6e5b72a","variableId":"fa5086fa-000f-4f0c-9fa2-395f222ddc6b","fieldName":"locale"},{"id":"b057cf22-e500-4f75-9df1-a973289529af","variableId":"2a4f7212-161c-472f-b762-fbc0c6bea50d","fieldName":"sku"},{"id":"60fa9101-3832-42c2-9741-54ccd25981e0","variableId":"3143b69e-2887-40db-b044-5f4368f0a545","fieldName":"price"},{"id":"d369ae91-8d33-44cf-95b3-fe2310e1c9c2","variableId":"491f0feb-e334-45f0-8552-9a4c2a603f09","fieldName":"currencyCode"},{"id":"d9d53323-ddd7-41a9-8c94-51a73a42aacb","variableId":"0959c32f-c08a-48bf-b8eb-06da13edb4f5","fieldName":"quantity"}]},{"id":"4dc56cf8-f399-461b-a794-7cb8b0937cc7","rowType":"EVENT","trigger":{"id":"0422aa66-fb37-474f-992b-8649ed327b25","name":"Moen_SearchResults_PageLoad","type":"PAGE_LOAD","delayMs":0,"groups":[{"id":"b21cc309-4257-45ed-a706-f4559439e96c","rows":[{"valueType":"BOOLEAN","id":"ac10524c-1375-436b-8125-29c62dbf69b1","variableId":"f3fe367e-749e-4e7b-95da-89b5a597523c","value":"true","operator":"EQUALS"},{"valueType":"TEXT","id":"555b85ba-107e-4de5-a04b-5426ded1239a","variableId":"75cf8fd1-fafe-44be-9806-23ff3017c9c1","value":"q=","operator":"CONTAINS"},{"valueType":"BOOLEAN","id":"073562d6-9570-41a4-925b-9cebfe06881b","variableId":"1c7a71fc-0089-4293-b2d8-7a5b4b801ea7","value":"true","operator":"EQUALS"}]}]},"action":{"type":"SEARCH"},"fields":[{"id":"7d431625-89a5-4e32-9e97-cd61d40251e4","variableId":"fa5086fa-000f-4f0c-9fa2-395f222ddc6b","fieldName":"locale"},{"id":"67f5aafc-8d4c-413d-95cf-b4c1e810a1cd","variableId":"272ee6ed-b0f3-440a-b56c-525d4fd6987e","fieldName":"eventLabel"}]},{"id":"8db77a1b-83aa-4c7a-bc65-a72435d865ce","rowType":"EVENT","trigger":{"id":"9d939f43-d294-4fd0-b7e8-858b5bdca982","name":"Moen_SearchResults_Empty_PageLoad","type":"PAGE_LOAD","delayMs":3000,"groups":[{"id":"66336cea-5e58-41f3-9066-b0dc6dec6d76","rows":[{"valueType":"BOOLEAN","id":"6cc0338d-844e-4961-98ca-f1546b904431","variableId":"f3fe367e-749e-4e7b-95da-89b5a597523c","value":"true","operator":"EQUALS"},{"valueType":"BOOLEAN","id":"f10cfeb5-2370-4ab8-907e-297e8a629121","variableId":"4b48ed68-4fae-46b8-9ddf-67179203f824","value":"true","operator":"EQUALS"},{"valueType":"BOOLEAN","id":"9d2adc39-1cd8-455b-85d4-ef83a049287d","variableId":"1c7a71fc-0089-4293-b2d8-7a5b4b801ea7","value":"true","operator":"EQUALS"}]}]},"action":{"type":"SEARCH_RESULT"},"fields":[{"id":"a3b818fd-0412-4cc5-b019-fd86a572ec4b","variableId":"fa5086fa-000f-4f0c-9fa2-395f222ddc6b","fieldName":"locale"},{"id":"629c0fb2-4826-4cb9-a45a-d72320b8b2bf","variableId":"4b48ed68-4fae-46b8-9ddf-67179203f824","fieldName":"isEmpty"},{"id":"95b6efd4-eb15-41c4-9e92-99bcbd379c6f","variableId":"04723323-c718-4040-b751-3a3f48d219a2","fieldName":"skus"},{"id":"65144f9b-7a14-4cc6-b68b-f8cff0199923","variableId":"555e5793-ef14-46c2-9a45-d01e45f1c492","fieldName":"eventLabel"}]},{"id":"e8b74391-4619-4bea-b537-c581be38243b","rowType":"EVENT","trigger":{"id":"b126da71-4cb0-4a53-9b3f-1bc0fcd41f34","name":"Moen_SearchResults_NonEmpty_PageLoad","type":"PAGE_LOAD","delayMs":3000,"groups":[{"id":"2c84c392-b970-44ab-9535-7e5a29d63c71","rows":[{"valueType":"BOOLEAN","id":"0b5f7864-5033-4e54-bc29-e1947eaf56b8","variableId":"f3fe367e-749e-4e7b-95da-89b5a597523c","value":"true","operator":"EQUALS"},{"valueType":"BOOLEAN","id":"d38a55b7-23bd-4f59-96d0-22e212d50219","variableId":"7a2f747d-1427-4fbb-aac0-fe7f6a2913fa","value":"true","operator":"EQUALS"},{"valueType":"BOOLEAN","id":"9d30784c-f70a-4759-aad3-bec258cf4fa9","variableId":"1c7a71fc-0089-4293-b2d8-7a5b4b801ea7","value":"true","operator":"EQUALS"}]}]},"action":{"type":"SEARCH_RESULT"},"fields":[{"id":"dbacec3b-af57-4d66-9a9b-3942f785826b","variableId":"fa5086fa-000f-4f0c-9fa2-395f222ddc6b","fieldName":"locale"},{"id":"01ef3a2d-22b7-41f3-8cf4-7d2f1d5458ed","variableId":"4b48ed68-4fae-46b8-9ddf-67179203f824","fieldName":"isEmpty"},{"id":"f6ec44ff-61f5-4faf-8498-e55f88193b06","variableId":"04723323-c718-4040-b751-3a3f48d219a2","fieldName":"skus"}]},{"id":"e4094616-ca80-40db-aadf-c799b49aab43","rowType":"EVENT","trigger":{"id":"4072397e-7e58-4f5a-8feb-98a072c6f3e5","name":"Moen_SearchPopup_Input","type":"INPUT","delayMs":1500,"groups":[{"id":"d2d79047-c73c-4fd4-b682-bc7ac5b7e95f","rows":[{"valueType":"EVENT","id":"be63f5f5-0efc-40ce-9418-186f590c9914","variableId":"ad6080ea-719b-4732-952f-ef4f1ce45e02","value":"#bloomreach-search-dialog input[type=search]","operator":"MATCHES_CSS_SELECTOR"},{"valueType":"BOOLEAN","id":"96c0f16e-f4d0-4121-b1b8-cedc268277f4","variableId":"06f4c1d3-fea8-4a9a-a222-13c3e1a38702","value":"true","operator":"EQUALS"}]}]},"action":{"type":"SEARCH_RESULT"},"fields":[{"id":"7bcccdc2-e9a4-48ce-a12d-a58df10fc394","variableId":"fa5086fa-000f-4f0c-9fa2-395f222ddc6b","fieldName":"locale"},{"id":"94e63854-b50b-45ac-9cc3-24000dc16049","variableId":"be729b94-605b-46ba-9831-59f43fbd9249","fieldName":"isEmpty"},{"id":"4c24bcc9-f2a7-45eb-8464-f4975a6ad22a","variableId":"5bfcdbdf-84f6-4ed3-a635-90cfb1755634","fieldName":"skus"},{"id":"fdf2fa8d-ec19-44cb-8666-d900c68c6134","variableId":"0d8b881a-b8ce-476d-9e98-b9bf390d2cb3","fieldName":"eventLabel"}]},{"id":"27932530-be67-414b-a762-b4994db271b5","rowType":"EVENT","trigger":{"id":"a7044656-eadd-4dd6-9a72-6bcd663bc04d","name":"Moen_SearchPopup_PDP_Click","type":"CLICK","delayMs":0,"groups":[{"id":"ba23a362-3cd8-4afa-a27b-7279a9da5dbd","rows":[{"valueType":"EVENT","id":"ae882036-de4a-4099-bb84-79cdf61642c5","variableId":"ad6080ea-719b-4732-952f-ef4f1ce45e02","value":"[data-analytics-card] a, [data-analytics-card] a *","operator":"MATCHES_CSS_SELECTOR"},{"valueType":"BOOLEAN","id":"f1c9fe95-9c58-4c8a-bda7-1db6ebc022c3","variableId":"bab9bd1e-7bb7-451e-ab7e-0d81ed7fe567","value":"true","operator":"EQUALS"},{"valueType":"BOOLEAN","id":"f863c3e7-c0a1-452e-a403-e262fd9bd617","variableId":"51cff814-c9ad-4c05-849c-485c16b9887e","value":"true","operator":"EQUALS"}]}]},"action":{"type":"CLICKOUT"},"fields":[{"id":"9cfd1475-5258-4be4-82e1-ca9161d3d4ce","variableId":"fa5086fa-000f-4f0c-9fa2-395f222ddc6b","fieldName":"locale"},{"id":"9695da78-f234-44b2-95c5-d9c7b2a3d9b3","variableId":"cde6ed7d-25e3-4abc-9c7e-51767ce7c319","fieldName":"targetUrl"},{"id":"ee6e9a5a-5e37-4de1-b876-52f3d148b258","variableId":"a4786aee-901d-497c-b4e5-b18536985331","fieldName":"namedReferral"},{"id":"0b0886f5-7989-486c-b0d6-3b25434d824a","variableId":"2e141436-b43c-40d6-bbed-64cfdca7f222","fieldName":"eventLabel"}]},{"id":"19ab5562-5fc0-46a8-9d82-6fd386870ac4","rowType":"EVENT","trigger":{"id":"ddbff277-baab-48eb-973d-f7a032fa9cd6","name":"Moen_SearchResults_PDP_Click","type":"CLICK","delayMs":0,"groups":[{"id":"6bd45d1c-ae04-49ad-b9c5-1dd79eed4490","rows":[{"valueType":"EVENT","id":"897199d2-0ac6-4592-baf3-05d7ee05760e","variableId":"ad6080ea-719b-4732-952f-ef4f1ce45e02","value":"[data-analytics-card] a, [data-analytics-card] a *","operator":"MATCHES_CSS_SELECTOR"},{"valueType":"BOOLEAN","id":"58210728-0f09-4cf3-8d13-b3b7786dbb18","variableId":"af88b22b-7ed9-4d2a-b899-70e97aa3afa8","value":"true","operator":"EQUALS"},{"valueType":"BOOLEAN","id":"e7d1658e-5d63-4e03-afdc-b71782d3c734","variableId":"51cff814-c9ad-4c05-849c-485c16b9887e","value":"true","operator":"EQUALS"}]}]},"action":{"type":"CLICKOUT"},"fields":[{"id":"4bfe74f6-ebe4-42ce-ad6c-fc7897786d62","variableId":"fa5086fa-000f-4f0c-9fa2-395f222ddc6b","fieldName":"locale"},{"id":"1d5fb192-2fcf-42b8-870f-9c1759622a85","variableId":"cde6ed7d-25e3-4abc-9c7e-51767ce7c319","fieldName":"targetUrl"},{"id":"d7a4102b-ecb4-4dea-9830-35115d86cad4","variableId":"a4786aee-901d-497c-b4e5-b18536985331","fieldName":"namedReferral"},{"id":"ae888aed-7916-4632-a494-379a34ee82c6","variableId":"2e141436-b43c-40d6-bbed-64cfdca7f222","fieldName":"eventLabel"}]},{"id":"3cb7a77a-da10-4520-810b-14098b6b1c89","rowType":"EVENT","trigger":{"id":"ff07cdf9-1aac-42a2-93ea-c5f41d2e8e75","name":"Moen_SearchPopup_ATC_Click","type":"CLICK","delayMs":0,"groups":[{"id":"cdb4b36c-e367-4bec-b860-04bd1909214e","rows":[{"valueType":"EVENT","id":"3e89b6ea-4a7d-4d47-96ac-b0db2f743438","variableId":"ad6080ea-719b-4732-952f-ef4f1ce45e02","value":"[data-analytics-card] button[name=add], [data-analytics-card] button[name=add] *","operator":"MATCHES_CSS_SELECTOR"},{"valueType":"BOOLEAN","id":"21615c0b-0e91-4394-81e6-2742e06d20c0","variableId":"bab9bd1e-7bb7-451e-ab7e-0d81ed7fe567","value":"true","operator":"EQUALS"},{"valueType":"BOOLEAN","id":"97e4db3a-528a-4dd6-8c30-684b4813dd88","variableId":"5ae2ce17-ec1b-40cf-b721-e6572f739188","value":"true","operator":"EQUALS"}]}]},"action":{"type":"CLICKOUT"},"fields":[{"id":"f379dbbc-fc68-46ed-adee-8903886d0de1","variableId":"fa5086fa-000f-4f0c-9fa2-395f222ddc6b","fieldName":"locale"},{"id":"6870b521-1c5c-49b3-b911-4fd26279f7eb","variableId":"cde6ed7d-25e3-4abc-9c7e-51767ce7c319","fieldName":"targetUrl"},{"id":"cc7a7e2c-52d2-4907-9e19-dde3c0a9f6ff","variableId":"a4786aee-901d-497c-b4e5-b18536985331","fieldName":"namedReferral"},{"id":"5ef93d8e-6d3d-4089-ba1d-0f99448e2f11","variableId":"b73b1d5c-2232-410e-be49-48ec6609f4d7","fieldName":"eventLabel"}]},{"id":"2ac759b8-d7ea-4da2-ad5b-2252876c10f5","rowType":"EVENT","trigger":{"id":"2894c755-c98e-4152-8ed3-67f899f5971b","name":"Moen_SearchPopup_QuickView_ATC_Click","type":"CLICK","delayMs":0,"groups":[{"id":"0f7627eb-1935-41df-9607-3d3e3053fd15","rows":[{"valueType":"EVENT","id":"2b0a98aa-4756-4b1b-9a55-0e1e5b0496fc","variableId":"ad6080ea-719b-4732-952f-ef4f1ce45e02","value":"#quick-shop-modal button[name=add], #quick-shop-modal button[name=add] *","operator":"MATCHES_CSS_SELECTOR"},{"valueType":"TEXT","id":"6cde1ba8-cf90-49e1-a3de-77a28fdb1761","variableId":"e14c1fcd-e035-4433-9024-29718b3ef8c3","value":"popup","operator":"EQUALS"},{"valueType":"BOOLEAN","id":"c24dc780-77dc-4db8-90b2-8f707f84bd0d","variableId":"5ae2ce17-ec1b-40cf-b721-e6572f739188","value":"true","operator":"EQUALS"}]}]},"action":{"type":"CLICKOUT"},"fields":[{"id":"498274ba-1b0e-4a0a-b092-d4a24b121e28","variableId":"fa5086fa-000f-4f0c-9fa2-395f222ddc6b","fieldName":"locale"},{"id":"52e761e1-ca01-4887-ac07-d0e46f0df517","variableId":"cde6ed7d-25e3-4abc-9c7e-51767ce7c319","fieldName":"targetUrl"},{"id":"463d9e99-f1b7-47ba-8328-a3f0c55fb113","variableId":"a4786aee-901d-497c-b4e5-b18536985331","fieldName":"namedReferral"},{"id":"087f0ed7-91e6-4e29-a3c7-f2446ecc8bbd","variableId":"b73b1d5c-2232-410e-be49-48ec6609f4d7","fieldName":"eventLabel"}]},{"id":"c6b1d0d4-4f3b-4577-8c5d-096d070a2d20","rowType":"EVENT","trigger":{"id":"7f09ba39-0710-475c-b0f5-d431cec3e581","name":"Moen_SearchResults_ATC_Click","type":"CLICK","delayMs":0,"groups":[{"id":"a86c5a5f-904e-41ed-8501-d4639f750bed","rows":[{"valueType":"EVENT","id":"4b6da94e-c16e-40b3-885c-bf7fa24be0c9","variableId":"ad6080ea-719b-4732-952f-ef4f1ce45e02","value":"[data-analytics-card] button[name=add], [data-analytics-card] button[name=add] *","operator":"MATCHES_CSS_SELECTOR"},{"valueType":"BOOLEAN","id":"984d9fe8-2721-4941-ab8e-adc0531df744","variableId":"af88b22b-7ed9-4d2a-b899-70e97aa3afa8","value":"true","operator":"EQUALS"},{"valueType":"BOOLEAN","id":"cc5326d6-8b78-4171-8763-c6313ebc9b1e","variableId":"5ae2ce17-ec1b-40cf-b721-e6572f739188","value":"true","operator":"EQUALS"}]}]},"action":{"type":"CLICKOUT"},"fields":[{"id":"d046a4b0-5ae4-47c1-b408-1308cc97353b","variableId":"fa5086fa-000f-4f0c-9fa2-395f222ddc6b","fieldName":"locale"},{"id":"70cfdced-1a95-4f1d-9eab-9efc607ac565","variableId":"cde6ed7d-25e3-4abc-9c7e-51767ce7c319","fieldName":"targetUrl"},{"id":"43484223-0ca2-4f3d-a7a2-38291c940173","variableId":"a4786aee-901d-497c-b4e5-b18536985331","fieldName":"namedReferral"},{"id":"718e21e0-5f09-4146-98ef-04c7b317ff53","variableId":"f51a1d5b-6bc1-4814-9140-af9516bcbd0d","fieldName":"eventLabel"}]},{"id":"48860f73-ee1e-4752-a2ea-cd14b1839272","rowType":"EVENT","trigger":{"id":"1334f24a-5c51-4b89-b400-b9bba9482c0f","name":"Moen_SearchResults_QuickView_ATC_Click","type":"CLICK","delayMs":0,"groups":[{"id":"04ba2cdf-7ee6-4094-acf1-8810ffb2f6e4","rows":[{"valueType":"EVENT","id":"2988bf64-0fe9-46ef-a412-4404edeaef7a","variableId":"ad6080ea-719b-4732-952f-ef4f1ce45e02","value":"#quick-shop-modal button[name=add], #quick-shop-modal button[name=add] *","operator":"MATCHES_CSS_SELECTOR"},{"valueType":"TEXT","id":"ba649da3-b94b-4c1e-88d3-362da29eaa97","variableId":"e14c1fcd-e035-4433-9024-29718b3ef8c3","value":"results","operator":"EQUALS"},{"valueType":"BOOLEAN","id":"c365af77-b49b-4f2f-b243-774264e2448a","variableId":"5ae2ce17-ec1b-40cf-b721-e6572f739188","value":"true","operator":"EQUALS"}]}]},"action":{"type":"CLICKOUT"},"fields":[{"id":"c68fad03-1275-4a87-b311-6bb08e1cd53c","variableId":"fa5086fa-000f-4f0c-9fa2-395f222ddc6b","fieldName":"locale"},{"id":"567e30c5-329c-4d7c-8a56-aff931046835","variableId":"cde6ed7d-25e3-4abc-9c7e-51767ce7c319","fieldName":"targetUrl"},{"id":"fc3511e7-c0da-4e00-ac03-1f320e6b7678","variableId":"a4786aee-901d-497c-b4e5-b18536985331","fieldName":"namedReferral"},{"id":"ac034382-94a2-40de-a9be-567673c98a43","variableId":"f51a1d5b-6bc1-4814-9140-af9516bcbd0d","fieldName":"eventLabel"}]},{"id":"f909b9e6-edfe-4043-b704-8dd8cd64a93f","rowType":"EVENT","trigger":{"id":"743c86cc-61de-468f-a989-6e2f4e51024b","name":"Moen_SearchResults_Category_Click","type":"CLICK","delayMs":0,"groups":[{"id":"526bec8c-0093-487a-b9b1-ce4e4207837d","rows":[{"valueType":"EVENT","id":"7431fbc4-a7d1-4143-83b6-1e45488934ef","variableId":"ad6080ea-719b-4732-952f-ef4f1ce45e02","value":"main a[href*=\"/collections/\"], main a[href*=\"/collections/\"] *","operator":"MATCHES_CSS_SELECTOR"},{"valueType":"BOOLEAN","id":"230fd0f0-de5a-41f7-a86c-7b55a1089120","variableId":"f3fe367e-749e-4e7b-95da-89b5a597523c","value":"true","operator":"EQUALS"}]}]},"action":{"type":"CLICKOUT"},"fields":[{"id":"96bdf664-107a-4cea-8827-02cb1f03359b","variableId":"fa5086fa-000f-4f0c-9fa2-395f222ddc6b","fieldName":"locale"},{"id":"bcfeb5b5-a069-4b75-8774-12d1682f5d67","variableId":"cde6ed7d-25e3-4abc-9c7e-51767ce7c319","fieldName":"targetUrl"},{"id":"2d5b8a26-82ea-4db6-8638-182642484303","variableId":"a4786aee-901d-497c-b4e5-b18536985331","fieldName":"namedReferral"},{"id":"38d2e9f8-b0ce-4b33-baee-97024ee4982e","variableId":"3b881070-bf08-4869-be93-2b5d94a99c5b","fieldName":"eventLabel"}]},{"id":"9e683f4c-adfd-45a2-a5d3-b9c41b73b1d7","rowType":"EVENT","trigger":{"id":"ec3f82b7-f508-4801-a4b8-dc29822a90a6","name":"Moen_SearchPopup_ViewAll_Click","type":"CLICK","delayMs":0,"groups":[{"id":"09dd7b2a-d1ba-43d7-8285-686a3e490e75","rows":[{"valueType":"EVENT","id":"8d37e904-41c6-4e03-8d52-1e339654a49d","variableId":"ad6080ea-719b-4732-952f-ef4f1ce45e02","value":"a[data-suggest-view-all], a[data-suggest-view-all] *","operator":"MATCHES_CSS_SELECTOR"}]}]},"action":{"type":"CLICKOUT"},"fields":[{"id":"25cdf262-08d7-486d-8029-0ec5650e88b2","variableId":"fa5086fa-000f-4f0c-9fa2-395f222ddc6b","fieldName":"locale"},{"id":"fc240fd3-f02c-4fb9-80f6-8b6a67c8268e","variableId":"cde6ed7d-25e3-4abc-9c7e-51767ce7c319","fieldName":"targetUrl"},{"id":"b4178c58-03e6-463b-bf3b-1a410bd33c07","variableId":"a4786aee-901d-497c-b4e5-b18536985331","fieldName":"namedReferral"},{"id":"f9eef8bb-b678-4a32-aeaf-cbe04c9fdb80","variableId":"d2d8bad9-06f7-4f78-9184-8c38a9a469a1","fieldName":"eventLabel"}]},{"id":"0fbbeb70-4124-44c9-98df-ffda22d0ee5b","rowType":"EVENT","trigger":{"id":"10c60b35-bc21-4b68-a44b-6c45ac0c07a0","name":"Moen_CartDrawer_Remove_Click","type":"CLICK","delayMs":0,"groups":[{"id":"e2d5d4c1-f911-4d2a-a449-e265feb51cdb","rows":[{"valueType":"EVENT","id":"56cf8b2b-4596-4876-834e-f77c72bea77a","variableId":"ad6080ea-719b-4732-952f-ef4f1ce45e02","value":"cart-remove-toggle button, cart-remove-toggle button *","operator":"MATCHES_CSS_SELECTOR"}]}]},"action":{"type":"REMOVE_FROM_CART"},"fields":[{"id":"419c0f9a-4781-4fb0-941e-4f3b18729dc6","variableId":"fa5086fa-000f-4f0c-9fa2-395f222ddc6b","fieldName":"locale"},{"id":"314be7ef-f697-4d7f-8017-b1ce5c561d23","variableId":"2b2b8a95-38bf-4d84-b02c-c2d0f4cda128","fieldName":"sku"},{"id":"1a81fce2-66e9-4bd1-a620-75b601648930","variableId":"15393d83-66dd-4b82-a4ba-73522aac17f9","fieldName":"price"},{"id":"2030257b-4019-45bf-aedf-1a0b313e10e0","variableId":"491f0feb-e334-45f0-8552-9a4c2a603f09","fieldName":"currencyCode"},{"id":"1ce8e381-3af6-4631-82b5-e0ed597e0e9c","variableId":"e65a7537-b1c9-40cc-9d3a-1e210c273c82","fieldName":"quantity"}]},{"id":"748cf365-ffbe-407e-848c-cdb0d40c32f8","rowType":"EVENT","trigger":{"id":"2cf298c3-374d-41ea-a2ac-22e189cecfa8","name":"Moen_CartDrawer_Quantity_Click","type":"CLICK","delayMs":0,"groups":[{"id":"fbac7586-7f28-4581-9195-2a757e8b88b3","rows":[{"valueType":"EVENT","id":"6ffa0d36-929e-4b8b-9cca-4c35dee80a45","variableId":"ad6080ea-719b-4732-952f-ef4f1ce45e02","value":"#cart-drawer-dialog quantity-input button, #cart-drawer-dialog quantity-input button *","operator":"MATCHES_CSS_SELECTOR"},{"valueType":"BOOLEAN","id":"31510892-b8bb-47df-bd59-fdfcbd53f6ca","variableId":"5ae2ce17-ec1b-40cf-b721-e6572f739188","value":"true","operator":"EQUALS"}]}]},"action":{"type":"UPDATE_CART"},"fields":[{"id":"e4979892-17fe-4075-8f9b-85295ea4b453","variableId":"fa5086fa-000f-4f0c-9fa2-395f222ddc6b","fieldName":"locale"},{"id":"9cb171ed-b07c-4e69-b069-1c71645ff31a","variableId":"2b2b8a95-38bf-4d84-b02c-c2d0f4cda128","fieldName":"sku"},{"id":"0c06adb7-9717-4e5e-b347-ad6bc7613fdf","variableId":"15393d83-66dd-4b82-a4ba-73522aac17f9","fieldName":"price"},{"id":"01c38d1f-aae0-4844-a140-01d5e73e3798","variableId":"491f0feb-e334-45f0-8552-9a4c2a603f09","fieldName":"currencyCode"},{"id":"91efed1f-b9ce-48d5-bf45-fca2d5f6dfb1","variableId":"6b5421d1-f27d-4e97-aa61-11b2cf5ea340","fieldName":"quantity"}]}]}; const advancedCode = function(){enableShopifyDataLayerPurchaseTrigger({ timeoutMs: 5000 });}; const url = 'https://queue-propagator.zoovu.com'; const currentEnvironment = 'barracuda'; const currentAccountId = 1836; const currencies = ['FJD', 'MXN', 'STD', 'SCR', 'LVL', 'CDF', 'BBD', 'HNL', 'UGX', 'ZAR', 'MXV', 'STN', 'CUC', 'SDD', 'BSD', 'SDG', 'IQD', 'CUP', 'GMD', 'TWD', 'RSD', 'UYI', 'MYR', 'FKP', 'XOF', 'UYU', 'CVE', 'OMR', 'SEK', 'KES', 'BTN', 'GNF', 'MZN', 'SVC', 'MZM', 'ARS', 'QAR', 'IRR', 'NLG', 'XPD', 'XPF', 'THB', 'UZS', 'BDT', 'LYD', 'KWD', 'XPT', 'RUB', 'ISK', 'BEF', 'MKD', 'RUR', 'DZD', 'PAB', 'SGD', 'KGS', 'XAD', 'XAF', 'XAG', 'CHF', 'HRK', 'ITL', 'ATS', 'DJF', 'CHE', 'TZS', 'ADP', 'VND', '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', 'XCD', 'GRD', 'HTG', 'XSU', 'AFA', 'XCG', 'SIT', 'BHD', 'PTE', 'SZL', 'KZT', 'YER', 'AFN', 'BYB', 'NPR', 'AWG', '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', 'AZM', 'TOP', 'AZN', 'PGK', 'UAH', 'ERN', 'TPE', 'MRO', 'CNY', 'MRU', 'BMD', 'XXX', 'PHP', 'PYG', 'JMD', 'GWP', 'ESP', 'COP', 'USD', 'COU', 'USN', 'ETB', 'VEB', 'VED', 'USS', 'SOS', 'VEF', 'VUV', 'LAK', 'BND', 'ZMK', 'LRD', 'ALL', 'GHC', 'MTL', 'VES', 'ZMW', 'TRL', 'ILS', 'KPW', 'GYD', 'GHS', 'BOB', 'MDL', 'AMD', 'TRY', 'LBP', 'JOD', 'HKD', 'EUR', 'LSL', 'CAD', 'BOV', 'EEK', 'MUR', 'ROL', 'GIP', 'RON', 'NGN', 'CRC', 'PKR', 'ANG', 'SRD', 'SAR', 'TTD', 'LTL', 'MVR', 'SRG', 'INR', 'KRW', 'JPY', 'AOA', 'PLN', 'SBD', 'CSD', 'LUF', 'MWK', 'MGA', 'FIM', '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 = []; function logDebug(...args) { if (typeof console !== 'undefined' && typeof console.debug === 'function') { console.debug(...args); } } /** * @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) { logDebug(`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. for (let i = 2; i <= parts.length; i++) { const candidate = parts.slice(-i).join('.'); const probe = `__zoovu_probe_${Math.random().toString(36).slice(2, 10)}`; document.cookie = `${probe}=1; domain=.${candidate}; path=/; max-age=10`; 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) { logDebug('[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) { // eventFields is only populated on the rule-driven path; direct public-API calls // (e.g. ZoovuTrackingManager.sendPurchaseEvent) reach this with eventFields empty, // where the unguarded lookup used to throw a raw TypeError instead of the intended // TrackingExecutionError. const matchedField = eventFields.find(field => field.fieldName === key); const variableId = matchedField ? matchedField.variableId : null; // 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) { logDebug('Failed to record tracking error', recordingError); } logDebug(error); } // ------------------------- eager purchase trigger (opt-in, see enableShopifyDataLayerPurchaseTrigger) ------------------------- let eagerPurchaseTriggerOptions = null; const eagerPendingRules = new Map(); // once fired in this document, never again - SPA URL changes re-arm rules in tight loops const eagerFiredRules = new Set(); function isPixelSandboxContext() { return typeof window !== 'undefined' && window.location.pathname.startsWith('/web-pixels@'); } function isEagerEligibleRule(rule) { return eagerPurchaseTriggerOptions !== null && rule.trigger.type === triggerTypes.PAGE_LOAD && rule.action && rule.action.type === actionTypes.PURCHASED; } function isPurchasePayloadReady(payload) { // [] would pass ensureRequiredFieldsPresent but is dropped silently by the backend return Array.isArray(payload.products) && payload.products.length > 0 && !!payload.currencyCode; } function attemptEagerRule(entry) { // .resolving blocks re-entrancy: payload resolution can re-trigger the dataLayer hooks if (entry.done || entry.resolving) { return; } entry.resolving = true; let payload; try { payload = preparePayloadFromFields(entry.rule.fields, entry.event); } catch (e) { return; // variables not resolvable yet - keep waiting } finally { entry.resolving = false; } // !payload is unreachable for today's preparePayloadFromFields, but this function runs // inside merchant dataLayer.push calls and must never throw there (test-pinned contract) if (!payload || !isPurchasePayloadReady(payload)) { return; } if (entry.done) { return; // a nested pass may have fired this entry during resolution } entry.done = true; clearTimeout(entry.timeoutId); eagerPendingRules.delete(entry.rule._uniqueId); eagerFiredRules.add(entry.rule._uniqueId); try { runAction(entry.rule.action, entry.rule.fields, entry.event, entry.rule.trigger); } catch (error) { recordTrackingError(error); } } let attemptingEagerRules = false; function attemptAllEagerRules() { if (attemptingEagerRules) { return; } attemptingEagerRules = true; try { eagerPendingRules.forEach(entry => attemptEagerRule(entry)); } finally { attemptingEagerRules = false; } } function scheduleEagerRule(rule, event) { if (eagerFiredRules.has(rule._uniqueId) || eagerPendingRules.has(rule._uniqueId)) { return; } const timeoutMs = eagerPurchaseTriggerOptions.timeoutMs || rule.trigger.delayMs; const entry = {rule, event, done: false, timeoutId: null}; // setTimeout is safe here: only pixel-sandbox documents reach this path, no merchant code runs there entry.timeoutId = setTimeout(() => { if (entry.done) { return; } entry.done = true; eagerPendingRules.delete(rule._uniqueId); let payload = null; try { payload = preparePayloadFromFields(rule.fields, event); } catch (e) { // recorded below } if (payload && isPurchasePayloadReady(payload)) { // covers pushes the hook could not observe eagerFiredRules.add(rule._uniqueId); try { runAction(rule.action, rule.fields, event, rule.trigger); } catch (error) { recordTrackingError(error); } } else { // never send an empty purchase - skip and make the failure visible recordTrackingError(new TrackingExecutionError( `Eager purchase trigger timed out after ${timeoutMs}ms without usable purchase data - skipping send.`, trackingEntityTypes.TRIGGER, null, rule.trigger.id, script.id, trackingErrorTypes.MISSING_VALUE )); } }, timeoutMs); eagerPendingRules.set(rule._uniqueId, entry); // The purchase data may already be present when the rule arms. attemptEagerRule(entry); } function hookDataLayerForEagerRules() { const wrapDataLayer = (arr) => { if (!arr || arr.__zoovuEagerHooked) { return arr; } const originalPush = arr.push.bind(arr); arr.push = function () { const result = originalPush.apply(undefined, arguments); attemptAllEagerRules(); return result; }; arr.__zoovuEagerHooked = true; return arr; }; let currentDataLayer = wrapDataLayer(window.dataLayer); try { // catch late (re)assignment of window.dataLayer Object.defineProperty(window, 'dataLayer', { configurable: true, get() { return currentDataLayer; }, set(value) { // `dataLayer = dataLayer || []` style self-assignment must be a no-op if (value === currentDataLayer) { return; } currentDataLayer = wrapDataLayer(value); attemptAllEagerRules(); }, }); } catch (e) { logDebug('Eager purchase trigger: could not intercept dataLayer reassignment', e); } } /** * @public PUBLIC API — externally consumed (enabled per script via advanced mode). * Renaming, changing the signature, or changing the return shape * will break callers. Do not modify without a deprecation plan. * * Opt-in for Shopify web-pixel sandboxes, where the rAF-based delayMs wait never * completes in Blink (hidden 0x0 iframe -> no frames). Fires delayed PAGE_LOAD * PURCHASED rules as soon as the payload is resolvable (checked on every * dataLayer.push), with delayMs/options.timeoutMs as the upper bound, and never * sends an empty-products purchase. No-op outside the sandbox context. */ function enableShopifyDataLayerPurchaseTrigger(options) { if (!isPixelSandboxContext()) { logDebug('Eager purchase trigger: not a pixel-sandbox context, keeping default behavior'); return false; } if (eagerPurchaseTriggerOptions !== null) { return true; } eagerPurchaseTriggerOptions = {timeoutMs: (options && options.timeoutMs) || null}; hookDataLayerForEagerRules(); return true; } 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) { if (isEagerEligibleRule(rule)) { scheduleEagerRule(rule, event); return; } // 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', keepalive: true, body: JSON.stringify(body), }); } else { logDebug("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; logDebug('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 { logDebug("Tracking disabled - no permission to track"); } } async function sendSSTRPurchaseEvent(body) { if (trackingEnabled) { const purchaseUrl = 'https://ev-co.zoovu.com/v1/CollectSalesEvent'; const response = await fetch(purchaseUrl, { method: 'POST', mode: 'cors', cache: 'no-cache', headers: { 'Content-Type': 'application/json', }, redirect: 'follow', referrerPolicy: 'no-referrer', keepalive: true, body: JSON.stringify(body), }); if (!response.ok) { // fetch resolves on HTTP errors - reject so the caller can forget the dedup key throw new Error(`CollectSalesEvent failed: ${response.status}`); } } else { logDebug("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', keepalive: true, body: JSON.stringify(body), }); } else { logDebug("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); } }; // ------------------------- purchase dedup guard (tab-scoped) ------------------------- // Suppresses same-tab re-sends of an identical purchase; fails open on storage errors. const PURCHASE_DEDUP_WINDOW_MS = 60 * 60 * 1000; // short window for id-less purchases: their identity is only inferred const PURCHASE_DEDUP_NOID_WINDOW_MS = 10 * 60 * 1000; const PURCHASE_DEDUP_MAX_KEYS = 20; const purchaseDedupStorageKey = `${domainId}_zvSentPurchases`; let purchaseDedupGuardEnabled = true; /** * @public PUBLIC API — externally consumed. * Escape hatch for integrations that intentionally re-send purchase events. */ function disablePurchaseDedupGuard() { purchaseDedupGuardEnabled = false; } function purchaseDedupHash(str) { let h = 5381; for (let i = 0; i < str.length; i++) { h = ((h << 5) + h + str.charCodeAt(i)) >>> 0; } return h.toString(36); } // returns null when the purchase carries no provable identity - the guard is skipped function buildPurchaseDedupKey(transactionId, products, currencyCode) { const items = (products || []) .filter(Boolean) .map(p => { const raw = getPurchaseProductPrice(p); const num = Number(raw); // unparseable prices stay verbatim - collapsing them all to one value would merge different carts const price = isFinite(num) ? num.toFixed(2) : String(raw); return `${p.sku}:${Number(p.quantity) || 0}:${price}`; }) .sort() // cart order must not change the key .join(','); if (transactionId) { return `tid|${transactionId}|${purchaseDedupHash(`${currencyCode || ''}|${items}`)}`; } if (isPixelSandboxContext()) { // the sandbox pathname is per-shop, not per-order - identity is not provable there return null; } // no transaction id: the confirmation-page pathname is the order-identity proxy const pathname = (typeof window !== 'undefined' && window.location) ? window.location.pathname : ''; return `noid|${purchaseDedupHash(`${pathname}|${currencyCode || ''}|${items}`)}`; } function readSentPurchaseMap() { let map = null; try { map = JSON.parse(sessionStorage.getItem(purchaseDedupStorageKey)); } catch (e) { // corrupted value - replaced on next write } return (map && typeof map === 'object') ? map : {}; } function isDuplicatePurchase(dedupKey) { if (!purchaseDedupGuardEnabled) { return false; } try { const map = readSentPurchaseMap(); const sentAt = map[dedupKey]; const windowMs = dedupKey.startsWith('noid|') ? PURCHASE_DEDUP_NOID_WINDOW_MS : PURCHASE_DEDUP_WINDOW_MS; return typeof sentAt === 'number' && (Date.now() - sentAt) < windowMs; } catch (e) { return false; // fail open } } function rememberSentPurchase(dedupKey) { try { const map = readSentPurchaseMap(); map[dedupKey] = Date.now(); const keys = Object.keys(map); if (keys.length > PURCHASE_DEDUP_MAX_KEYS) { keys.sort((a, b) => map[a] - map[b]); for (let i = 0; i < keys.length - PURCHASE_DEDUP_MAX_KEYS; i++) { delete map[keys[i]]; } } sessionStorage.setItem(purchaseDedupStorageKey, JSON.stringify(map)); } catch (e) { // fail open } } function forgetSentPurchase(dedupKey) { try { const map = readSentPurchaseMap(); delete map[dedupKey]; sessionStorage.setItem(purchaseDedupStorageKey, JSON.stringify(map)); } catch (e) { // fails closed: the key stays remembered and suppression persists for the window } } // ------------------------- purchase payload validation ------------------------- // Caught here or nowhere: SSTR 200s any product shape. An empty list blocks the send (worth // nothing); a missing price only forfeits that line (SSTR reads it as 0) so it reports and sends. function getPurchaseProductPrice(product) { if (!product) { return undefined; } return product.price != null ? product.price : product.pricePerUnit; } function purchaseProductsError(message) { const productsField = eventFields.find(field => field.fieldName === 'products'); return new TrackingExecutionError( message, trackingEntityTypes.VARIABLE, productsField ? productsField.variableId : null, null, script.id, trackingErrorTypes.MISSING_VALUE ); } function ensurePurchaseProductsValid(products) { if (!Array.isArray(products) || products.length === 0) { const error = purchaseProductsError(`Invalid products for ${eventTypes.PURCHASED}: products must be a non-empty array.`); eventFields = []; // as ensureRequiredFieldsPresent throw error; } const unpriced = []; for (let i = 0; i < products.length; i++) { const price = getPurchaseProductPrice(products[i]); if (price === null || price === undefined || price === '') { unpriced.push(`products[${i}] (sku: ${(products[i] && products[i].sku) || 'unknown'})`); } } if (unpriced.length > 0) { recordTrackingError(purchaseProductsError( `Missing price for ${eventTypes.PURCHASED}, counted as 0: ${unpriced.join(', ')} - set price or pricePerUnit.` )); } return true; } const KEPT_SEGMENTS = ['checkouts', 'checkout', 'orders', 'order-received', 'cart', 'thank_you', 'thank-you', 'information', 'payment', 'shipping', 'success', 'products', 'collections', 'account', 'search', 'sandbox', 'modern', 'custom']; function redactPathSegment(segment) { if (segment.indexOf('web-pixels') === 0) { return 'web-pixels'; } if (segment === '' || KEPT_SEGMENTS.indexOf(segment) !== -1 || /^[a-z]{2}-[a-z]{2}$/.test(segment)) { return segment; } return '_'; } function redactPathname(pathname) { const segments = pathname.split('/'); for (let i = 0; i < segments.length; i++) { segments[i] = redactPathSegment(segments[i]); } return segments.join('/'); } function getRedactedDocumentLocation() { if (typeof window === 'undefined' || !window.location) { return null; } return window.location.origin + redactPathname(window.location.pathname); } /** * @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(), documentLocation: getRedactedDocumentLocation(), }; if (ensureRequiredFieldsPresent({products, currencyCode,}, eventTypes.PURCHASED) && ensurePurchaseProductsValid(products)) { const dedupKey = buildPurchaseDedupKey(transactionId, products, currencyCode); if (dedupKey && isDuplicatePurchase(dedupKey)) { logDebug(`Duplicate purchase suppressed (same tab): ${dedupKey}`); return; } if (dedupKey && trackingEnabled) { // remembered before the fetch (the duplicate can fire before the response), and // only when a send can actually happen - a purchase swallowed by disabled // tracking must stay re-sendable after consent rememberSentPurchase(dedupKey); } // a failed send must not block the refresh-retry sendSSTRPurchaseEvent(sstrBody).catch(() => { if (dedupKey) { forgetSentPurchase(dedupKey); } }); } }; /** * @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, enableShopifyDataLayerPurchaseTrigger, disablePurchaseDedupGuard, 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) { logDebug("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(); } })();