async function boot() { let init = async () => { let [navigation, session] = await initializeService() getAsyncInfos(navigation, session) await initProcessSession(navigation, session) initProcessProduct(navigation, session) if (_cvConfig.cart_all_pages === true) initProcessAddCart(navigation) initProcessUser(navigation, session) initProcessRemoveCart(navigation, session) initProcessOrder(navigation, session) initCustomClicks(navigation, session, _cvConfig.custom_clicks) initPopPushs(navigation, session, _cvConfig.pushs) initProcessCheckout(navigation) initProcessCheckProducts(navigation) if (window['_cAddProductToWishlist']) _cAddProductToWishlist() if (window['_cvGetFormDependent']) _cvGetFormDependent(session) if (window['_cInfoInputs']) _cInfoInputs() if (window['_cInfoInputsSecond']) _cInfoInputsSecond() if (window['_cInfoInputsThird']) _cInfoInputsThird() if (window['_cGetNewsletter']) _cGetNewsletter(session) if (window['_cvResize']) _cvResize() } try { if (_cvConfig.windowLoad == true) { window.addEventListener("load", async () => { init() }) } else if (_cvConfig.singlePage == true) { if (_cvFlagSinglePage === false) init() // init first time if (_cvConfig.customerKey === '91658BAB23') { // beta _cvLoadSingleConfig(init) } else { let currentUrl = window.location.href setInterval(() => { let urlNow = window.location.href if (currentUrl != urlNow) { currentUrl = urlNow init() } }, 500) } } else { init() } } catch (err) { console.log('[ERRO] FlagSinglePage -->', err) } } let _cvFlagSinglePage = false // evitam que os listeners de opt-in sejam duplicados a cada init em SPA let _cvOptinIntervals = {} let _cvOptinKeydown = {} function _cvLoadSingleConfig(initCallback) { try { if (_cvFlagSinglePage === false) { _cvFlagSinglePage = true // console.log('[DEBUG] chamou _cvLoadSingleConfig...') const generateId = () => Math.round(Math.random() * 8999 + 1000); const requestUrl = new Map(); const blacklist = ["sumo", "shopback", "/_next/data/"]; const isOnBlackList = (url) => blacklist.some((key) => new RegExp(key, "i").test(url)); // alteração do metodo fetch const { fetch: originalFetch } = window; window.fetch = (...args) => { const [url] = args; const id = generateId(); if (!isOnBlackList(url)) { requestUrl.set(id, url); // console.log("Começou uma requisição fetch", id, url); setTimeout(() => { requestUrl.delete(id); // console.log("Não concluiu a requisição", id, Array.from(requestUrl)); }, 5000); } else { // console.log("Está na blacklist", url); } const response = originalFetch(...args); response.finally(() => { requestUrl.delete(id); // console.log("Terminou uma requisição fetch", id, url); }); return response; }; // alteração da classe XMLHttpRequest const { send: originalSend, open: originalOpen } = XMLHttpRequest.prototype; XMLHttpRequest.prototype.open = function (...args) { const [, url] = args; new RegExp(`//`); if (!isOnBlackList(url)) { const id = generateId(); this.id = id; requestUrl.set(id, url); // console.log("Começou uma requisição xHttp", id, url); } else { // console.log("Está na blacklist", url); } originalOpen.call(this, ...args); }; XMLHttpRequest.prototype.send = function (...args) { // console.log("Começou uma requisição xHttp", this); const originalReadyStateChange = this.onreadystatechange?.bind(this); this.onreadystatechange = function (event) { if (this.readyState === 4) { requestUrl.delete(this.id); // console.log("Terminou uma requisição xHttp", this.id, this.responseURL); // console.log("Requisições restantes", Array.from(requestUrl)); } originalReadyStateChange?.(event); }; setTimeout(() => { if (this.readyState < 4) { requestUrl.delete(this.id); // console.log("Não concluiu a requisição", this.id, Array.from(requestUrl)); } }, 5000); originalSend.call(this, ...args); }; // alteração do método pushState const originalPushState = history.pushState; history.pushState = function (...args) { originalPushState.call(this, ...args); window.dispatchEvent(new Event("cv_changepage")); }; // alteração do método replaceState const originalReplaceState = history.pushState; history.replaceState = function (...args) { originalReplaceState.call(this, ...args); window.dispatchEvent(new Event("cv_changepage")); }; window.addEventListener("cv_changepage", () => { console.log("Mudou de URL"); const interval = setInterval(() => { if (requestUrl.size === 0) { console.log("[DEBUG] terminou as requisições..."); clearInterval(interval); initCallback() } else console.log('[DEBUG] requestUrl.size -->', requestUrl.size); }, 500); }); window.addEventListener("popstate", () => { const interval = setInterval(() => { if (requestUrl.size === 0) { console.log("[DEBUG] terminou as requisições..."); clearInterval(interval); initCallback() } else console.log('[DEBUG] requestUrl.size -->', requestUrl.size); }, 500); }); } } catch (err) { console.log('[ERRO] _cvLoadSingleConfig -->', err) } } async function initializeService() { return new Promise(async resolve => { let navigation = new _Navigation() let session = new _Session() let cart = new _Cart() let newSession let initNewSession = async () => { newSession = true let hashCookie = await getCookieCV('_cvhash') if (hashCookie != 0) { hashCookie = JSON.parse(hashCookie) session.hash = hashCookie.hash session.identified = hashCookie.identified } else { session.hash = await generateCVID() setCookieCV('_cvhash', JSON.stringify({ hash: session.hash, identified: session.identified }), 365) } session.session = await generateCVID() session.started_at = new Date() session.processTimeOut(_cvConfig.session_timeout) processTimeSpend() session.save() } if (localStorage._cvsession) { session.init() if (await session.processSessionTime() == true) { session = new _Session() await initNewSession() } else { newSession = false session.processTimeOut(_cvConfig.session_timeout) processTimeSpend() session.page_views++ } resolve([navigation, session]) } else { await initNewSession() resolve([navigation, session]) } if (localStorage._cvcart) { cart.init() if (newSession == true) { await cart.clearRemoved() if (cart.hasActiveProducts == true) { cart.cart_id = await generateCVID() cart.save() } else cart.clear() } if (cart.hasActiveProducts == true) { if (cart.record_duration != _cvConfig.cart_duration) cart.record_duration = _cvConfig.cart_duration await cart.processCartDuration() } } }) } async function initProcessSession(navigation, session) { await processForSession(navigation, _cvConfig.functions.forNavCategories, session, 'categories_view', [navigation, 'getPathCategories', _cvConfig.notValidCategories]) await processForSession(navigation, _cvConfig.functions.forNavProduct, session, 'products_view') await processForSession(navigation, _cvConfig.functions.forNavSearches, session, 'searches') let utms = await getUrlParameters(navigation.params, 'utm') if (Object.entries(utms).length > 0) { if (session.utms.length > 0) { await processForUtms(utms, session) } else session.utms.push(utms) } session.has_order = await initProcessOrder(navigation, session) new _PayloadNavigation(navigation, session).send() if (session.bis == false) session.bis = true if (navigation.geolocation != undefined || session.gls == false) session.gls = true await updateInterationLists(session) session.save() } async function updateInterationLists(session) { if (session.pushs) { // for pushs for (let i = 0; i < session.pushs.length; i++) { session.pushs[i].intervalAttempts++ } } } async function processForSession(navigation, functionName, objTo, paramTo, callbackElse) { if (functionName != 'default') { if (window[functionName]) { let values = await window[functionName](navigation).catch(err => { new _CVPROCESSBEAT('ERROR', functionName, `Erro na função de captação para sessão --> ${err}`) }) if (values) { let valuesList = await processListObjs(values, objTo[paramTo]) objTo[paramTo] = valuesList[0] navigation[paramTo] = valuesList[1] } } else new _CVPROCESSBEAT('ERROR', functionName, 'Função de captação para sessão não existe na tag') } else { objTo[paramTo] = await processListObjs(await callbackElse[0][callbackElse[1]](callbackElse[2]), objTo[paramTo]) } } async function processForUtms(utms, session) { let exist = false for (let i = 0; i < session.utms.length; i++) { if (session.utms[i].utm_campaign && utms.utm_campaign) if (await session.utms[i].utm_campaign == utms.utm_campaign) exist = true } if (!exist) session.utms.push(utms) } async function initProcessProduct(navigation) { if (window[_cvConfig.functions.isProductPage] && window[_cvConfig.functions.forProductInfos]) { if (await window[_cvConfig.functions.isProductPage](navigation) != undefined) { if (_cvConfig.cart_all_pages !== true) initProcessAddCart(navigation) let product = await window[_cvConfig.functions.forProductInfos](navigation).catch(err => { new _CVPROCESSBEAT('ERROR', _cvConfig.functions.isProductPage, `Erro na função de captação para produto --> ${err}`) }) if (product) { let productClass = new _Product(product) productClass.save() productClass.send() productClass.validate() } else new _CVPROCESSBEAT('ERROR', _cvConfig.functions.forProductInfos, 'Função de captação para produto não retornou o objeto esperado', product) } } else new _CVPROCESSBEAT('ERROR', _cvConfig.functions.isProductPage, 'Função de captação para produto não existe na tag') } async function initProcessAddCart(navigation) { if (window[_cvConfig.functions.addProductToCart]) { window[_cvConfig.functions.addProductToCart](navigation) } else new _CVPROCESSBEAT('ERROR', _cvConfig.functions.addProductToCart, 'Função de adicionar produto no carrinho não existe na tag') } async function initProcessRemoveCart(navigation) { if (window[_cvConfig.functions.removeProductToCart]) { window[_cvConfig.functions.removeProductToCart](navigation) } else new _CVPROCESSBEAT('ERROR', _cvConfig.functions.removeProductToCart, 'Função de remover produto do carrinho não existe na tag') } async function initProcessCheckProducts(navigation) { if (window[_cvConfig.functions.checkCartProducts]) { let objProducts = await window[_cvConfig.functions.checkCartProducts](navigation) if (objProducts != undefined) { let cart = new _Cart() if (localStorage._cvcart) cart.init() cart.checkProducts(objProducts[0], objProducts[1], objProducts[2]) } } else new _CVPROCESSBEAT('ERROR', 'initProcessCheckProducts', 'Função de validação de produtos no carrinho não existe na tag') } async function initProcessUser(navigation, session) { if (_cvConfig.optin_forms.length > 0) { let mapped = form => { if (form.default_visible == true) { let trigger = document.querySelector(form.trigger) if (trigger) addEvent(trigger, form) else new _CVPROCESSBEAT('ERROR', 'initProcessUser', `Trigger ${form.trigger} cadastrada para form ${form.name} não existe`) } else { // let interval1 = form => { // let idI1 = setInterval(() => { // let trigger = document.querySelector(form.trigger) // if (trigger) { // clearInterval(idI1) // addEvent(trigger, form) // interval2(form) // } // }, 600) // } // let interval2 = form => { // let idI2 = setInterval(() => { // let trigger = document.querySelector(form.trigger) // if (trigger == undefined) { // clearInterval(idI2) // interval1(form) // } // }, 800) // } // interval1(form) // mantem o polling para remapear o trigger quando a SPA recria o elemento if (_cvOptinIntervals[form.name] === undefined) { _cvOptinIntervals[form.name] = setInterval(() => { let trigger = document.querySelector(form.trigger) if (trigger) addEvent(trigger, form) }, 400) } } } let addEvent = (element, form) => { if (element.__cvOptinMapped !== true) { element.__cvOptinMapped = true element.addEventListener('click', () => { catchInfo(form) }) element.addEventListener('touchstart', () => { catchInfo(form) }) } if (_cvOptinKeydown[form.name] !== true) { _cvOptinKeydown[form.name] = true document.addEventListener('keydown', event => { if (event.key == 'Enter' || event.keyCode == 13) catchInfo(form) }) } } let catchInfo = form => { let acceptTerm = async form => { let getVariants = async form => { let variantsToReturn = {} if (form.variants.length > 0) { for (let i = 0; i < form.variants.length; i++) { const variant = form.variants[i] let field = document.querySelector(variant.field) if (field) { let value = field.value if (await value.length > 0) { variantsToReturn[variant.name] = value } else { value = field.innerText if (await value.length > 0) { variantsToReturn[variant.name] = value } } } else if (variant.required == true) { new _CVPROCESSBEAT('ERROR', 'initProcessUser', `Variante ${variant.field} para form ${form.name} não existe`) return undefined } } } return variantsToReturn } let email = document.querySelector(form.email) if (email) { email = email.innerText ? email.innerText : email.value if (await validateEmail(email) == true) { let variants = await getVariants(form) if (variants) { let user = new _User() user.email_address = email user.variants = variants user.origin = form.name if (form.optin_field == true) user.optin_field = true else { let optin_field = document.querySelector(form.optin_field) if (await optin_field) user.optin_field = optin_field.checked else { user.optin_field = false new _CVPROCESSBEAT('ERROR', 'initProcessUser', `Campo de opt-in ${form.optin_field} para form ${form.name} não existe`) } } registerCvUser(user, session) } } } else new _CVPROCESSBEAT('ERROR', 'initProcessUser', `Campo de email ${form.email} para form ${form.name} não existe`) } if (form.term_field == true) { acceptTerm(form) } else { let termField = document.querySelector(form.term_field) if (termField) { if (termField.checked == true) acceptTerm(form) else if (termField.value == 'true') acceptTerm(form) } else new _CVPROCESSBEAT('ERROR', 'initProcessUser', `Termo de consentimento ${form.term_field} para form ${form.name} não existe`) } } for (let i = 0; i < _cvConfig.optin_forms.length; i++) { const form = _cvConfig.optin_forms[i] if (form.url == 'all') mapped(form) else if (form.url == navigation.cleanUrl) mapped(form) } } else new _CVPROCESSBEAT('ERROR', 'initProcessUser', 'Não existem formulários de opt-in para captação de usuários') } async function initProcessOrder(navigation, session) { if (window[_cvConfig.functions.isOrderPage]) { if (session.has_order == false) { if (await window[_cvConfig.functions.isOrderPage](navigation) == true) { if (localStorage._cvcart) { let cart = new _Cart() cart.init() cart.type = 2 new _PayloadCart(cart).send() cart.clear() } return true } else return false } else return true } else { new _CVPROCESSBEAT('ERROR', _cvConfig.functions.isOrderPage, 'Função de identificação de página de compra não existe na tag') return false } } async function initProcessCheckout(navigation) { if (window[_cvConfig.functions.forOriginCart]) { if (await window[_cvConfig.functions.forOriginCart](navigation) == 2) { if (localStorage._cvcart) { let cart = new _Cart() cart.init() cart.id_origin = 2 cart.save() cart.type = 1 new _PayloadCart(cart).send() } } } else new _CVPROCESSBEAT('ERROR', _cvConfig.functions.forOriginCart, 'Função de origem de carrinho não existe na tag') } async function registerCvUser(user, session) { let hash = await triggerRequest('post', user, _cvConfig.endpoints.user, true) if (hash) { let saveSession = async (session, hashCookie) => { session.identified = true session.hash = hashCookie setCookieCV('_cvhash', JSON.stringify({ hash: hashCookie, identified: true }), 365) await session.save() session = new _Session().init() let navigation = new _Navigation() new _PayloadNavigation(navigation, session, true).send() } if (session.identified == false) { saveSession(session, hash) } else if (session.hash != hash) { saveSession(session, hash) } } else new _CVPROCESSBEAT('ERROR', 'registerCvUser', 'Requisição para registro de usário não retornou a hash esperada') } async function initPopPushs(navigation, session, pushs) { let pushToProcess = [] for (let i = 0; i < pushs.length; i++) { let rules = pushs[i].rules let passHomolog = true if (rules.homologHashs != false) { passHomolog = false if (session.identified == true) { let index = await rules.homologHashs.findIndex(e => e == session.hash) if (index >= 0) passHomolog = true else passHomolog = false } } else { if (rules.identified == 'all') passHomolog = true else if (await rules.identified == true && session.identified == false) passHomolog = false else if (await rules.identified == false && session.identified == true) passHomolog = false } if (passHomolog == true) { let passPage = true for (let j = 0; j < rules.pagesToIgnore.length; j++) { if (await navigation.cleanUrl == rules.pagesToIgnore[j]) passPage = false } if (passPage == true) { if (rules.pagesToShow == 'all') { pushToProcess.push(pushs[i]) } else { let valid = false for (let j = 0; j < rules.pagesToShow.length; j++) { if (rules.pagesToShow[j] == '$product') { valid = await window[_cvConfig.functions.isProductPage](navigation) != undefined ? true : false if (valid == true) break } else if (rules.pagesToShow[j] == '$categorie') { valid = await window[_cvConfig.functions.isCategoriePage](navigation) if (valid == true) break } else if (navigation.cleanUrl == rules.pagesToShow[j]) { valid = true; break } } if (valid == true) { pushToProcess.push(pushs[i]) } } } } } if (pushToProcess.length > 0) processPopPushs(navigation, pushToProcess, 0, session) } async function processPopPushs(navigation, pushs, index, session) { let initPush = push => { let show = true if (push.rules.hasCart === true) { if (localStorage._cvcart) { let cart = new _Cart().init() if (cart.hasActiveProducts === true) show = true else show = false } else show = false } if (show == true) { if (push.rules.show.mouseLeave == true) { let lock = false document.addEventListener('mouseleave', () => { if (!lock) { new _Push(push).showPush(navigation) lock = true } }) } else if (push.rules.show.pageLoad == true) new _Push(push).showPush(navigation) else if (push.rules.show.clickSelectors != false) { // dev selectors } } else { if (index + 1 < pushs.length) { processPopPushs(navigation, pushs, index + 1, session) } } } let currentPush = pushs[index] if (session.pushs) { let sessionIndex = await session.pushs.findIndex(e => e.name == currentPush.name) if (sessionIndex >= 0) { if (currentPush.rules.oneTimeClick === false || (currentPush.rules.oneTimeClick === true && session.pushs[sessionIndex].clicks == 0)) { if (session.pushs[sessionIndex].attempts < currentPush.rules.maxTry) { if (currentPush.rules.intervalPagesToTry < session.pushs[sessionIndex].intervalAttempts) { initPush(currentPush) } else if (index + 1 < pushs.length) processPopPushs(navigation, pushs, index + 1, session) } else if (index + 1 < pushs.length) processPopPushs(navigation, pushs, index + 1, session) } else if (index + 1 < pushs.length) processPopPushs(navigation, pushs, index + 1, session) } else { initPush(currentPush) } } else { initPush(currentPush) } } async function processCartAddProduct(product, paramToCompare, send, isEquals, check) { let cart = new _Cart() if (localStorage._cvcart) { cart.init() await cart.insertProduct(product, paramToCompare, isEquals, check) if (send) { cart.type = 1 new _PayloadCart(cart).send() } } else { cart.cart_id = await generateCVID() cart.created_at = new Date() cart.record_duration = _cvConfig.cart_duration await cart.insertProduct(product, paramToCompare, isEquals, check) if (send) { cart.type = 1 new _PayloadCart(cart).send() } } } async function processCartRemoveProduct(paramToCompare, valueToCompare, send, isEquals) { let cart = new _Cart() if (localStorage._cvcart) { cart.init() await cart.deleteProduct(paramToCompare, valueToCompare, isEquals) if (send) new _PayloadCart(cart).send() } else new _CVPROCESSBEAT('INFO', 'processCartRemoveProduct', 'Objeto de persistência de carrinho não existe') } async function initCustomClicks(navigation, session, custom) { for (let i = 0; i < custom.length; i++) { if (custom[i].page == 'all') { processCustomClicks(custom[i], session) } else if (custom[i].page == navigation.cleanUrl) { processCustomClicks(custom[i], session) } } } async function processCustomClicks(custom, session) { let indexClick = session.custom_clicks.findIndex(e => e.name == custom.name) let mapedElements = async () => { let element if (await custom.unique === true) { element = document.querySelector(custom.selector) } else { element = document.querySelectorAll(custom.selector) if (await custom.index != 'all') element = element[custom.index] } if (element) { if (await custom.unique === true || custom.index != 'all') { element.addEventListener('click', countClick) } else { for (let i = 0; i < element.length; i++) { element[i].addEventListener('click', countClick) } } } else { new _CVPROCESSBEAT('ERROR', 'processCustomClicks', `Não foi possível mapear o elemento configurado no clique personalizado`, custom) } } let countClick = async () => { custom.clicked_at = new Date() if (indexClick >= 0) { session.custom_clicks[indexClick].countClick++ } else { if (await custom.count === true) custom.countClick = 1 session.custom_clicks.push(custom) } session.save() } if (custom.count === false) { if (indexClick >= 0) mapedElements() } else mapedElements() } async function getAsyncInfos(navigation, session) { if (_cvConfig.geolocation === true) { navigation.geolocation = await navigation.getGeolocation().catch(err => { }) } if (window[_cvConfig.functions.getLoged]) window[_cvConfig.functions.getLoged](navigation, session) } function triggerRequest(method, payload, endpoint, hasReturn) { // console.log('[DEBUG] triggerRequest -->', method, payload, endpoint, hasReturn) return new Promise((resolve, reject) => { let request = new XMLHttpRequest() request.open(method, endpoint, true) request.setRequestHeader('Authorization', _cvConfig.customerKey) if (payload) { request.setRequestHeader('Content-Type', 'application/json; charset=UTF-8') request.send(JSON.stringify(payload)) } else { request.send() } request.onload = () => { if (request.status === 200 || request.status === 202) { if (hasReturn) resolve(request.responseText) else resolve() } else { new _CVPROCESSBEAT('INFO', 'triggerRequest', `Status ${request.status} para endpoint ${endpoint}`, payload) reject() } } request.onerror = () => { new _CVPROCESSBEAT('ERROR', 'triggerRequest', `Status ${request.status} para endpoint ${endpoint}`, payload) reject() } }) } async function setCookieCV(name, value, days) { let d = new Date() d.setTime(d.getTime() + (days * 24 * 60 * 60 * 1000)) let expires = "expires=" + d.toUTCString() document.cookie = name + "=" + value + ";" + expires + ";path=/" } async function getCookieCV(cname) { return new Promise(async resolve => { let name = cname + "=" let decodedCookie = decodeURIComponent(document.cookie) let ca = decodedCookie.split(';') for (let i = 0; i < ca.length; i++) { let c = ca[i] while (c.charAt(0) == ' ') { c = c.substring(1); } if (c.indexOf(name) == 0) { resolve(c.substring(name.length, c.length)) } } resolve(0) }) } async function generateCVID() { let s4 = () => { return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1) } return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() } async function replaceChain(s) { return await s.replaceAll('-', ' ').replaceAll('_', ' ').replaceAll(' ', ' ') .replaceAll('á', 'a').replaceAll('à', 'a').replaceAll('ã', 'a').replaceAll('â', 'a') .replaceAll('é', 'e').replaceAll('è', 'e').replaceAll('ê', 'e') .replaceAll('í', 'i').replaceAll('ì', 'i') .replaceAll('ó', 'o').replaceAll('ò', 'o').replaceAll('õ', 'o').replaceAll('ô', 'o') .replaceAll('ú', 'u').replaceAll('ù', 'u').replaceAll('û', 'u') .replaceAll('ç', 'c').trim().toLowerCase() } async function validateEmail(email) { if (/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,4})+$/.test(email)) return true else return false } function processTimeSpend() { let startInterval = () => { setInterval(() => { let time = Number(localStorage._cvtime) time++ localStorage.setItem('_cvtime', time) }, 1000) } if (_cvConfig.singlePage == true) { if (!localStorage._cvtime) { _cvConfig.intervalStarts = true localStorage.setItem('_cvtime', '0') startInterval() } else if (!_cvConfig.intervalStarts) { _cvConfig.intervalStarts = true startInterval() } } else { if (!localStorage._cvtime) localStorage.setItem('_cvtime', '0') startInterval() } } async function getUrlParameters(params, compare) { if (!params) params = location.search let results = {} if (params) { let query = params.substring(1); query.split('&').forEach(p => { let item = p.split('=') if (compare) { if (item[0].indexOf(compare) >= 0) { results[item[0]] = decodeURIComponent(item[1]) } } else { results[item[0]] = decodeURIComponent(item[1]) } }) return results } else { return results } } async function processListObjs(values, list) { if (values.length > 0) { if (list.length > 0) { let listToNav = [] for (let i = 0; i < values.length; i++) { listToNav.push({ value: values[i], count: 1, level: i }) let index = await checkValueExists(values[i], list) if (index >= 0) { list[index].count++ } else { await list.push({ value: values[i], count: 1, level: i }) } } await orderCvList(list, 'count') return [list, listToNav] } else { let listReturn = [] for (let i = 0; i < values.length; i++) { listReturn.push({ value: values[i], count: 1, level: i }) } await orderCvList(listReturn, 'count') return [listReturn, listReturn] } } else { await orderCvList(list, 'count') return [list, undefined] } } async function checkValueExists(value, list) { for (let i = 0; i < list.length; i++) { if (await value == list[i].value) { return i } } return -1 } async function orderCvList(list, param) { if (list.length > 0) { let n = list.length for (let i = 1; i < n; ++i) { let key = list[i] let j = i - 1 while (j >= 0 && list[j][param] < key[param]) { list[j + 1] = list[j] j = j - 1 } list[j + 1] = key } } } class _Navigation { constructor() { this.urlBase = decodeURIComponent(window.location.origin + '/') this.url = decodeURIComponent(window.location.href) this.params = decodeURIComponent(window.location.search) this.cleanUrl = this.url.replace(this.params, '') this.pathsEncoded = window.location.pathname == '/' ? '' : window.location.pathname this.pathsDecoded = decodeURIComponent(this.pathsEncoded) this.geolocation = undefined this.browserInfos = this.getBrowserInfos(), this.categories_view = undefined this.products_view = undefined this.searches = undefined } getBrowserInfos() { let userAgentString = navigator.userAgent let chromeAgent = userAgentString.indexOf("Chrome") > -1 ? 'chrome' : undefined let IExplorerAgent = userAgentString.indexOf("MSIE") > -1 || userAgentString.indexOf("rv:") > -1 ? 'internet explorer' : undefined let firefoxAgent = userAgentString.indexOf("Firefox") > -1 ? 'firefox' : undefined let safariAgent = userAgentString.indexOf("Safari") > -1 ? 'safari' : undefined if ((chromeAgent) && (safariAgent)) safariAgent = undefined let operaAgent = userAgentString.indexOf("OP") > -1 ? 'opera' : undefined if ((chromeAgent) && (operaAgent)) chromeAgent = undefined return { userAgent: userAgentString, language: navigator.language, browser: safariAgent || chromeAgent || IExplorerAgent || operaAgent || firefoxAgent } } async getGeolocation() { return new Promise(resolve => { try { navigator.geolocation.getCurrentPosition(position => { resolve(position) }) } catch (e) { } }) } async getPathCategories(notValidCategories) { let validCategories = [] if (this.pathsEncoded != '') { let pathToCompare = this.pathsEncoded if (pathToCompare.slice(0, 1) == '/') pathToCompare = pathToCompare.slice(1) let pathCategories = pathToCompare.split('/') for (let i = 0; i < pathCategories.length; i++) { pathCategories[i] = decodeURIComponent(pathCategories[i]) let valid = true for (let j = 0; j < notValidCategories.length; j++) { if (await notValidCategories[j] == '$pagination') { if (isNaN(pathCategories[i]) == false) valid = false } else if (await notValidCategories[j] == pathCategories[i] || pathCategories[i] == '') valid = false } if (valid) validCategories.push(await replaceChain(pathCategories[i].toLowerCase())) } } return validCategories } } class _PayloadNavigation { constructor(navigation, session, hasChange) { this.url = navigation.url this.cleanUrl = navigation.cleanUrl this.params = navigation.params this.paths = navigation.pathsDecoded this.session = session.session this.hash = session.hash this.identified = session.identified this.page_views = session.page_views this.utms = session.utms.length > 0 ? session.utms : undefined this.permanence_time = Number(localStorage._cvtime) this.browserInfos = session.bis == false ? navigation.browserInfos : undefined this.categories_view = session.categories_view.length > 0 ? session.categories_view : undefined this.products_view = session.products_view.length > 0 ? session.products_view : undefined this.searches = session.searches.length > 0 ? session.searches : undefined this.navigation = { categories_view: navigation.categories_view, products_view: navigation.products_view, searches: navigation.searches } this.has_order = session.has_order this.custom_clicks = session.custom_clicks.length > 0 ? session.custom_clicks : undefined this.pushs = session.pushs this.shc = hasChange this.nps_answered = session.nps_answered this.geolocation = session.gls == false ? navigation.geolocation : undefined this.started_at = session.started_at } send() { triggerRequest('post', this, _cvConfig.endpoints.navigation, false) } } class _Session { constructor() { this.session = undefined this.hash = undefined this.identified = false this.page_views = 1 this.utms = [] this.categories_view = [] this.products_view = [] this.searches = [] this.custom_clicks = [] this.pushs = undefined this.nps_answered = undefined this.has_order = false this.started_at = undefined this.updated_at = undefined this.bis = false this.gls = false } init() { let session = JSON.parse(localStorage._cvsession) this.session = session.session this.hash = session.hash this.identified = session.identified this.page_views = session.page_views this.utms = session.utms this.categories_view = session.categories_view this.products_view = session.products_view this.searches = session.searches this.custom_clicks = session.custom_clicks this.pushs = session.pushs this.nps_answered = session.nps_answered this.has_order = session.has_order this.started_at = new Date(session.started_at) this.updated_at = new Date(session.updated_at) this.bis = session.bis this.gls = session.gls return this } save() { this.updated_at = new Date() localStorage.setItem('_cvsession', JSON.stringify(this)) } remove() { localStorage.removeItem('_cvsession') localStorage.removeItem('_cvtime') } async processSessionTime() { let now = new Date() let cloneUpdate = new Date(this.updated_at) cloneUpdate.setMinutes(cloneUpdate.getMinutes() + _cvConfig.session_timeout) if (now.getTime() >= cloneUpdate) { this.remove() return true } else return false } processTimeOut(sessionTimeOut) { let intervalId = setTimeout(() => { // triggerRequest('post', { session: this.session }, _cvConfig.endpoints.endsession, false) this.remove() clearInterval(intervalId) }, sessionTimeOut * 60000) } } class _Cart { constructor() { this.cart_id = undefined this.products = [] this.hasActiveProducts = false this.type = 1 this.id_origin = undefined this.created_at = undefined this.updated_at = undefined this.record_duration = undefined } init() { let cart = JSON.parse(localStorage._cvcart) this.cart_id = cart.cart_id this.products = cart.products this.hasActiveProducts = cart.hasActiveProducts this.type = cart.type this.id_origin = cart.id_origin this.created_at = new Date(cart.created_at) this.updated_at = new Date(cart.updated_at) this.record_duration = cart.record_duration return this } save() { this.updated_at = new Date() localStorage.setItem('_cvcart', JSON.stringify(this)) } async processCartDuration() { let now = new Date() let cloneUpdate = new Date(this.updated_at) cloneUpdate.setMinutes(cloneUpdate.getMinutes() + this.record_duration) if (now.getTime() >= cloneUpdate) { await this.clear() } } async clear() { localStorage.removeItem('_cvcart') } async clearRemoved() { this.hasActiveProducts = false for (let i = 0; i < this.products.length; i++) { if (await this.products[i].removed == true) { this.products.splice(i, 1) i-- } else this.hasActiveProducts = true } } async insertProduct(productToInsert, param, isEquals, check) { if (productToInsert[param]) { let index = false for (let i = 0; i < this.products.length; i++) { let product = this.products[i] try { if (isEquals == true) { if (await product[param] == productToInsert[param]) { index = i break } } else { if (await productToInsert[param].indexOf(product[param]) >= 0) { index = i break } } } catch (err) { new _CVPROCESSBEAT('ERROR', 'insertProduct', err) } } if (index === false) { this.products.push(productToInsert) this.hasActiveProducts = true this.save() } else { if (!check) { if (this.products[index].selectable) { this.products[index].selectable.qtd += productToInsert.selectable ? productToInsert.selectable.qtd : 1 if (productToInsert?.selectable?.sku_id) this.products[index].selectable.sku_id = productToInsert.selectable.sku_id } else this.products[index].selectable = productToInsert.selectable } if (this.products[index].removed == true) this.products[index].removed = false this.save() } } else { new _CVPROCESSBEAT('ERROR', 'insertProduct', 'Produto a ser inserido não possui parâmetro de comparação informado', productToInsert) } } async deleteProduct(param, valueToCompare, isEquals) { let exist = false for (let i = 0; i < this.products.length; i++) { let product = this.products[i] if (isEquals) { if (await product[param] == valueToCompare) { exist = true product.removed = true } } else { if (await valueToCompare.indexOf(product[param]) > -1) { exist = true product.removed = true } } } if (exist) { let active = this.products.findIndex(e => e.removed !== true) this.hasActiveProducts = active >= 0 ? true : false this.save() } else new _CVPROCESSBEAT('INFO', 'deleteProduct', 'Produto a ser deletado não foi encontrado no objeto de carrinho') } async checkProducts(currentProducts, param, isEquals) { let update = false for (let i = 0; i < currentProducts.length; i++) { // insert products in cart let exist = false for (let j = 0; j < this.products.length; j++) { if (isEquals == true) { if (await this.products[j][param] == currentProducts[i][param]) { exist = true break } } else { if (await currentProducts[i][param].indexOf(this.products[j][param]) >= 0) { exist = true break } } } if (!exist) { await processCartAddProduct(currentProducts[i], param, false, isEquals) update = true } else { await processCartAddProduct(currentProducts[i], param, false, isEquals, true) } } for (let i = 0; i < this.products.length; i++) { // delete products that are no longer in cart let exist = false for (let j = 0; j < currentProducts.length; j++) { if (isEquals == true) { if (await this.products[i][param] == currentProducts[j][param]) { exist = true break } } else { if (await currentProducts[j][param].indexOf(this.products[i][param]) >= 0) { exist = true break } } } if (!exist && this.products[i].removed != true) { update = true await processCartRemoveProduct(param, this.products[i][param], false, true) } } if (update) new _PayloadCart(this.init()).send() } } class _User { constructor() { this.email_address = undefined this.variants = {} this.origin = undefined this.optin_field = false } } class _Recommendation { constructor() { this.products = undefined } init() { let recommendation = JSON.parse(sessionStorage._cvrecommendation) this.products = recommendation.products return this } save() { sessionStorage.setItem('_cvrecommendation', JSON.stringify(this)) } } class _Product { constructor(product) { this.name = product.name this.sku = product.sku this.categories = product.categories this.url = product.url this.price = product.price this.selectable = product.selectable this.images = product.images this.defaultImage = product.defaultImage this.variants = product.variants this.removed = product.removed this.unavailable = product.unavailable } init() { if (sessionStorage._cvproduct) { let obj = JSON.parse(sessionStorage._cvproduct) this.name = obj.name this.sku = obj.sku this.categories = obj.categories this.url = obj.url this.price = obj.price this.selectable = obj.selectable this.images = obj.images this.defaultImage = obj.defaultImage this.variants = obj.variants this.removed = obj.removed this.unavailable = obj.unavailable return this } else return undefined } validate() { if (!this.name && !this.sku && this.categories.length > 0 && !this.price && !this.defaultImage) new _CVPROCESSBEAT('ERROR', 'validateProduct', 'Produto captado não atendeu os requisitos de validação', this) } save() { sessionStorage.setItem('_cvproduct', JSON.stringify(this)) } send() { triggerRequest('post', this, _cvConfig.endpoints.product, false) } } class _Push { constructor({ name, pushHTML, rules }) { this.name = name this.pushHTML = pushHTML this.rules = rules this.pushElement = undefined } showPush(navigation) { let alreadyPush = document.querySelector('#cv-push') if (!alreadyPush) { if (window.screen.width > 700) { let pushElement = document.createElement('section') pushElement.id = 'cv-push' pushElement.innerHTML = this.pushHTML this.pushElement = pushElement if (this.rules.recommendation === true) { this.processPushRecommendation(navigation) } else { document.body.appendChild(pushElement) if (this.rules.customCss) document.head.insertAdjacentHTML('beforeend', this.rules.customCss) this.addEvents() this.processShow(navigation) } } } } async processPushRecommendation(navigation) { if (sessionStorage._cvrecommendation) { let session = new _Session().init() let recommendation = new _Recommendation().init() this.showPushRecommendation(session, recommendation, navigation) } else { let session = new _Session().init() let payload = { hash: session.hash, categories: session.categories_view, rules: { limit_of_products: 5 } } let recCall = await triggerRequest('post', payload, _cvConfig.endpoints.recommendation, true).catch(err => { }) recCall = JSON.parse(recCall) if (recCall.recommended) { let recommendation = new _Recommendation() recommendation.products = recCall.recommended recommendation.save() this.showPushRecommendation(session, recommendation, navigation) }//else no products to recommended } } async showPushRecommendation(session, recommendation, navigation) { let indexRecom = 0 let productRecom = recommendation.products[indexRecom] if (session.products_view.length > 0) { for (let i = 0; i < session.products_view.length; i++) { if (session.products_view[i].value == productRecom.sku || productRecom.show == true) { indexRecom += 1 if (await recommendation.products[indexRecom] != undefined) { productRecom = recommendation.products[indexRecom] } else { productRecom = undefined; break } } else break } } if (productRecom) { this.pushElement.style = 'display: none' document.body.appendChild(this.pushElement) if (this.rules.customCss) document.head.insertAdjacentHTML('beforeend', this.rules.customCss) let titleElement = document.querySelector('.cv-push-box p') titleElement.innerText = productRecom.name let imgElement = document.querySelector('.cv-push-img img') imgElement.src = productRecom.defaultImage this.pushElement.style = '' recommendation.products[indexRecom].show = true recommendation.save() this.addEvents(productRecom.url + this.rules.params) this.processShow(navigation, productRecom) }//else no products to recommended } addEvents(redirect) { let closedElement = document.querySelector('.cv-push-closed img') closedElement.addEventListener('click', () => { this.pushElement.remove() }) let boxElement = document.querySelector('.cv-push-box') boxElement.addEventListener('click', () => { this.click(redirect) }) let imgElement = document.querySelector('.cv-push-img') imgElement.addEventListener('click', () => { this.click(redirect) }) } async processShow(navigation, product) { let session = new _Session().init() let createPush = exist => { let pushPayload = { name: this.name, attempts: 1, clicks: 0, intervalAttempts: 0, history: [{ url: navigation.cleanUrl, created_at: new Date(), click: 0, product: product ? { sku: product.sku } : undefined }] } if (exist) { session.pushs.push(pushPayload) } else { session.pushs = [pushPayload] } session.save() } if (session.pushs) { let index = await session.pushs.findIndex(e => e.name == this.name) if (index >= 0) { session.pushs[index].attempts++ session.pushs[index].intervalAttempts = 0 session.pushs[index].history.push({ url: navigation.cleanUrl, created_at: new Date(), click: 0, product: product ? { sku: product.sku } : undefined }) session.save() } else createPush(true) } else createPush(false) } async click(redirect) { let session = new _Session().init() let index = await session.pushs.findIndex(e => e.name == this.name) if (index >= 0) { session.pushs[index].clicks++ session.pushs[index].history[session.pushs[index].history.length - 1].click++ session.save() if (redirect) window.location.replace(redirect) else window.location.replace(this.rules.redirect) } else new _CVPROCESSBEAT('ERROR', 'clickPush', `Objeto push ${this.name} não existe na lista de pushs da sessão`) } } class _PayloadCart { constructor(cart) { let session = new _Session().init() this.session = session.session this.hash = session.hash this.instance = new _Navigation().cleanUrl this.identified = session.identified this.utms = session.utms.length > 0 ? session.utms : undefined this.cart_id = cart.cart_id this.products = cart.products this.type = cart.type this.id_origin = cart.id_origin this.created_at = cart.created_at this.updated_at = cart.updated_at this.pushs = session.pushs } send() { triggerRequest('post', this, _cvConfig.endpoints.cart, false) } } class _CVPROCESSBEAT { constructor(type, method, message, payload) { this.type = type this.method = method this.message = message this.payload = payload this.process() } process() { switch (this.type) { case 'INFO': console.log(`[CV.INFO.${this.method}] ${this.message}`) break case 'ERROR': // triggerRequest('POST', this, _cvConfig.endpoints.beat, false) break default: break } } }let _cvConfig = { "customerKey": "6259EEDMWW", "session_timeout": 5, "windowLoad": false, "cart_duration": 10080, "singlePage": true, "geolocation": false, "notValidCategories": [ "$pagination", "#", "checkout", "cart", "profile", "shipping", "account", "_secure", "orders", "addresses", "cards", "login", "nossas-lojas", "p" ], "endpoints": { "navigation": "https://oci.claravista-api.com.br/data/navigation", "cart": "https://oci.claravista-api.com.br/data/cart", "product": "https://oci.claravista-api.com.br/data/product", "user": "https://oci.claravista-api.com.br/data/user", "beat": "https://oci.claravista-api.com.br/data/beat", "recommendation": "https://dashp5.claravista.com.br/core/api/v.2.2/recommendation/push" }, "functions": { "forNavCategories": "_cGetNavCategorie", "isProductPage": "_cIsProductPage", "isCategoriePage": "_cIsCategoriePage", "forNavProduct": "_cGetNavProduct", "forNavSearches": "_cGetSeachers", "forProductInfos": "_cGetProductInfo", "addProductToCart": "_cAddProductToCart", "removeProductToCart": "_cRemoveProductToCart", "forOriginCart": "_cOriginCartPage", "isOrderPage": "_cIsOrderPage", "getLoged": "_cGetLoged", "checkCartProducts": "_cCheckCartProducts" }, "custom_clicks": [], "pushs": [], "optin_forms": [], "modals": [], "cart_all_pages": true } // ============================== // MALWEE - v.2.10 // // 2.10 (25/09/2026): // sku da PDP -> aceita skuId/idsku/sku, a query do runtime e, sem parametro, // resolve pelo slug no catalogo. So o skuId era lido e 74% das // PDPs chegavam sem produto visto // eventos -> size_select, shipping_simulation, add_to_cart e tag_error // vao como cv_event dentro do POST de navigation. A troca de // tamanho deixa de contar como page view // dispositivo -> o hash anonimo segue em variants.device_hashes ao identificar // checkout -> o Compra Rapida (iframe) grava direto no orderForm; a leitura // passa a ser do orderForm no servidor, nao da copia do vtexjs // Reescrita seguindo o padrao validado na Caedu. O site segue em VTEX IO, entao // nao ha mudanca de front: o que muda e de onde o dado vem. // // Preservado por estar funcionando bem (95% de cobertura de categoria): // _cGetNavCategorie, _cGetNavProduct, getProductFromEvent, _cGetNewsletter // // Trocado: // produto -> API de catalogo pelo skuId, em vez de raspagem do DOM. Devolve // o sku real, o preco e as imagens; os seletores de imagem e de // cor do DOM estavam mortos e o sku capturado era o productId // carrinho -> orderForm da VTEX em qualquer pagina, com reconciliacao // continua, em vez de escutar so o botao da PDP // busca -> reativada; estava retornando [] fixo // usuario -> /api/sessions (fonte verificada), em vez do e-mail persistido // em localStorage.orderform, que pode ser de um acesso antigo // ============================== window.__cvMalweeState = window.__cvMalweeState || { routeKey: undefined, addToCartListenersStarted: false, identifyListenersStarted: false, newsletterListenersStarted: false, catalogCache: {}, productsSent: {}, cartSignature: undefined, cartTimer: undefined, reconciling: false, orderFormId: undefined, lastOrderFormRead: 0, cartSignalAt: 0, identifiedEmail: undefined, slugSku: {}, lastNav: undefined, cartMap: undefined, eventsSent: {}, errorsSent: 0, skuInfo: {} } try { const saved = JSON.parse(sessionStorage.getItem('_cvmalwee_skuinfo') || 'null') if (saved && typeof saved === 'object') window.__cvMalweeState.skuInfo = Object.assign(saved, window.__cvMalweeState.skuInfo) } catch (e) { } // Liga os cv_event. So pode ficar true com o Core que os desvia para // score_events em producao; antes disso eles virariam navegacoes comuns const _cvMalweeEventsEnabled = true function _cvMalweeResetSpaState() { const key = window.location.pathname + window.location.search + window.location.hash if (window.__cvMalweeState.routeKey === key) return window.__cvMalweeState.routeKey = key window.__cvMalweeState.errorsSent = 0 } // ============================== // PAGINA: TIPO // ============================== // Nao depende mais do objeto navigation, que podia vir nulo em chamadas fora // do ciclo do motor function _cIsProductPage() { return /\/p\/?$/i.test(window.location.pathname) } // A busca do VTEX IO fica em /s?q=. Os parametros legados seguem aceitos function _cIsSearchPage() { const url = new URL(window.location.href) const path = url.pathname.toLowerCase() if (/^\/s(\/|$)/.test(path)) return true if (url.searchParams.has('q') || url.searchParams.has('ft') || url.searchParams.has('_q')) return true if ((url.searchParams.get('map') || '').split(',').indexOf('ft') >= 0) return true return false } async function _cIsCategoriePage(navigation) { const layer = window.dataLayer || [] for (let i = 0; i < layer.length; i++) { const pc = layer[i]?.pageCategory if (pc === 'Category' || pc === 'Departament') return true } if (!_cIsProductPage() && !_cIsSearchPage()) return true return false } // ============================== // PAGINA: CATEGORIA, PRODUTO E BUSCA // ============================== // Preservada da versao anterior. A espera pelo evento productDetail e essencial: // numa carga nova ele ainda nao existe quando o motor pede as categorias function getProductFromEvent(navigation) { return new Promise((resolve) => { if (_cIsProductPage()) { let attempts = 0 const interval = setInterval(() => { let event const layer = window.dataLayer || [] for (let i = layer.length - 1; i >= 0; i--) { if (layer[i]?.event === 'productDetail') { event = layer[i] break } } if (event || ++attempts >= 5) { const [product] = event?.ecommerce?.detail?.products || [] clearInterval(interval) return resolve(product) } }, 400) } else resolve() }) } // Preservada: e o que sustenta os 95% de cobertura de categoria async function _cGetNavCategorie(navigation) { const categories = [] const isProduct = _cIsProductPage() const product = await getProductFromEvent(navigation) if (isProduct && product?.category) { let prodCategories = product.category.toLowerCase().split('/') for (let i = 0; i < prodCategories.length; i++) { const category = prodCategories[i] if (category) categories.push(await replaceChain(category)) } } else if (isProduct) { // o evento nao veio. O path de uma PDP e //p, entao usa-lo daria // o slug e a letra p como categoria. O catalogo tem o caminho correto const sku = await _cvMalweeResolveSku() const catalog = sku ? await _cvMalweeGetCatalogProduct(sku) : undefined for (let i = 0; i < (catalog?.categories || []).length; i++) { categories.push(await replaceChain(String(catalog.categories[i]).toLowerCase())) } } else { if (!_cIsSearchPage() && !(await _cIsOrderPage(navigation))) { const pathCategories = await navigation.getPathCategories(_cvConfig.notValidCategories) for (let i = 0; i < pathCategories.length; i++) { categories.push(await replaceChain(pathCategories[i].toLowerCase())) } } } return categories } async function _cGetNavProduct(navigation) { if (!_cIsProductPage()) return [] const sku = await _cvMalweeResolveSku() return sku ? [String(sku)] : [] } // O sku vem com nomes diferentes conforme a origem do link: skuId na vitrine, // idsku em e-mail e midia, e nenhum quando o link aponta so para o produto function _cvMalweeSkuFromParams() { const read = entries => { for (const [key, value] of entries) { if (['skuid', 'idsku', 'sku'].indexOf(String(key).toLowerCase()) >= 0 && /^\d+$/.test(String(value || ''))) return String(value) } return undefined } const fromUrl = read(new URLSearchParams(window.location.search).entries()) if (fromUrl) return fromUrl try { const query = window.__RUNTIME__?.query if (query && typeof query === 'object') return read(Object.entries(query)) } catch (e) { } return undefined } async function _cvMalweeResolveSku() { if (!_cIsProductPage()) return undefined const fromParams = _cvMalweeSkuFromParams() if (fromParams) return fromParams // sem parametro: o slug da PDP e o linkText do produto const slug = window.location.pathname.replace(/\/p\/?$/i, '').split('/').filter(Boolean).pop() if (!slug) return undefined if (window.__cvMalweeState.slugSku[slug] !== undefined) return window.__cvMalweeState.slugSku[slug] try { const cached = sessionStorage.getItem(`_cvmalwee_slug_${slug}`) if (cached) return window.__cvMalweeState.slugSku[slug] = cached } catch (e) { } try { const response = await fetch(`/api/catalog_system/pub/products/search/${encodeURIComponent(slug)}/p`, { headers: { accept: 'application/json' } }) if (!response.ok) return undefined const list = await response.json() const raw = Array.isArray(list) ? list[0] : undefined const items = raw?.items || [] const item = items.find(i => (i.sellers || []).some(s => s.commertialOffer?.AvailableQuantity > 0)) || items[0] if (!item) return undefined const sku = String(item.itemId) const product = _cvMalweeBuildCatalogProduct(raw, sku) if (product) { window.__cvMalweeState.catalogCache[sku] = product try { sessionStorage.setItem(`_cvmalwee_sku_${sku}`, JSON.stringify(product)) } catch (e) { } } window.__cvMalweeState.slugSku[slug] = sku try { sessionStorage.setItem(`_cvmalwee_slug_${slug}`, sku) } catch (e) { } return sku } catch (err) { new _CVPROCESSBEAT('ERROR', '_cvMalweeResolveSku', `Falha ao resolver o sku pelo slug ${slug} --> ${err}`) return undefined } } // Reativada. Estava retornando [] fixo, com a implementacao comentada, e a // captacao de busca ficou em 0% por meses async function _cGetSeachers(navigation) { const url = new URL(window.location.href) let values = [ url.searchParams.get('q'), url.searchParams.get('ft'), url.searchParams.get('_q') ] // /s/ e /?map=ft, formatos alternativos do VTEX IO const match = url.pathname.match(/^\/s\/(.+)$/i) if (match) values.push(match[1].split('/')[0]) if ((url.searchParams.get('map') || '').split(',').indexOf('ft') >= 0) { const seg = url.pathname.split('/').filter(Boolean) if (seg.length) values.push(seg[seg.length - 1]) } values = values .filter(Boolean) .map(v => decodeURIComponent(String(v)).replace(/\+/g, ' ').trim().toLowerCase()) .filter(v => v.length > 0 && v.length < 80) const unicos = values.filter((v, i) => values.indexOf(v) === i) let out = [] for (let i = 0; i < unicos.length; i++) out.push(await replaceChain(unicos[i])) return out } // ============================== // CATALOGO // A raspagem do DOM perdeu os seletores de imagem e de cor, e gravava o // productId no lugar do sku. O catalogo da VTEX resolve os tres de uma vez // ============================== function _cvMalweeBuildCatalogProduct(raw, expectedSku) { const items = raw.items || [] const item = items.find(i => String(i.itemId) === String(expectedSku)) || items[0] if (!item) return undefined const offer = (item.sellers || []).map(s => s.commertialOffer).find(Boolean) // a troca de tamanho acontece antes de o sku novo estar em cache; guardar // todos os skus do produto permite montar o size_select na hora for (let i = 0; i < items.length; i++) { window.__cvMalweeState.skuInfo[String(items[i].itemId)] = { product_id: String(raw.productId), size: Array.isArray(items[i].Tamanho) ? items[i].Tamanho[0] : undefined } } // o produto pode vir do cache da sessao na proxima pagina, sem passar aqui try { sessionStorage.setItem('_cvmalwee_skuinfo', JSON.stringify(window.__cvMalweeState.skuInfo)) } catch (e) { } let categories = [] const paths = (raw.categories || []).slice().sort((a, b) => b.split('/').length - a.split('/').length) if (paths[0]) categories = paths[0].split('/').filter(Boolean).map(c => c.trim().toLowerCase()) const images = (item.images || []).map(i => i.imageUrl).filter(Boolean) let installment = undefined const installments = (offer?.Installments || []).filter(i => i.InterestRate === 0) for (let i = 0; i < installments.length; i++) { if (!installment || installments[i].NumberOfInstallments > installment.NumberOfInstallments) installment = installments[i] } // raw.link vem no dominio interno e essa url vai para o e-mail de campanha let url = raw.link if (raw.linkText) url = `${window.location.origin}/${raw.linkText}/p` else if (typeof url === 'string') url = url.replace(/^https?:\/\/[^/]+/, window.location.origin) return { name: raw.productName, sku: String(item.itemId), categories, url, // sku sem oferta vem com preco sentinela e ia assim para o e-mail price: offer?.Price > 0 && offer?.Price < 99998 ? offer.Price : undefined, images, defaultImage: images[0], variants: { brand: raw.brand, category: (paths[0] || '').split('/').filter(Boolean).join('/'), dimension3: item.name, id: String(raw.productId), size: Array.isArray(item.Tamanho) ? item.Tamanho[0] : undefined, qtdInstallments: installment?.NumberOfInstallments, valueInstallments: installment?.Value }, unavailable: offer ? !(offer.AvailableQuantity > 0) : undefined } } function _cvMalweeGetCatalogProduct(sku) { return new Promise(async resolve => { if (!sku) return resolve(undefined) sku = String(sku) if (window.__cvMalweeState.catalogCache[sku] !== undefined) return resolve(window.__cvMalweeState.catalogCache[sku]) try { const cached = sessionStorage.getItem(`_cvmalwee_sku_${sku}`) if (cached) { const parsed = JSON.parse(cached) window.__cvMalweeState.catalogCache[sku] = parsed return resolve(parsed) } } catch (e) { } try { const response = await fetch(`/api/catalog_system/pub/products/search?fq=skuId:${sku}`, { headers: { accept: 'application/json' } }) if (!response.ok) return resolve(undefined) const list = await response.json() const raw = Array.isArray(list) ? list[0] : undefined if (!raw) return resolve(undefined) const product = _cvMalweeBuildCatalogProduct(raw, sku) if (!product) return resolve(undefined) window.__cvMalweeState.catalogCache[sku] = product try { sessionStorage.setItem(`_cvmalwee_sku_${sku}`, JSON.stringify(product)) } catch (e) { } return resolve(product) } catch (err) { new _CVPROCESSBEAT('ERROR', '_cvMalweeGetCatalogProduct', `Falha ao buscar o sku ${sku} no catálogo --> ${err}`) return resolve(undefined) } }) } function _cvMalweeSendProductOnce(product) { if (!product || !product.sku) return if (!product.name || !product.price || !product.defaultImage) return const sku = String(product.sku) if (window.__cvMalweeState.productsSent[sku] === true) return window.__cvMalweeState.productsSent[sku] = true new _Product(product).send() } // Produto visualizado na PDP async function _cGetProductInfo(navigation) { _cvMalweeResetSpaState() const sku = await _cvMalweeResolveSku() if (!sku) return undefined const catalog = await _cvMalweeGetCatalogProduct(sku) if (!catalog) { new _CVPROCESSBEAT('ERROR', '_cGetProductInfo', `Produto ${sku} não foi encontrado no catálogo`) return undefined } let product = JSON.parse(JSON.stringify(catalog)) // o breadcrumb do dataLayer reflete a navegacao melhor que o path do catalogo const navCategories = await _cGetNavCategorie(navigation) if (navCategories.length > 0) product.categories = navCategories return product } // ============================== // CARRINHO // ============================== function _cvMalweeNormalizeItems(items) { let products = [] for (let i = 0; i < (items || []).length; i++) { const item = items[i] const sku = item?.id || item?.itemId || item?.sku const qtd = Number(item?.quantity) if (sku) products.push({ sku: String(sku), qtd: Number.isFinite(qtd) && qtd > 0 ? qtd : 1 }) } return products } function _cvMalweeGetOrderFormId() { return new Promise(async resolve => { if (window.__cvMalweeState.orderFormId) return resolve(window.__cvMalweeState.orderFormId) try { const response = await fetch('/api/sessions?items=*', { credentials: 'include', headers: { accept: 'application/json' } }) if (!response.ok) return resolve(undefined) const json = await response.json() const id = json?.namespaces?.checkout?.orderFormId?.value if (id) window.__cvMalweeState.orderFormId = id return resolve(id) } catch (e) { return resolve(undefined) } }) } // Retorna [] quando o carrinho existe e esta vazio e undefined quando nao foi // possivel ler, para nunca limpar o carrinho local por engano function _cvMalweeReadOrderFormItems() { return new Promise(async resolve => { // no checkout o orderForm vivo e a autoridade, inclusive quando vazio const live = window.vtexjs?.checkout?.orderForm if (live && Array.isArray(live.items)) return resolve(_cvMalweeNormalizeItems(live.items)) // localStorage.orderform NAO entra aqui: a copia persistida sobrevive no // dispositivo e mascararia o carrinho real com um estado antigo const id = await _cvMalweeGetOrderFormId() if (id) { try { const response = await fetch(`/api/checkout/pub/orderForm/${id}`, { credentials: 'include', headers: { accept: 'application/json' } }) if (response.status === 404) { window.__cvMalweeState.orderFormId = undefined return resolve(undefined) } if (response.ok) { const json = await response.json() window.__cvMalweeState.lastOrderFormRead = new Date().getTime() if (Array.isArray(json.items)) return resolve(_cvMalweeNormalizeItems(json.items)) } } catch (err) { new _CVPROCESSBEAT('ERROR', '_cvMalweeReadOrderFormItems', `Falha ao ler o orderForm --> ${err}`) } } // ultima alternativa: o evento view_cart do dataLayer, que era a unica // fonte da versao anterior. So existe na pagina de carrinho const layer = window.dataLayer || [] for (let i = layer.length - 1; i >= 0; i--) { const items = layer[i]?.event === 'view_cart' ? layer[i]?.ecommerce?.items : undefined if (Array.isArray(items)) { let products = [] for (let j = 0; j < items.length; j++) { const sku = items[j]?.item_variant if (sku) products.push({ sku: String(sku), qtd: Number(items[j]?.quantity) > 0 ? Number(items[j].quantity) : 1 }) } return resolve(products) } } return resolve(undefined) }) } async function _cCheckCartProducts(navigation) { _cvMalweeResetSpaState() // o motor chama esta funcao em todo init, entao ela tambem garante os // observadores quando a sessao nao comeca por uma pagina de produto _cAddProductToCart() const items = await _cvMalweeReadOrderFormItems() if (items === undefined) return undefined let products = [] for (let i = 0; i < items.length; i++) { const item = items[i] const catalog = await _cvMalweeGetCatalogProduct(item.sku) let product = catalog ? JSON.parse(JSON.stringify(catalog)) : { sku: String(item.sku) } if (catalog) _cvMalweeSendProductOnce(catalog) product.selectable = { qtd: item.qtd, sku_id: String(item.sku) } products.push(product) } // checkProducts resolve entrada e saida de produto, mas nao altera a // quantidade de um produto que ja esta no carrinho _cvMalweeSyncQuantities(items) return [products, 'sku', true] } function _cvMalweeSyncQuantities(items) { if (!localStorage._cvcart) return try { let cart = new _Cart().init() let update = false for (let i = 0; i < items.length; i++) { const item = items[i] const product = cart.products.find(p => String(p.sku) === String(item.sku)) if (product) { if (!product.selectable) product.selectable = {} if (product.selectable.qtd !== item.qtd) { product.selectable.qtd = item.qtd update = true } } } if (update) { cart.save() new _PayloadCart(cart).send() } } catch (err) { new _CVPROCESSBEAT('ERROR', '_cvMalweeSyncQuantities', `Falha ao sincronizar as quantidades --> ${err}`) } } async function _cvMalweeReconcileCart() { if (window.__cvMalweeState.reconciling === true) return window.__cvMalweeState.reconciling = true try { const objProducts = await _cCheckCartProducts() if (objProducts === undefined) return let cart = new _Cart() if (localStorage._cvcart) cart.init() await cart.checkProducts(objProducts[0], objProducts[1], objProducts[2]) } catch (err) { new _CVPROCESSBEAT('ERROR', '_cvMalweeReconcileCart', `Falha ao reconciliar o carrinho --> ${err}`) } finally { window.__cvMalweeState.reconciling = false } } async function _cvMalweeCheckCartChanged() { // na confirmacao de compra a VTEX ja esvaziou o carrinho. Reconciliar aqui // marcaria tudo como removido antes de o pedido ser enviado if (await _cIsOrderPage() === true) return const items = await _cvMalweeReadOrderFormItems() if (items === undefined) return _cvMalweeDetectAddToCart(items) const signature = items.map(i => `${i.sku}:${i.qtd}`).sort().join('|') if (signature === window.__cvMalweeState.cartSignature) return if (window.__cvMalweeState.reconciling === true) return await _cvMalweeReconcileCart() window.__cvMalweeState.cartSignature = signature } function _cvMalweeScheduleCartCheck(delay) { window.__cvMalweeState.cartSignalAt = new Date().getTime() clearTimeout(window.__cvMalweeState.cartTimer) window.__cvMalweeState.cartTimer = setTimeout(() => { _cvMalweeCheckCartChanged() }, delay) } // Observa o carrinho em qualquer pagina. A versao anterior escutava apenas o // botao da PDP, entao adicao pela vitrine ou pelo minicarrinho se perdia async function _cAddProductToCart() { if (window.__cvMalweeState.addToCartListenersStarted) return window.__cvMalweeState.addToCartListenersStarted = true // sinal mais forte: o proprio front chama a API de checkout para alterar o // carrinho. Vale para adicao, remocao e troca de quantidade try { const isCartCall = url => /\/api\/checkout\/pub\/orderForm/i.test(String(url || '')) const originalFetch = window.fetch window.fetch = function (...args) { let url = undefined try { url = typeof args[0] === 'string' ? args[0] : args[0]?.url } catch (e) { } const promise = originalFetch.apply(this, args) if (isCartCall(url) && /\/(items|attachments|coupons)/i.test(String(url))) promise.then(() => { _cvMalweeScheduleCartCheck(600) }).catch(() => { }) _cvMalweeWatchShipping(url, args[1]?.body) return promise } const originalOpen = XMLHttpRequest.prototype.open XMLHttpRequest.prototype.open = function (method, url, ...rest) { if (isCartCall(url) && /\/(items|attachments|coupons)/i.test(String(url))) this.addEventListener('load', () => { _cvMalweeScheduleCartCheck(600) }) this.__cvMalweeUrl = url return originalOpen.call(this, method, url, ...rest) } const originalSend = XMLHttpRequest.prototype.send XMLHttpRequest.prototype.send = function (body) { try { _cvMalweeWatchShipping(this.__cvMalweeUrl, body) } catch (e) { } return originalSend.call(this, body) } } catch (err) { new _CVPROCESSBEAT('ERROR', '_cAddProductToCart', `Falha ao observar as chamadas de carrinho --> ${err}`) } // o Apollo do VTEX IO guarda a referencia do fetch no boot, entao o // simulador de frete da PDP nao passa pelo wrapper acima. A Resource Timing // registra a chamada de qualquer forma, com a URL e as variaveis try { new PerformanceObserver(list => { const entries = list.getEntries() for (let i = 0; i < entries.length; i++) { if (/getShippingEstimates/.test(entries[i].name)) _cvMalweeWatchShipping(entries[i].name) } }).observe({ type: 'resource' }) } catch (err) { new _CVPROCESSBEAT('ERROR', '_cAddProductToCart', `Falha ao observar o simulador de frete --> ${err}`) } // eventos de analytics publicados pela loja try { const dataLayer = window.dataLayer = window.dataLayer || [] const originalPush = dataLayer.push.bind(dataLayer) const cartEvents = ['add_to_cart', 'addToCart', 'remove_from_cart', 'removeFromCart', 'view_cart', 'cart'] dataLayer.push = function (...args) { try { for (let i = 0; i < args.length; i++) { if (cartEvents.indexOf(args[i]?.event) >= 0) _cvMalweeScheduleCartCheck(700) } } catch (e) { } return originalPush(...args) } } catch (err) { new _CVPROCESSBEAT('ERROR', '_cAddProductToCart', `Falha ao observar o dataLayer --> ${err}`) } // rede de seguranca por clique, delegada no documento para sobreviver ao // reaproveitamento de elementos pela SPA const isCartButton = el => { const text = (el?.innerText || el?.textContent || '').trim().toLowerCase() const label = (el?.getAttribute?.('aria-label') || '').toLowerCase() return /adicionar ao carrinho|adicionar a sacola|adicionar à sacola|comprar agora/.test(text + ' ' + label) } const onClick = event => { if (isCartButton(event.target?.closest('button, a'))) _cvMalweeScheduleCartCheck(1500) } document.addEventListener('click', onClick, true) document.addEventListener('touchstart', onClick, true) // rede final. No checkout a leitura e local e barata; fora dele custa uma // chamada, entao mantem ritmo curto apenas apos um sinal setInterval(() => { const local = !!window.vtexjs?.checkout?.orderForm if (local) return _cvMalweeCheckCartChanged() const agora = new Date().getTime() const recente = agora - window.__cvMalweeState.cartSignalAt < 60000 const intervalo = recente ? 3000 : 30000 if (agora - window.__cvMalweeState.lastOrderFormRead >= intervalo) _cvMalweeCheckCartChanged() }, 1000) } // A remocao passa a ser resolvida pela reconciliacao. A versao anterior criava // um setInterval novo em cada init, com seletores hasheados que nao existem mais async function _cRemoveProductToCart(navigation) { _cAddProductToCart() } async function _cOriginCartPage(navigation) { const alvo = (window.location.pathname + window.location.hash).toLowerCase() if (alvo.indexOf('/cart') >= 0) return 1 if (alvo.indexOf('checkout') >= 0) return 2 return 1 } // Passa a considerar tambem o hash, porque o motor monta pathsDecoded apenas // com location.pathname e a VTEX usa rotas com hash no checkout async function _cIsOrderPage(navigation) { const alvo = (window.location.pathname + window.location.hash).toLowerCase() return alvo.indexOf('orderplaced') >= 0 || alvo.indexOf('/confirmacao') >= 0 } // ============================== // IDENTIFICACAO // Nunca ler o e-mail de um formulario de login: isso identifica a sessao mesmo // quando a autenticacao falha. E nunca ler o orderform persistido, que pode // carregar o e-mail de um acesso anterior no mesmo dispositivo // ============================== async function _cvMalweeGetLogedProfile() { try { const response = await fetch('/api/sessions?items=*', { credentials: 'include', headers: { accept: 'application/json' } }) if (response.ok) { const json = await response.json() const id = json?.namespaces?.checkout?.orderFormId?.value if (id) window.__cvMalweeState.orderFormId = id const profile = json?.namespaces?.profile const email = profile?.email?.value if (email) return { email, name: profile?.firstName?.value, origin: profile?.isAuthenticated?.value === 'true' ? 'loged' : 'checkout' } } } catch (e) { } // No checkout a identificacao acontece dentro do iframe do Compra Rapida, // que grava direto no orderForm da VTEX. A copia do vtexjs nesta pagina // nao ve essa escrita, entao a leitura e do orderForm no servidor if (/\/checkout/i.test(window.location.pathname)) { const id = window.__cvMalweeState.orderFormId || window.vtexjs?.checkout?.orderForm?.orderFormId if (id) { try { const response = await fetch(`/api/checkout/pub/orderForm/${id}`, { credentials: 'include', headers: { accept: 'application/json' } }) if (response.ok) { const json = await response.json() const email = json?.clientProfileData?.email // mascarado quando o perfil ja existe e o cliente nao logou. // Esse caso fica para o pedido que chega pelo servidor if (email && email.indexOf('*') < 0) return { email, name: json?.clientProfileData?.firstName, origin: json?.loggedIn === true ? 'loged' : 'checkout' } } } catch (e) { } } } return undefined } async function _cvMalweeIdentify(session) { if (!session) return const profile = await _cvMalweeGetLogedProfile() if (!profile?.email) return if (await validateEmail(profile.email) != true) return if (window.__cvMalweeState.identifiedEmail === profile.email) return window.__cvMalweeState.identifiedEmail = profile.email let user = new _User() user.email_address = profile.email user.origin = profile.origin user.optin_field = true if (profile.name) user.variants = { name: profile.name } _cvMalweeAttachDevice(user, session) registerCvUser(user, session) } const _submit_triggers = ['#btn-client-pre-email', '#go-to-shipping', 'button[type="submit"]'].join(', ') async function _cGetLoged(navigation, session) { _cvMalweeIdentify(session) if (window.__cvMalweeState.identifyListenersStarted === true) return window.__cvMalweeState.identifyListenersStarted = true const identifyNow = () => { if (window.__cvMalweeState.identifiedEmail !== undefined) return if (!localStorage._cvsession) return _cvMalweeIdentify(new _Session().init()) } // um clique de envio pode ter autenticado o cliente. Em vez de ler o campo // de e-mail, que registraria tambem um login que falhou, reconsulta a fonte // verificada algumas vezes depois do clique const checkAfterSubmit = () => { if (window.__cvMalweeState.identifiedEmail !== undefined) return const delays = [1500, 4000, 8000] for (let i = 0; i < delays.length; i++) setTimeout(identifyNow, delays[i]) } document.addEventListener('click', event => { if (event.target?.closest(_submit_triggers)) checkAfterSubmit() }, true) document.addEventListener('keydown', event => { if (event.key !== 'Enter' && event.keyCode !== 13) return if (event.target?.closest('form')) checkAfterSubmit() }, true) // no checkout o e-mail aparece no orderForm sem clique nesta pagina setInterval(() => { if (/\/checkout/i.test(window.location.pathname)) identifyNow() }, 4000) setInterval(identifyNow, 10000) } // ============================== // NEWSLETTER // Logica preservada da versao anterior, que funciona. Corrigido apenas o ciclo // de vida: o intervalo antigo nunca era limpo (o else com o limite estava // comentado) e nascia um novo a cada init, rodando a 500ms indefinidamente // ============================== function _cvMappedClick(element, callback) { if (element.__cvMapped === true) return element.__cvMapped = true element.addEventListener('click', callback) element.addEventListener('touchstart', callback) } async function _cGetNewsletter(session) { if (window.__cvMalweeState.newsletterListenersStarted) return window.__cvMalweeState.newsletterListenersStarted = true let keydownMapped = false const capture = async (emailInput) => { const email = emailInput.value || emailInput.innerText if (!email) return if (await validateEmail(email) != true) return if (window.__cvMalweeState.identifiedEmail === email) return window.__cvMalweeState.identifiedEmail = email let user = new _User() user.email_address = email user.origin = 'newsletter' user.optin_field = true user.variants = { categories: [] } // as categorias marcadas no rodape entram como preferencia declarada const wrapper = [...document.body.querySelectorAll('div')] .find(e => String(e.className).toLowerCase().includes('malnewsletterwrapperfields')) if (wrapper) { const marcadas = wrapper.querySelectorAll('input:checked') let categories = [] for (let i = 0; i < marcadas.length; i++) { const name = marcadas[i].getAttribute('name') if (name) categories.push(name.toLowerCase()) } if (categories.length) user.variants.categories = categories } const current = localStorage._cvsession ? new _Session().init() : session _cvMalweeAttachDevice(user, current) registerCvUser(user, current) } // remapeia enquanto a SPA recria o rodape, mas sem duplicar o listener setInterval(() => { const emailInput = document.querySelector('form input[name="email"]') if (!emailInput) return const botao = [...document.body.querySelectorAll('button')] .find(e => (e.innerText || '').toLowerCase().includes('cadastrar email')) if (!botao) return _cvMappedClick(botao, () => { capture(emailInput) }) if (keydownMapped === false) { keydownMapped = true document.addEventListener('keydown', event => { if (event.key !== 'Enter' && event.keyCode !== 13) return const campo = document.querySelector('form input[name="email"]') if (campo && event.target?.closest('form')?.contains(campo)) capture(campo) }) } }, 1000) } // ============================== // EVENTOS (Score de Intencao) // Cada evento vai como cv_event dentro de um POST de navigation. O Core desvia // esses POSTs para score_events e nao os conta como navegacao nem page view // ============================== const _cvMalweeOriginalTrigger = triggerRequest // A troca de tamanho muda o skuId na URL e o motor faz um init completo. O POST // de navigation que ele monta passa a carregar o size_select, e o page_views // incrementado e desfeito triggerRequest = function (method, payload, endpoint, hasReturn) { try { if (_cvMalweeEventsEnabled && endpoint === _cvConfig.endpoints.navigation && payload && !payload.cv_event && !payload.shc) _cvMalweeTagSizeSelect(payload) } catch (e) { } return _cvMalweeOriginalTrigger(method, payload, endpoint, hasReturn) } function _cvMalweeTagSizeSelect(payload) { let current try { const u = new URL(payload.url) // PDP aberta sem parametro: o sku inicial veio do slug const slug = u.pathname.replace(/\/p\/?$/i, '').split('/').filter(Boolean).pop() const sku = _cIsProductPage() ? (_cvMalweeSkuFromParams() || window.__cvMalweeState.slugSku[slug]) : undefined current = { path: u.pathname, sku } } catch (e) { return } const last = window.__cvMalweeState.lastNav window.__cvMalweeState.lastNav = current if (!last || !current.sku || !last.sku) return if (last.path !== current.path || last.sku === current.sku) return const info = window.__cvMalweeState.skuInfo[current.sku] payload.cv_event = { type: 'size_select', sku: current.sku, product_id: info?.product_id, size: info?.size, at: new Date() } // o motor ainda salva a sessao depois deste envio, com o page_views ja // incrementado. A correcao espera esse save e so desfaz se ninguem mexeu const counted = Number(payload.page_views) payload.page_views = Math.max(1, counted - 1) setTimeout(() => { try { let session = new _Session().init() if (session.page_views === counted && counted > 1) { session.page_views-- session.save() } } catch (e) { } }, 1500) } function _cvMalweeEmitEvent(type, data) { if (!_cvMalweeEventsEnabled) return if (!localStorage._cvsession) return // o mesmo evento chega por mais de um sinal (fetch, dataLayer, clique) const key = `${type}:${JSON.stringify(data)}` const now = new Date().getTime() if (now - (window.__cvMalweeState.eventsSent[key] || 0) < 5000) return window.__cvMalweeState.eventsSent[key] = now try { let payload = new _PayloadNavigation(new _Navigation(), new _Session().init()) payload.cv_event = Object.assign({ type }, data, { at: new Date() }) _cvMalweeOriginalTrigger('post', payload, _cvConfig.endpoints.navigation, false).catch(() => { }) } catch (e) { } } // Compara com o ultimo carrinho lido. A primeira leitura so registra a base: // o que ja estava no carrinho nao e adicao function _cvMalweeDetectAddToCart(items) { let previous = window.__cvMalweeState.cartMap if (previous === undefined) { try { previous = JSON.parse(sessionStorage.getItem('_cvmalwee_cartmap') || 'null') || undefined } catch (e) { } } let current = {} for (let i = 0; i < items.length; i++) current[items[i].sku] = (current[items[i].sku] || 0) + items[i].qtd window.__cvMalweeState.cartMap = current try { sessionStorage.setItem('_cvmalwee_cartmap', JSON.stringify(current)) } catch (e) { } if (previous === undefined) return for (const sku in current) { const added = current[sku] - (previous[sku] || 0) if (added > 0) { const info = window.__cvMalweeState.skuInfo[sku] _cvMalweeEmitEvent('add_to_cart', { sku, product_id: info?.product_id, size: info?.size, qty: added }) } } } // Na PDP o simulador e o app malwee-custom-shipping-simulator, que consulta a // query GraphQL getShippingEstimates. No carrinho o CEP vai em shippingData function _cvMalweeWatchShipping(url, body) { url = String(url || '') if (/getShippingEstimates/.test(url)) { let variables try { const params = new URL(url, window.location.origin).searchParams const ext = JSON.parse(params.get('extensions') || '{}') if (ext.variables) variables = JSON.parse(atob(ext.variables)) else variables = JSON.parse(params.get('variables') || '{}') } catch (e) { } const item = variables?.items?.[0] const info = item?.id ? window.__cvMalweeState.skuInfo[String(item.id)] : undefined _cvMalweeEmitEvent('shipping_simulation', { source: 'pdp', sku: item?.id ? String(item.id) : undefined, product_id: info?.product_id, cep_prefix: String(variables?.postalCode || '').replace(/\D/g, '').slice(0, 5) || undefined }) return } if (/\/api\/checkout\/pub\/orderForm\/[^/]+\/attachments\/shippingData/i.test(url)) { let cep try { const json = typeof body === 'string' ? JSON.parse(body) : undefined cep = json?.address?.postalCode || json?.selectedAddresses?.[0]?.postalCode } catch (e) { } if (!cep) return _cvMalweeEmitEvent('shipping_simulation', { source: 'cart', cep_prefix: String(cep).replace(/\D/g, '').slice(0, 5) }) } } // ============================== // DISPOSITIVO // Ao identificar, o motor troca o hash anonimo pelo hash do e-mail e descarta o // anterior. Ele segue junto para os eventos das ultimas 72h do mesmo aparelho // passarem a contar para o cliente // ============================== function _cvMalweeAttachDevice(user, session) { try { if (!session || session.identified === true || !session.hash) return user.variants = Object.assign({}, user.variants, { device_hashes: [String(session.hash)] }) } catch (e) { } } // ============================== // TELEMETRIA // O envio de erro do motor esta desligado, e a quebra de pedidos passou dois // meses sem ser notada. Os erros seguem amostrados, no maximo 3 por pagina // ============================== const _cvMalweeErrorSampleRate = 0.2 // avisos de configuracao que o motor registra como erro em toda pagina const _cvMalweeErrorNoise = [/formul.rios de opt-in/i] function _cvMalweeReportError(method, message) { if (_cvMalweeErrorNoise.some(r => r.test(String(message || '')))) return // o motor pede a captacao de produto em toda pagina (isProductPage != undefined // e sempre verdadeiro); fora da PDP o retorno vazio e esperado if (method === '_cGetProductInfo' && !_cIsProductPage()) return if (window.__cvMalweeState.errorsSent >= 3) return if (Math.random() >= _cvMalweeErrorSampleRate) return window.__cvMalweeState.errorsSent++ _cvMalweeEmitEvent('tag_error', { method: String(method || '').slice(0, 80), message: String(message || '').slice(0, 300), version: '2.10' }) } try { const originalProcess = _CVPROCESSBEAT.prototype.process _CVPROCESSBEAT.prototype.process = function () { if (this.type === 'ERROR') _cvMalweeReportError(this.method, this.message) return originalProcess.call(this) } } catch (e) { } boot()