import { storeActions } from "@/constants/store-actions"; import { getDeviceIdValue, getSessionIdValue, getSessionKeyValue, getUserIdValue, regenerateDeviceId, regenerateUserId, refreshSessionExpiration, areAllSessionCookiesSet, setSessionIdIfUnset, setSessionKeyIfUnset, } from "@/helpers/heritage-integration/cookie-helper"; import { queryStrings } from "@/constants/query-strings"; import { experimentSettings } from "@/constants/experiments"; import { analyticsPageEvents, GaCategories, GaActions, GaLabels, GaEvents, ValueToLogTypes, } from "@/constants/analytics"; import { AppointmentTypeStrings } from "@/constants/schedule-constants"; import store from "@/store"; import baseMixin from "@/mixins/base-mixin"; import { applicationConfig } from "../constants/application-config"; import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper"; import { fmgPageValues } from "@/router/router-constants/fmgPage-values"; import router from "@/router/index.js"; import { getQuerystringParameter } from "@/helpers/querystring-helper"; import { Variables } from "../constants/analytics"; export default { methods: { getPageName() { return getPageNameByQueryString(); }, async logPageView(pageEvent) { const currentPageName = getPageNameByQueryString(); await this.validateSession(); const submittedOrder = baseMixin.methods.getSubmittedOrder(); const hasSubmittedOrder = baseMixin.methods.hasSubmittedOrder(); const hasSubmittedOrderAtConfirmationPage = hasSubmittedOrder && currentPageName?.toLowerCase() == fmgPageValues.CONFIRMATION; const refSequenceNum = hasSubmittedOrderAtConfirmationPage ? submittedOrder.referralSequenceNumber : store.getters.order.referralSequenceNumber; const parentAccountNum = hasSubmittedOrderAtConfirmationPage ? submittedOrder.payment.parentAccountNumber : store.getters.order.payment.parentAccountNumber; var payload = { userId: getUserIdValue(), sessionKey: getSessionKeyValue(), pageName: currentPageName, sessionId: getSessionIdValue(), action: "", event: pageEvent, shouldUseSessionId: false, experimentsForUser: store.getters.applicationUser.experiments, referralSequenceNumber: refSequenceNum, parentAccountNumber: parentAccountNum, }; await baseMixin.methods.dispatchStoreAction(storeActions.LOG_PAGE_VIEW, payload, false); }, async logCustomEvent(category, action, label, value) { const currentPageName = getPageNameByQueryString(); await this.validateSession(); const refSequenceNum = store.getters.order.referralSequenceNumber || store.getters.submittedOrder?.referralSequenceNumber; var payload = { userId: getUserIdValue(), sessionKey: getSessionKeyValue(), pageName: currentPageName, sessionId: getSessionIdValue(), category: category, action: action, label: label, value: value, shouldUseSessionId: false, experimentsForUser: store.getters.applicationUser.experiments, referralSequenceNumber: refSequenceNum, parentAccountNumber: store.getters.order.payment.parentAccountNumber, }; await baseMixin.methods.dispatchStoreAction( storeActions.LOG_CUSTOM_EVENT, payload, false ); }, async pushEventToGA(category, action, label, pushToLogApp = false, valueToLogType = null) { const currentPageName = getPageNameByQueryString(); const labelToLog = getValueToLog(label, valueToLogType); const eventToBePushed = { event: GaEvents.GENERIC_EVENT, category: category, action: action, label: labelToLog, value: undefined, path: `/fmg/?${queryStrings.FMG_PAGE}=${currentPageName}`, }; pushToDataLayerIfDefined(eventToBePushed); if (pushToLogApp) { await this.logCustomEvent(category, action, labelToLog, undefined); } }, async pushVariableToDataLayer(data) { pushToDataLayerIfDefined(data); }, async pushPageViewToGA() { const currentPageName = getPageNameByQueryString(); const pageViewEvent = { event: GaEvents.PAGE_VIEW_EVENT, pagePath: `/fmg/?${queryStrings.FMG_PAGE}=${currentPageName}`, pageTitle: currentPageName, }; pushToDataLayerIfDefined(pageViewEvent); await this.logPageView(analyticsPageEvents.ENTRY); }, pushOrderToDataLayer() { // helper check for if an object is defined (but maybe falsey) const isDefined = (x) => x !== null && x !== undefined; // Get correct order object const hasSubmittedOrder = baseMixin.methods.hasSubmittedOrder(); const submittedOrder = baseMixin.methods.getSubmittedOrder(); const order = hasSubmittedOrder ? submittedOrder : store.getters.order; // Begin assembling payload for data layer const payload = {}; // Service Zip if ( order.serviceLocation.appointmentType === AppointmentTypeStrings.MOBILE && isDefined(order.serviceLocation.zipCode) ) { payload.serviceZipCode = order.serviceLocation.zipCode; } else if ( isDefined(order.serviceLocation.appointmentType) && order.serviceLocation.appointmentType !== AppointmentTypeStrings.MOBILE && isDefined(order.serviceLocation.provider.address.zipCode) ) { payload.serviceZipCode = order.serviceLocation.provider.address.zipCode; } else { payload.serviceZipCode = ""; } // Damage Type if (isDefined(order.damage.isRepair)) { payload.damageType = order.damage.isRepair ? "repair" : "replace"; } else { payload.damageType = ""; } // Account Type if (isDefined(order.payment.isInsurance)) { payload.accountType = order.payment.isInsurance ? "insurance" : "cash"; } else { payload.accountType = ""; } // Promo Codes const promos = order.lineItems.promos ?? []; if (promos.length === 0) { payload.promoCodes = ""; } else { const promoCodes = promos.map((promo) => promo.promoCode); const promoString = promoCodes.reduce((prev, next) => `${prev},${next}`); payload.promoCodes = promoString; } // Vehicle info if (isDefined(order.vehicle.year)) { // Ensure cast to string. payload.vehicleYear = `${order.vehicle.year}`; } else { payload.vehicleYear = ""; } if (isDefined(order.vehicle.make)) { payload.vehicleMake = order.vehicle.make; } else { payload.vehicleMake = ""; } if (isDefined(order.vehicle.model)) { payload.vehicleModel = order.vehicle.model; } else { payload.vehicleModel = ""; } if (isDefined(order.vehicle.style)) { payload.vehicleStyle = order.vehicle.style; } else { payload.vehicleStyle = ""; } // Glass pieces const glass = order.damage.glassToReplace ?? []; if (glass.length === 0) { payload.glassToReplace = ""; } else { const glassNames = glass.map((g) => `${g.glassLocation}/${g.glassName}`); const glassString = glassNames.reduce((prev, next) => `${prev},${next}`); payload.glassToReplace = glassString; } //EON if (order.eon) { payload.eon = order.eon; } else { payload.eon = ""; } // Work Order Id if (order.workOrderId) { const parsedId = parseInt(order.workOrderId); if (!isNaN(parsedId)) { payload.workOrderId = parsedId; } else { payload.workOrderId = ""; } } else { payload.workOrderId = ""; } // Provider Ctu if (isDefined(order.serviceLocation.zipCodeCtu)) { payload.providerCtu = order.serviceLocation.zipCodeCtu; } else { payload.providerCtu = ""; } // Work Order Number if (order.workOrderNumber) { payload.orderNumber = order.workOrderNumber; } else { payload.orderNumber = ""; } // Pricing // Only fire for completed orders? const lineItems = order.lineItems ?? {}; const combinedLineItems = [ ...(lineItems.glassParts ?? []), ...(lineItems.supportingItems ?? []), ...(lineItems.vaps ?? []), ...(lineItems.promos ?? []), ]; const isPricingAvailable = combinedLineItems.length > 0 && combinedLineItems.every( (lineItem) => isDefined(lineItem.kitPrice) && isDefined(lineItem.laborAmount) && isDefined(lineItem.sellingPrice) ); const isTaxAvailable = isPricingAvailable && combinedLineItems.every((lineItem) => isDefined(lineItem.salesTax)); //unverified (in scenarios we don’t display the price) if ( order.payment.isInsurance && isDefined(order.payment.insuranceCoverage.isVerified) && !order.payment.insuranceCoverage.isVerified ) { payload.priceSubTotal = ""; } //deductible (in scenarios we don’t display the price) else if ( order.payment.isInsurance && isDefined(order.payment.insuranceCoverage.isVerified) && order.payment.insuranceCoverage.isVerified && isDefined(order.policy.currentDeductible) && order.policy.currentDeductible >= 0 && !order.policy.isItac && !order.policy.isNoComp ) { payload.priceSubTotal = ""; } else if (isPricingAvailable) { const subtotal = baseMixin.methods .getTotalPriceOfAllLineItemsAndChildParts(combinedLineItems, false) .toFixed(2); payload.priceSubTotal = parseFloat(subtotal); } else { payload.priceSubTotal = ""; } //unverified (in scenarios we don’t display the price) if ( order.payment.isInsurance && isDefined(order.payment.insuranceCoverage.isVerified) && !order.payment.insuranceCoverage.isVerified ) { payload.priceTotal = ""; } //deductible (in scenarios we don’t display the price) else if ( order.payment.isInsurance && isDefined(order.payment.insuranceCoverage.isVerified) && order.payment.insuranceCoverage.isVerified && isDefined(order.policy.currentDeductible) && order.policy.currentDeductible >= 0 && !order.policy.isItac && !order.policy.isNoComp ) { payload.priceTotal = ""; } else if (isTaxAvailable) { const total = baseMixin.methods .getTotalPriceOfAllLineItemsAndChildParts(combinedLineItems, true) .toFixed(2); payload.priceTotal = parseFloat(total); } else { payload.priceTotal = ""; } // Recalibration if (hasSubmittedOrder) { payload.isRecalibrationOnOrder = store.getters.isRecalibrationOnSubmittedState; } else { payload.isRecalibrationOnOrder = store.getters.isRecalibrationOnOrder; } // Appointment Type if (isDefined(order.serviceLocation.appointmentType)) { payload.appointmentType = order.serviceLocation.appointmentType; } else { payload.appointmentType = ""; } //Insurance if (order.payment.isInsurance) { payload.isInsuranceVerified = order.payment.insuranceCoverage.isVerified ?? ""; payload.insuranceCompanyName = order.policy.insuranceCompanyName ?? ""; if (!order.payment.insuranceCoverage.isVerified) { payload.isInsuranceItac = ""; payload.isInsuranceNoComp = ""; } else { payload.isInsuranceItac = order.policy.isItac ?? ""; payload.isInsuranceNoComp = order.policy.isNoComp ?? ""; } if ( order.policy.isItac || order.policy.isNoComp || !order.payment.insuranceCoverage.isVerified ) { payload.insuranceDeductible = ""; } else { payload.insuranceDeductible = order.policy.currentDeductible ?? ""; } } else { payload.isInsuranceVerified = ""; payload.insuranceDeductible = ""; payload.isInsuranceItac = ""; payload.isInsuranceNoComp = ""; payload.insuranceCompanyName = ""; } pushToDataLayerIfDefined(payload); }, pushExperimentsToDataLayer() { const experiments = store.getters.applicationUser.experiments; experiments?.forEach((exp) => { // Set Google Dimension Index based on experiment settings. let googleDimensionIndex = 99; if (exp.settings[experimentSettings.GOOGLE_CUSTOM_DIMENSION_INDEX] !== undefined) { googleDimensionIndex = exp.settings[experimentSettings.GOOGLE_CUSTOM_DIMENSION_INDEX]; } // Create object with dimension index and value. const experimentWithDimension = { [`experimentId_${googleDimensionIndex}`]: exp.universeId, [`variationId_${googleDimensionIndex}`]: exp.variationId, [`experimentName_${googleDimensionIndex}`]: exp.universeName, [`variationName_${googleDimensionIndex}`]: exp.variationName, [`customDimension_${googleDimensionIndex}`]: `${exp.universeId}_${exp.variationId}_${exp.universeName}_${exp.variationName}`, }; // Push to the data layer with the Google Custom Dimension Index. pushToDataLayerIfDefined(experimentWithDimension); }); }, pushProductArrayToDataLayer() { // Get correct order object const hasSubmittedOrder = baseMixin.methods.hasSubmittedOrder(); const submittedOrder = baseMixin.methods.getSubmittedOrder(); if (hasSubmittedOrder) { var lineItems = submittedOrder.lineItems ? submittedOrder.lineItems : {}; // combine all line items var combinedLineItems = [ ...(lineItems.glassParts ?? []), ...(lineItems.supportingItems ?? []), ...(lineItems.vaps ?? []), ...(lineItems.promos ?? []), ]; //Get child parts combinedLineItems = flattenArray(combinedLineItems); var products = []; var discount = 0; var coupon = ""; var subTotal = 0; for (var i = 0; i < combinedLineItems.length; i++) { let part = combinedLineItems[i]; let productSku = part.partNumber; let productType = part.partType; let promoCode = part.promoCode; let productPrice = baseMixin.methods.getTotalPriceOfAllLineItemsAndChildParts( [part], false ); if (productSku == "DISCOUNT" || productType == "SERVICE PACKAGE DISCOUNT") { if (coupon) { coupon += ","; } if (productSku == "DISCOUNT") { coupon += promoCode; } else { coupon += productSku; } discount += productPrice * -1; } else { products.push({ productType: productType, productSku: productSku, productPrice: productPrice.toFixed(2), productQuantity: "1", }); subTotal += productPrice; } } pushToDataLayerIfDefined({ event: "productArray", productArray: { coupon: coupon, discount: discount.toFixed(2), subTotal: subTotal.toFixed(2), products: products, }, }); } }, pushECommerceCartToDataLayer() { const isDefined = (x) => x !== null && x !== undefined; // Get correct order object const hasSubmittedOrder = baseMixin.methods.hasSubmittedOrder(); const submittedOrder = baseMixin.methods.getSubmittedOrder(); if (hasSubmittedOrder) { var lineItems = submittedOrder.lineItems ? submittedOrder.lineItems : {}; var supportingItems = submittedOrder.lineItems && submittedOrder.lineItems.supportingItems ? submittedOrder.lineItems.supportingItems : []; var insurance = submittedOrder.payment && submittedOrder.payment.isInsurance; var itac = submittedOrder.policy && submittedOrder.policy.isItac; var nocomp = submittedOrder.policy && submittedOrder.policy.isNoComp; var recalLineItem = supportingItems.filter(function (lineItem) { return lineItem.partType.indexOf("RECALIBRATION") !== -1; }); var disposalFee = supportingItems.filter(function (lineItem) { return lineItem.partType.indexOf("DISPOSAL FEE") !== -1; }); var mobileFee = supportingItems.filter(function (lineItem) { return lineItem.partType.indexOf("MOBILE FEE") !== -1; }); var repairFee = supportingItems.filter(function (lineItem) { return lineItem.partType.indexOf("REPAIR FEE") !== -1; }); var vaps = submittedOrder.lineItems && submittedOrder.lineItems.vaps ? submittedOrder.lineItems.vaps : []; var wipers = vaps.filter(function (lineItem) { return ( lineItem.partType.indexOf("FRONT WIPER") !== -1 || lineItem.partType.indexOf("REAR WIPER") !== -1 ); }); var rainDefense = vaps.filter(function (lineItem) { return lineItem.partType.indexOf("RAIN DEFENSE") !== -1; }); // Combine line items var combinedLineItems = []; if (lineItems.glassParts) { combinedLineItems = combinedLineItems.concat(lineItems.glassParts); } if (recalLineItem) { combinedLineItems = combinedLineItems.concat(recalLineItem); } if (disposalFee) { combinedLineItems = combinedLineItems.concat(disposalFee); } if (mobileFee && !(insurance && !itac && !nocomp)) { combinedLineItems = combinedLineItems.concat(mobileFee); } if (repairFee) { combinedLineItems = combinedLineItems.concat(repairFee); } if (wipers) { combinedLineItems = combinedLineItems.concat(wipers); } if (rainDefense) { combinedLineItems = combinedLineItems.concat(rainDefense); } //Remove child Parts if any combinedLineItems.forEach((lineItem) => { lineItem.childParts = []; }); var isPricingAvailable = combinedLineItems.length > 0 && combinedLineItems.every(function (lineItem) { return ( isDefined(lineItem.kitPrice) && isDefined(lineItem.laborAmount) && isDefined(lineItem.sellingPrice) ); }); let subtotal = 0; if (isPricingAvailable) { subtotal = baseMixin.methods.getTotalPriceOfAllLineItemsAndChildParts( combinedLineItems, false ); } pushToDataLayerIfDefined({ event: "eCommerceCart", eCommerceCart: { products: combinedLineItems, subTotal: parseInt(subtotal), }, }); } }, pushCommissionJunctionGtmDataToDataLayer() { // helper check for if an object is defined (but maybe falsey) const isDefined = (x) => x !== null && x !== undefined; let repairReplace = ""; let coupons = ""; let refSequenceNum = ""; let accountType = ""; let amount = 0; let cjEvent = ""; if (store.getters.applicationUser.affiliateCookies?.length > 0) { const affiliateCookies = store.getters.applicationUser.affiliateCookies; var cookieArray = []; affiliateCookies.forEach((item) => { let cookieObject = convertCookieStringToObject(item.CookieValue); cookieArray.push(cookieObject); }); const sortedCookies = cookieArray.sort( (a, b) => new Date(b.timestamp.replace("/", "T")) - new Date(a.timestamp.replace("/", "T")) ); const filteredCookie = sortedCookies?.find( (x) => x.tagEvent?.length > 0 && x.batchEvent?.length > 0 ); if (isDefined(filteredCookie)) { cjEvent = filteredCookie.tagEvent; const hasSubmittedOrder = baseMixin.methods.hasSubmittedOrder(); const submittedOrder = baseMixin.methods.getSubmittedOrder(); if (hasSubmittedOrder) { repairReplace = submittedOrder.damage.isRepair ? "Repair" : "Replace"; const promos = submittedOrder.lineItems.promos ?? []; if (promos.length === 0) { coupons = ""; } else { const promoCodes = promos.map((promo) => promo.promoCode); const promoString = promoCodes.reduce( (prev, next) => `${prev}_${next}` ); coupons = promoString; } const serviceZipPackage = submittedOrder.lineItems?.supportingItems?.find( (x) => x.partType == "SERVICE PACKAGE DISCOUNT" ); if (isDefined(serviceZipPackage)) { if (coupons) { coupons += ","; } coupons += serviceZipPackage.partNumber; } refSequenceNum = submittedOrder.referralSequenceNumber; if (isDefined(submittedOrder.payment.isInsurance)) { accountType = submittedOrder.payment.isInsurance ? "insurance" : "cash"; } else { accountType = ""; } const lineItems = submittedOrder.lineItems ?? {}; const combinedLineItems = [ ...(lineItems.glassParts ?? []), ...(lineItems.supportingItems ?? []), ...(lineItems.vaps ?? []), ...(lineItems.promos ?? []), ]; const isPricingAvailable = combinedLineItems.length > 0 && combinedLineItems.every( (lineItem) => isDefined(lineItem.kitPrice) && isDefined(lineItem.laborAmount) && isDefined(lineItem.sellingPrice) ); if (accountType == "cash" && isPricingAvailable) { amount = baseMixin.methods.getTotalPriceOfAllLineItemsAndChildParts( combinedLineItems, false ); } } } } pushToDataLayerIfDefined({ event: "commissionJunctionGtmData", commissionJunctionGtmData: { cj_commission_junction_event: cjEvent, cj_referral_sequence_number: refSequenceNum, cj_amount: amount.toFixed(2), cj_repair_replace: repairReplace, cj_coupon: coupons, }, }); }, pushPageErrorToDataLayer(error) { pushToDataLayerIfDefined({ event: "page-error", error: error, }); }, prependActionToMethod(object, method, actionToPrepend) { const baseMethodName = method.name.startsWith("bound ") ? method.name.substring(6) : method.name; const baseMethod = object[baseMethodName]; object[baseMethodName] = function () { actionToPrepend.apply(this, arguments); return baseMethod.apply(object, arguments); }; }, async initSession() { regenerateDeviceId(); regenerateUserId(); const userId = getUserIdValue(); // cookieNames.FUNNEL_USER_ID const deviceId = getDeviceIdValue(); // cookieNames.DXDEV const sessionId = getSessionIdValue(); // cookieNames.SESSION_ID const userAgent = navigator.userAgent; // navigator.userAgent const referrer = applicationConfig.CURRENT_ENVIRONMENT != "Localhost" ? document.referrer : null; // see above const payload = { userId: userId, deviceId: deviceId, sessionId: sessionId, userAgent: userAgent, referrer: referrer, }; const response = await baseMixin.methods.dispatchStoreAction( storeActions.INITIALIZE_SESSION, payload, false ); if (response?.data) { if (response.data.sessionKey) { setSessionKeyIfUnset(response.data.sessionKey); } if (response.data.sessionId) { setSessionIdIfUnset(response.data.sessionId); } } }, noSession() { return !areAllSessionCookiesSet(); }, // sessionExpired is true when one of the analytics cookies(sid, dxdev) has expired but we still have the vehicle year in vuex sessionExpired() { const fromHeritage = getQuerystringParameter(queryStrings.FROM_HERITAGE) === "true"; if (this.noSession() && !fromHeritage && store.getters.order.vehicle?.year > 0) { return true; } else { return false; } }, async validateSession() { // do not init session if it is expired. let the click event on nav-bar handle session expired logic if (this.sessionExpired()) { return; } if (this.noSession()) { await this.initSession(); } refreshSessionExpiration(); }, removeParamsFromEndpoint(endpoint) { const endpointWithoutParams = endpoint.split("?")[0]; const numSlashesBeforeParams = 6; let splitString = endpointWithoutParams.split("/"); if (splitString.length > numSlashesBeforeParams) { splitString = splitString.slice(0, numSlashesBeforeParams); return splitString.join("/"); } else { return endpointWithoutParams; } }, }, computed: { analyticsPageEvents() { return analyticsPageEvents; }, GaCategories() { return GaCategories; }, GaActions() { return GaActions; }, GaLabels() { return GaLabels; }, ValueToLogTypes() { return ValueToLogTypes; }, Variables() { return Variables; }, }, }; function pushToDataLayerIfDefined(data) { if (window.dataLayer !== undefined) { window.dataLayer.push(data); } } function getPageNameByQueryString() { const params = new URLSearchParams(location.search); if (params.has(queryStrings.FMG_PAGE)) { return params.get(queryStrings.FMG_PAGE); } else { return ""; } } function getValueToLog(value, valueToLogType) { if (valueToLogType != null && valueToLogType === ValueToLogTypes.LAST_5) { return value.slice(-5); } return value; } function flattenArray(arr) { let result = []; arr.forEach((item) => { result.push(item); if (item.childParts) { result = result.concat(item.childParts); delete item.childParts; // Remove childParts after flattening } }); return result; } function convertCookieStringToObject(cookieValue) { const cookieObject = cookieValue.split("&").reduce((acc, pair) => { const [key, value] = pair.split("="); acc[key] = value; return acc; }, {}); return cookieObject; }