From eaa031f1dbd045f0df8f4f572462f9627f99634e Mon Sep 17 00:00:00 2001 From: Bizz <56281168+dr-bizz@users.noreply.github.com> Date: Mon, 2 Oct 2023 15:02:08 -0400 Subject: [PATCH 1/3] updating tracking to match brandedCheckout tracking (#1054) * Updating Analytics tracking to match data broadcasted in brandedCheckout analytics tracking * Ensure cartData and other data are not undefined * Adding tests and fixing functions which were broken * updating dimensions names on transaction event * Adding testingTransaction data to items. * Persisting the testingTransaction value for each item on sessionStorage * Storing different sessionStorage for an item with different frequencys * After purchase complete, removing TestingTransaction sessionStorage items. --- package.json | 1 + src/app/analytics/analytics.factory.js | 1259 ++++++++--------- src/app/analytics/analytics.factory.spec.js | 637 ++++++++- .../productConfigForm.component.js | 4 +- 4 files changed, 1260 insertions(+), 641 deletions(-) diff --git a/package.json b/package.json index 430e5f851..0d101d858 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "build:analyze": "webpack -p --env.analyze", "test": "jest", "lint": "standard", + "lint:write": "standard --fix", "lint:ts": "tsc" }, "standard": { diff --git a/src/app/analytics/analytics.factory.js b/src/app/analytics/analytics.factory.js index 675753473..223cc9be5 100644 --- a/src/app/analytics/analytics.factory.js +++ b/src/app/analytics/analytics.factory.js @@ -5,18 +5,69 @@ import find from 'lodash/find' import sha3 from 'crypto-js/sha3' import merge from 'lodash/merge' import isEmpty from 'lodash/isEmpty' +import moment from 'moment' /* global localStorage */ +function suppressErrors (func) { + return function wrapper (...args) { + try { + return func.apply(this, args) + } catch (e) { } + } +} + +function testingTransactionName (item) { + const designationNumber = item.designationNumber + const frequencyObj = find(item.frequencies, { name: item.frequency }) + const frequency = frequencyObj?.display || item.frequency + if (designationNumber && frequency) { + return `isItemTestingTransaction_${designationNumber}_${frequency.toLowerCase()}` + } else { + return undefined + } +} + +// Generate a datalayer product object +const generateProduct = suppressErrors(function (item, additionalData = {}) { + const sessionStorageTestName = testingTransactionName(item) + const testingTransaction = sessionStorageTestName + ? window.sessionStorage.getItem(sessionStorageTestName) || undefined + : undefined + const price = additionalData?.price || item.amount + const category = additionalData?.category || item.designationType + const name = additionalData?.name || item.displayName || undefined + const recurringDate = additionalData.recurringDate + ? additionalData.recurringDate.format('MMMM D, YYYY') + : item.giftStartDate + ? moment(item.giftStartDate).format('MMMM D, YYYY') + : undefined + const frequencyObj = find(item.frequencies, { name: item.frequency }) + const variant = additionalData?.variant || frequencyObj?.display || item.frequency + + return { + item_id: item.designationNumber, + item_name: name, + item_brand: item.orgId, + item_category: category ? category.toLowerCase() : undefined, + item_variant: variant ? variant.toLowerCase() : undefined, + currency: 'USD', + price: price ? price.toString() : undefined, + quantity: '1', + recurring_date: recurringDate, + testing_transaction: testingTransaction + } +}) + const analyticsFactory = /* @ngInject */ function ($window, $timeout, envService, sessionService) { return { - checkoutFieldError: function (field, error) { + checkoutFieldError: suppressErrors((field, error) => { $window.dataLayer = $window.dataLayer || [] $window.dataLayer.push({ event: 'checkout_error', error_type: field, error_details: error }) - }, + }), // Send checkoutFieldError events for any invalid fields in a form handleCheckoutFormErrors: function (form) { @@ -35,237 +86,232 @@ const analyticsFactory = /* @ngInject */ function ($window, $timeout, envService }) }, - buildProductVar: function (cartData) { - try { - let item, donationType + buildProductVar: suppressErrors(function (cartData) { + if (!cartData) return + let donationType - // Instantiate cart data layer - const hash = sha3(cartData.id, { outputLength: 80 }) // limit hash to 20 characters + const { id } = cartData + // Instantiate cart data layer + const hash = id ? sha3(id, { outputLength: 80 }).toString() : null // limit hash to 20 characters + if ($window?.digitalData) { $window.digitalData.cart = { - id: cartData.id, - hash: cartData.id ? hash.toString() : null, + id: id, + hash: hash, item: [] } - - // Build cart data layer - $window.digitalData.cart.price = { - cartTotal: cartData && cartData.cartTotal + } else { + $window.digitalData = { + cart: { + id: id, + hash: hash, + item: [] + } } + } - if (cartData && cartData.items) { - for (let i = 0; i < cartData.items.length; i++) { - // Set donation type - if (cartData.items[i].frequency.toLowerCase() === 'single') { - donationType = 'one-time donation' - } else { - donationType = 'recurring donation' - } - - item = { - productInfo: { - productID: cartData.items[i].designationNumber, - designationType: cartData.items[i].designationType, - orgId: cartData.items[i].orgId ? cartData.items[i].orgId : 'cru' - }, - price: { - basePrice: cartData.items[i].amount - }, - attributes: { - donationType: donationType, - donationFrequency: cartData.items[i].frequency.toLowerCase(), - siebel: { - productType: 'designation', - campaignCode: cartData.items[i].config['campaign-code'] - } - } - } + // Build cart data layer + $window.digitalData.cart.price = { + cartTotal: cartData?.cartTotal + } - $window.digitalData.cart.item.push(item) + if (cartData.items?.length) { + cartData.items.forEach((item) => { + const frequency = item?.frequency ? item.frequency.toLowerCase() : undefined + // Set donation type + if (frequency === 'single') { + donationType = 'one-time donation' + } else { + donationType = 'recurring donation' } - } - } catch (e) { - // Error caught in analyticsFactory.buildProductVar - } - }, - cartAdd: function (itemConfig, productData) { - try { - let siteSubSection - const cart = { - item: [{ + + item = { productInfo: { - productID: productData.designationNumber, - designationType: productData.designationType + productID: item.designationNumber, + designationType: item.designationType, + orgId: item.orgId ? item.orgId : 'cru' }, price: { - basePrice: itemConfig.amount + basePrice: item.amount }, attributes: { + donationType: donationType, + donationFrequency: frequency, siebel: { - productType: 'designation' + productType: 'designation', + campaignCode: item.config['campaign-code'] } } - }] - } + } - // Set site sub-section - if (typeof $window.digitalData.page !== 'undefined') { - if (typeof $window.digitalData.page.category !== 'undefined') { - $window.digitalData.page.category.subCategory1 = siteSubSection - } else { - $window.digitalData.page.category = { - subCategory1: siteSubSection + $window.digitalData.cart.item.push(item) + }) + } + }), + saveTestingTransaction: suppressErrors(function (item, testingTransaction) { + if (testingTransaction) { + $window.sessionStorage.setItem(testingTransactionName(item), testingTransaction) + } + }), + cartAdd: suppressErrors(function (itemConfig, productData) { + let siteSubSection + const cart = { + item: [{ + productInfo: { + productID: productData.designationNumber, + designationType: productData.designationType + }, + price: { + basePrice: itemConfig.amount + }, + attributes: { + siebel: { + productType: 'designation' } } + }] + } + + // Set site sub-section + if ($window?.digitalData?.page) { + if ($window.digitalData.page?.category) { + $window.digitalData.page.category.subCategory1 = siteSubSection } else { + $window.digitalData.page.category = { + subCategory1: siteSubSection + } + } + } else { + if ($window?.digitalData) { $window.digitalData.page = { category: { subcategory1: siteSubSection } } - } - - // Set donation type - if (productData.frequency === 'NA') { - cart.item[0].attributes.donationType = 'one-time donation' } else { - cart.item[0].attributes.donationType = 'recurring donation' - } - - // Set donation frequency - const frequencyObj = find(productData.frequencies, { name: productData.frequency }) - cart.item[0].attributes.donationFrequency = frequencyObj && frequencyObj.display.toLowerCase() - - // Set data layer - $window.digitalData.cart = cart - // Send GTM Advance Ecommerce event - if (typeof $window.dataLayer !== 'undefined') { - $window.dataLayer.push({ - event: 'add-to-cart', - ecommerce: { - currencyCode: 'USD', - add: { - products: [{ - name: productData.designationNumber, - id: productData.designationNumber, - price: itemConfig.amount.toString(), - brand: productData.orgId, - category: productData.designationType.toLowerCase(), - variant: frequencyObj.display.toLowerCase(), - quantity: '1' - }] + $window.digitalData = { + page: { + category: { + subcategory1: siteSubSection } } - }) + } } - } catch (e) { - // Error caught in analyticsFactory.cartAdd } - }, - cartRemove: function (item) { - try { - if (item) { - $window.digitalData.cart.item = [{ - productInfo: { - productID: item.designationNumber, - designationType: item.designationType - }, - price: { - basePrice: item.amount - }, - attributes: { - donationType: item.frequency.toLowerCase() === 'single' ? 'one-time donation' : 'recurring donation', - donationFrequency: item.frequency.toLowerCase(), - siebel: { - productType: 'designation', - campaignCode: item.config['campaign-code'] - } - } - }] - // Send GTM Advance Ecommerce event - if (typeof $window.dataLayer !== 'undefined') { - $window.dataLayer.push({ - event: 'remove-from-cart', - ecommerce: { - currencyCode: 'USD', - remove: { - products: [{ - name: item.designationNumber, - id: item.designationNumber, - price: item.amount.toString(), - brand: item.orgId, - category: item.designationType.toLowerCase(), - variant: item.frequency.toLowerCase(), - quantity: '1' - }] - } - } - }) + + let recurringDate = null + // Set donation type + if (productData.frequency === 'NA') { + cart.item[0].attributes.donationType = 'one-time donation' + } else { + cart.item[0].attributes.donationType = 'recurring donation' + recurringDate = moment(`${moment().year()}-${itemConfig['recurring-start-month']}-${itemConfig['recurring-day-of-month']} ${moment().format('h:mm:ss a')}`) + } + + // Set donation frequency + const frequencyObj = find(productData.frequencies, { name: productData.frequency }) + cart.item[0].attributes.donationFrequency = frequencyObj && frequencyObj.display.toLowerCase() + + // Set data layer + $window.digitalData.cart = cart + // Send GTM Advance Ecommerce event + $window.dataLayer = $window.dataLayer || [] + $window.dataLayer.push({ + event: 'add-to-cart', + ecommerce: { + currencyCode: 'USD', + add: { + products: [generateProduct(productData, { + price: itemConfig.amount, + recurringDate + })] } } - } catch (e) { - // Error caught in analyticsFactory.cartRemove - } - }, - cartView: function (isMiniCart = false) { - try { - // Send GTM Advance Ecommerce event - if (typeof $window.dataLayer !== 'undefined') { - $window.dataLayer.push({ - event: isMiniCart ? 'view-mini-cart' : 'view-cart' - }) + }) + }), + cartRemove: suppressErrors(function (item) { + if (!item) return + const frequency = item.frequency ? item.frequency.toLowerCase() : undefined + $window.digitalData.cart.item = [{ + productInfo: { + productID: item.designationNumber, + designationType: item.designationType + }, + price: { + basePrice: item.amount + }, + attributes: { + donationType: frequency === 'single' ? 'one-time donation' : 'recurring donation', + donationFrequency: frequency, + siebel: { + productType: 'designation', + campaignCode: item.config['campaign-code'] + } } - } catch (e) { - // Error caught in analyticsFactory.cartView - } - }, - checkoutStepEvent: function (step, cart) { + }] + // Send GTM Advance Ecommerce event + $window.dataLayer = $window.dataLayer || [] + $window.dataLayer.push({ + event: 'remove-from-cart', + ecommerce: { + currencyCode: 'USD', + remove: { + products: [generateProduct(item)] + } + } + }) + }), + cartView: suppressErrors(function (isMiniCart = false) { + // Send GTM Advance Ecommerce event + $window.dataLayer = $window.dataLayer || [] + $window.dataLayer.push({ + event: isMiniCart ? 'view-mini-cart' : 'view-cart' + }) + }), + checkoutStepEvent: suppressErrors(function (step, cart) { + $window.dataLayer = $window.dataLayer || [] + const cartObject = cart.items.map((cartItem) => generateProduct(cartItem)) let stepNumber switch (step) { case 'contact': stepNumber = 1 + $window.dataLayer.push({ + event: 'begin_checkout', + ecommerce: { + items: cartObject + } + }) break case 'payment': stepNumber = 2 + $window.dataLayer.push({ + event: 'add_payment_info' + }) break case 'review': stepNumber = 3 + $window.dataLayer.push({ + event: 'review_order' + }) break } - const cartObject = cart.items.map((cartItem) => { - return { - name: cartItem.designationNumber, - id: cartItem.designationNumber, - price: cartItem.amount.toString(), - brand: cartItem.orgId, - category: cartItem.designationType.toLowerCase(), - variant: cartItem.frequency.toLowerCase(), - quantity: '1' + $window.dataLayer.push({ + event: 'checkout-step', + cartId: cart.id, + ecommerce: { + currencyCode: 'USD', + checkout: { + actionField: { + step: stepNumber, + option: '' + }, + products: [ + ...cartObject + ] + } } }) - try { - if (typeof $window.dataLayer !== 'undefined') { - $window.dataLayer.push({ - event: 'checkout-step', - cartId: cart.id, - ecommerce: { - currencyCode: 'USD', - checkout: { - actionField: { - step: stepNumber, - option: '' - }, - products: [ - ...cartObject - ] - } - } - }) - } - } catch (e) { - // Error caught in analyticsFactory.checkoutStepEvent - } - }, - checkoutStepOptionEvent: function (option, step) { + }), + checkoutStepOptionEvent: suppressErrors(function (option, step) { let stepNumber switch (step) { case 'contact': @@ -278,540 +324,481 @@ const analyticsFactory = /* @ngInject */ function ($window, $timeout, envService stepNumber = 3 break } - try { - $window.dataLayer.push({ - event: 'checkout-option', - ecommerce: { - checkout_option: { - actionField: { - step: stepNumber, - option: option.toLowerCase() - } + $window.dataLayer = $window.dataLayer || [] + $window.dataLayer.push({ + event: 'checkout-option', + ecommerce: { + checkout_option: { + actionField: { + step: stepNumber, + option: option ? option.toLowerCase() : undefined } } - }) - } catch (e) { - // Error caught in analyticsFactory.checkoutStepOptionEvent - } - }, - editRecurringDonation: function (giftData) { - try { - let frequency = '' + } + }) + }), + editRecurringDonation: suppressErrors(function (giftData) { + let frequency = '' - if (giftData && giftData.length) { - if (get(giftData, '[0].gift["updated-rate"].recurrence.interval')) { - frequency = giftData[0].gift['updated-rate'].recurrence.interval.toLowerCase() - } else { - const interval = get(giftData, '[0].parentDonation.rate.recurrence.interval') - frequency = interval && interval.toLowerCase() - } + if (giftData?.length) { + if (get(giftData, '[0].gift["updated-rate"].recurrence.interval')) { + frequency = giftData[0].gift['updated-rate'].recurrence.interval.toLowerCase() + } else { + const interval = get(giftData, '[0].parentDonation.rate.recurrence.interval') + frequency = interval && interval.toLowerCase() + } - if (typeof $window.digitalData !== 'undefined') { - if (typeof $window.digitalData.recurringGift !== 'undefined') { - $window.digitalData.recurringGift.originalFrequency = frequency - } else { - $window.digitalData.recurringGift = { - originalFrequency: frequency - } - } + if ($window?.digitalData) { + if ($window.digitalData?.recurringGift) { + $window.digitalData.recurringGift.originalFrequency = frequency } else { - $window.digitalData = { - recurringGift: { - originalFrequency: frequency - } + $window.digitalData.recurringGift = { + originalFrequency: frequency } } - } - - this.pageLoaded() - } catch (e) { - // Error caught in analyticsFactory.editRecurringDonation - } - }, - getPath: function () { - try { - let pagename = '' - const delim = ' : ' - let path = $window.location.pathname - - if (path !== '/') { - const extension = ['.html', '.htm'] - - for (let i = 0; i < extension.length; i++) { - if (path.indexOf(extension[i]) > -1) { - path = path.split(extension[i]) - path = path.splice(0, 1) - path = path.toString() - - break + } else { + $window.digitalData = { + recurringGift: { + originalFrequency: frequency } } + } + } - path = path.split('/') + this.pageLoaded() + }), + getPath: suppressErrors(function () { + let pagename = '' + const delim = ' : ' + let path = $window.location.pathname - if (path[0].length === 0) { - path.shift() - } + if (path !== '/') { + const extension = ['.html', '.htm'] - // Capitalize first letter of each page - for (let i = 0; i < path.length; i++) { - path[i] = path[i].charAt(0).toUpperCase() + path[i].slice(1) - } + for (let i = 0; i < extension.length; i++) { + if (path.indexOf(extension[i]) > -1) { + path = path.split(extension[i]) + path = path.splice(0, 1) + path = path.toString() - // Set pageName - pagename = 'Give' + delim + path.join(delim) - } else { - // Set pageName - pagename = 'Give' + delim + 'Home' + break + } } - this.setPageNameObj(pagename) + path = path.split('/') - return path - } catch (e) { - // Error caught in analyticsFactory.getPath - } - }, - getSetProductCategory: function (path) { - try { - const allElements = $window.document.getElementsByTagName('*') - - for (let i = 0, n = allElements.length; i < n; i++) { - const desigType = allElements[i].getAttribute('designationtype') - - if (desigType !== null) { - const productConfig = $window.document.getElementsByTagName('product-config') - $window.digitalData.product = [{ - productInfo: { - productID: productConfig.length ? productConfig[0].getAttribute('product-code') : null - }, - category: { - primaryCategory: 'donation ' + desigType.toLowerCase(), - siebelProductType: 'designation', - organizationId: path[0] - } - }] + if (path[0].length === 0) { + path.shift() + } - return path[0] - } + // Capitalize first letter of each page + for (let i = 0; i < path.length; i++) { + path[i] = path[i].charAt(0).toUpperCase() + path[i].slice(1) } - return false - } catch (e) { - // Error caught in analyticsFactory.getSetProductCategory + // Set pageName + pagename = 'Give' + delim + path.join(delim) + } else { + // Set pageName + pagename = 'Give' + delim + 'Home' } - }, - giveGiftModal: function (productData) { - try { - const product = [{ - productInfo: { - productID: productData.designationNumber - }, - attributes: { - siebel: { - producttype: 'designation' - } - } - }] - $window.digitalData.product = product - $window.dataLayer.push({ - event: 'give-gift-modal', - ecommerce: { - currencyCode: 'USD', - detail: { - products: [{ - name: productData.designationNumber, - id: productData.designationNumber, - price: undefined, - brand: productData.orgId, - category: productData.designationType.toLowerCase(), - variant: undefined, - quantity: '1' - }] + this.setPageNameObj(pagename) + + return path + }), + getSetProductCategory: suppressErrors(function (path) { + const allElements = $window.document.getElementsByTagName('*') + + for (let i = 0, n = allElements.length; i < n; i++) { + const desigType = allElements[i].getAttribute('designationtype') + + if (desigType !== null) { + const productConfig = $window.document.getElementsByTagName('product-config') + $window.digitalData.product = [{ + productInfo: { + productID: productConfig.length ? productConfig[0].getAttribute('product-code') : null + }, + category: { + primaryCategory: 'donation ' + desigType ? desigType.toLowerCase() : '', + siebelProductType: 'designation', + organizationId: path[0] } - } - }) - this.setEvent('give gift modal') - this.pageLoaded() - } catch (e) { - // Error caught in analyticsFactory.giveGiftModal + }] + + return path[0] + } } - }, - pageLoaded: function (skipImageRequests) { - try { - const path = this.getPath() - this.getSetProductCategory(path) - this.setSiteSections(path) - this.setLoggedInStatus() - - if (typeof $window.digitalData.page.attributes !== 'undefined') { - if ($window.digitalData.page.attributes.angularLoaded === 'true') { - $window.digitalData.page.attributes.angularLoaded = 'false' - } else { - $window.digitalData.page.attributes.angularLoaded = 'true' + + return false + }), + giveGiftModal: suppressErrors(function (productData) { + const product = [{ + productInfo: { + productID: productData.designationNumber + }, + attributes: { + siebel: { + producttype: 'designation' } - } else { - $window.digitalData.page.attributes = { - angularLoaded: 'true' + } + }] + const modifiedProductData = { ...productData } + modifiedProductData.frequency = undefined + $window.digitalData.product = product + $window.dataLayer = $window.dataLayer || [] + $window.dataLayer.push({ + event: 'give-gift-modal', + ecommerce: { + currencyCode: 'USD', + detail: { + products: [generateProduct(modifiedProductData)] } } - - if (!skipImageRequests) { - // Allow time for data layer changes to be consumed & fire image request - $timeout(function () { - try { - $window.s.t() - $window.s.clearVars() - } catch (e) { - // Error caught in analyticsFactory.pageLoaded while trying to fire analytics image request or clearVars - } - }, 1000) + }) + this.setEvent('give gift modal') + this.pageLoaded() + }), + pageLoaded: suppressErrors(function (skipImageRequests) { + const path = this.getPath() + this.getSetProductCategory(path) + this.setSiteSections(path) + this.setLoggedInStatus() + + if (typeof $window.digitalData.page.attributes !== 'undefined') { + if ($window.digitalData.page.attributes.angularLoaded === 'true') { + $window.digitalData.page.attributes.angularLoaded = 'false' + } else { + $window.digitalData.page.attributes.angularLoaded = 'true' + } + } else { + $window.digitalData.page.attributes = { + angularLoaded: 'true' } - } catch (e) { - // Error caught in analyticsFactory.pageLoaded } - }, - pageReadyForOptimize: function () { - if (typeof $window.dataLayer !== 'undefined') { - let found = false - angular.forEach($window.dataLayer, (value) => { - if (value.event && value.event === 'angular.loaded') { - found = true + + if (!skipImageRequests) { + // Allow time for data layer changes to be consumed & fire image request + $timeout(function () { + try { + $window.s.t() + $window.s.clearVars() + } catch (e) { + // Error caught in analyticsFactory.pageLoaded while trying to fire analytics image request or clearVars } - }) - if (!found) { - $window.dataLayer.push({ event: 'angular.loaded' }) - } + }, 1000) } - }, - productViewDetailsEvent: function (product) { - try { - if (typeof $window.dataLayer !== 'undefined') { - $window.dataLayer.push({ - event: 'product-detail-click', - ecommerce: { - currencyCode: 'USD', - click: { - actionField: { - list: 'search results' - }, - products: [ - { - name: product.designationNumber, - id: product.designationNumber, - price: undefined, - brand: product.orgId, - category: product.type, - variant: undefined, - position: undefined - } - ] - } - } - }) + }), + pageReadyForOptimize: suppressErrors(function () { + $window.dataLayer = $window.dataLayer || [] + let found = false + angular.forEach($window.dataLayer, (value) => { + if (value.event && value.event === 'angular.loaded') { + found = true } - } catch (e) { - // Error caught in analyticsFactory.productViewDetailsEvent - } - }, - purchase: function (donorDetails, cartData, coverFeeDecision) { - try { - // Build cart data layer - this.setDonorDetails(donorDetails) - this.buildProductVar(cartData) - // Stringify the cartObject and store in localStorage for the transactionEvent - localStorage.setItem('transactionCart', JSON.stringify(cartData)) - // Store value of coverFeeDecision in sessionStorage for the transactionEvent - sessionStorage.setItem('coverFeeDecision', coverFeeDecision) - } catch (e) { - // Error caught in analyticsFactory.purchase + }) + if (!found) { + $window.dataLayer.push({ event: 'angular.loaded' }) } - }, - setPurchaseNumber: function (purchaseNumber) { - try { + }), + productViewDetailsEvent: suppressErrors(function (product) { + $window.dataLayer = $window.dataLayer || [] + $window.dataLayer.push({ + event: 'product-detail-click', + ecommerce: { + currencyCode: 'USD', + click: { + actionField: { + list: 'search results' + }, + products: [ + generateProduct(product, { + category: product.type, + name: product.name + }) + ] + } + } + }) + }), + purchase: suppressErrors(function (donorDetails, cartData, coverFeeDecision) { + // Build cart data layer + this.setDonorDetails(donorDetails) + this.buildProductVar(cartData) + // Stringify the cartObject and store in localStorage for the transactionEvent + localStorage.setItem('transactionCart', JSON.stringify(cartData)) + // Store value of coverFeeDecision in sessionStorage for the transactionEvent + sessionStorage.setItem('coverFeeDecision', coverFeeDecision) + }), + setPurchaseNumber: suppressErrors(function (purchaseNumber) { + if ($window?.digitalData) { $window.digitalData.purchaseNumber = purchaseNumber - } catch (e) { - // Error caught in analyticsFactory.setPurchaseNumber + } else { + $window.digitalData = { + purchaseNumber + } } - }, - transactionEvent: function (purchaseData) { - try { - // The value of whether or not user is covering credit card fees for the transaction - const coverFeeDecision = JSON.parse(sessionStorage.getItem('coverFeeDecision')) - // Parse the cart object of the last purchase - const transactionCart = JSON.parse(localStorage.getItem('transactionCart')) - // The purchaseId number from the last purchase - const lastTransactionId = sessionStorage.getItem('transactionId') - // The purchaseId number from the pruchase data being passed in - const currentTransactionId = purchaseData && purchaseData.rawData['purchase-number'] - let purchaseTotal = 0 - // If the lastTransactionId and the current one do not match, we need to send an analytics event for the transaction - if (purchaseData && lastTransactionId !== currentTransactionId) { - // Set the transactionId in localStorage to be the one that is passed in - sessionStorage.setItem('transactionId', currentTransactionId) - const cartObject = transactionCart.items.map((cartItem) => { - purchaseTotal += cartItem.amount - return { - name: cartItem.designationNumber, - id: cartItem.designationNumber, - price: cartItem.amount.toString(), - processingFee: cartItem.amountWithFees && coverFeeDecision ? (cartItem.amountWithFees - cartItem.amount).toFixed(2) : undefined, - brand: cartItem.orgId, - category: cartItem.designationType.toLowerCase(), - variant: cartItem.frequency.toLowerCase(), - quantity: '1', - dimension1: localStorage.getItem('gaDonorType'), - dimension3: cartItem.frequency.toLowerCase() === 'single' ? 'one-time' : 'recurring', - dimension4: cartItem.frequency.toLowerCase(), - dimension6: purchaseData.paymentMeans['account-type'] ? 'bank account' : 'credit card', - dimension7: purchaseData.rawData['purchase-number'], - dimension8: 'designation', - dimension9: cartItem.config['campaign-code'] !== '' ? cartItem.config['campaign-code'] : undefined - } - }) - // Send the transaction event if the dataLayer is defined - if (typeof $window.dataLayer !== 'undefined') { - $window.dataLayer.push({ - event: 'transaction', - paymentType: purchaseData.paymentMeans['account-type'] ? 'bank account' : 'credit card', - ecommerce: { - currencyCode: 'USD', - purchase: { - actionField: { - id: purchaseData.rawData['purchase-number'], - affiliation: undefined, - revenue: purchaseTotal.toString(), - shipping: undefined, - tax: undefined, - coupon: undefined - }, - products: [ - ...cartObject - ] - } - } - }) - // Send cover fees event if value is true - if (coverFeeDecision) { - $window.dataLayer.push({ - event: 'ga-cover-fees-checkbox' - }) - } + }), + transactionEvent: suppressErrors(function (purchaseData) { + // The value of whether or not user is covering credit card fees for the transaction + const coverFeeDecision = JSON.parse(sessionStorage.getItem('coverFeeDecision')) + // Parse the cart object of the last purchase + const transactionCart = JSON.parse(localStorage.getItem('transactionCart')) + // The purchaseId number from the last purchase + const lastTransactionId = sessionStorage.getItem('transactionId') + // The purchaseId number from the pruchase data being passed in + const currentTransactionId = purchaseData && purchaseData.rawData['purchase-number'] + let purchaseTotal = 0 + let purchaseTotalWithFees = 0 + // If the lastTransactionId and the current one do not match, we need to send an analytics event for the transaction + if (purchaseData && lastTransactionId !== currentTransactionId) { + // Set the transactionId in localStorage to be the one that is passed in + + sessionStorage.setItem('transactionId', currentTransactionId) + const cartObject = transactionCart.items.map((cartItem) => { + const { amount, amountWithFees } = cartItem + purchaseTotal += amount + purchaseTotalWithFees += amountWithFees || 0 + const frequency = cartItem?.frequency ? cartItem.frequency.toLowerCase() : undefined + return { + ...generateProduct(cartItem), + processingFee: amountWithFees && coverFeeDecision ? (amountWithFees - amount).toFixed(2) : undefined, + ga_donator_type: localStorage.getItem('gaDonorType'), + donation_type: frequency === 'single' ? 'one-time' : 'recurring', + donation_frequency: frequency, + payment_type: purchaseData.paymentMeans['account-type'] ? 'bank account' : 'credit card', + purchase_number: purchaseData.rawData['purchase-number'], + campaign_code: cartItem.config['campaign-code'] !== '' ? cartItem.config['campaign-code'] : undefined, + designation: 'designation' + } + }) + // Send the transaction event if the dataLayer is defined + $window.dataLayer = $window.dataLayer || [] + $window.dataLayer.push({ + event: 'purchase', + paymentType: purchaseData.paymentMeans['account-type'] ? 'bank account' : 'credit card', + ecommerce: { + currency: 'USD', + payment_type: purchaseData.paymentMeans['account-type'] ? 'bank account' : 'credit card', + donator_type: purchaseData.donorDetails['donor-type'], + pays_processing: purchaseTotalWithFees && coverFeeDecision ? 'yes' : 'no', + value: purchaseTotalWithFees && coverFeeDecision ? purchaseTotalWithFees.toFixed(2).toString() : purchaseTotal.toFixed(2).toString(), + processing_fee: purchaseTotalWithFees && coverFeeDecision ? (purchaseTotalWithFees - purchaseTotal).toFixed(2) : undefined, + transaction_id: purchaseData.rawData['purchase-number'], + items: [ + ...cartObject + ] } + }) + // Send cover fees event if value is true + if (coverFeeDecision) { + $window.dataLayer.push({ + event: 'ga-cover-fees-checkbox' + }) } - // Remove the transactionCart from localStorage since it is no longer needed - localStorage.removeItem('transactionCart') - // Remove the coverFeeDecision from sessionStorage since it is no longer needed - sessionStorage.removeItem('coverFeeDecision') - } catch (e) { - // Error in analyticsFactory.transactionEvent } - }, - search: function (params, results) { - try { - if (typeof params !== 'undefined') { - if (typeof $window.digitalData.page !== 'undefined') { - if (typeof $window.digitalData.page.pageInfo !== 'undefined') { - $window.digitalData.page.pageInfo.onsiteSearchTerm = params.keyword - $window.digitalData.page.pageInfo.onsiteSearchFilter = params.type - } else { - $window.digitalData.page.pageInfo = { - onsiteSearchTerm: params.keyword, - onsiteSearchFilter: params.type - } - } + // Remove the transactionCart from localStorage since it is no longer needed + localStorage.removeItem('transactionCart') + // Remove the coverFeeDecision from sessionStorage since it is no longer needed + sessionStorage.removeItem('coverFeeDecision') + // Remove testingTransaction from sessionStorage for each item if any since it is no longer needed + transactionCart.items.forEach((item) => { + $window.sessionStorage.removeItem(testingTransactionName(item)) + }) + }), + search: suppressErrors(function (params, results) { + if (params) { + if ($window?.digitalData?.page) { + if ($window.digitalData.page?.pageInfo) { + $window.digitalData.page.pageInfo.onsiteSearchTerm = params.keyword + $window.digitalData.page.pageInfo.onsiteSearchFilter = params.type } else { - $window.digitalData.page = { - pageInfo: { - onsiteSearchTerm: params.keyword, - onsiteSearchFilter: params.type - } + $window.digitalData.page.pageInfo = { + onsiteSearchTerm: params.keyword, + onsiteSearchFilter: params.type + } + } + } else { + $window.digitalData.page = { + pageInfo: { + onsiteSearchTerm: params.keyword, + onsiteSearchFilter: params.type } } } + } - if (typeof results !== 'undefined' && results.length > 0) { - if (typeof $window.digitalData.page !== 'undefined') { - if (typeof $window.digitalData.page.pageInfo !== 'undefined') { - $window.digitalData.page.pageInfo.onsiteSearchResults = results.length - } else { - $window.digitalData.page.pageInfo = { - onsiteSearchResults: results.length - } - } + if (results?.length) { + if ($window?.digitalData?.page) { + if ($window?.digitalData?.page?.pageInfo) { + $window.digitalData.page.pageInfo.onsiteSearchResults = results.length } else { - $window.digitalData.page = { - pageInfo: { - onsiteSearchResults: results.length - } + $window.digitalData.page.pageInfo = { + onsiteSearchResults: results.length } } } else { - $window.digitalData.page.pageInfo.onsiteSearchResults = 0 + $window.digitalData.page = { + pageInfo: { + onsiteSearchResults: results.length + } + } } - } catch (e) { - // Error caught in analyticsFactory.search + } else { + $window.digitalData.page.pageInfo.onsiteSearchResults = 0 } - }, - setLoggedInStatus: function () { - try { - const profileInfo = {} - if (typeof sessionService !== 'undefined') { - let ssoGuid - if (typeof sessionService.session.sso_guid !== 'undefined') { - ssoGuid = sessionService.session.sso_guid - } else if (typeof sessionService.session.sub !== 'undefined') { - ssoGuid = sessionService.session.sub.split('|').pop() - } - if (typeof ssoGuid !== 'undefined' && ssoGuid !== 'cas') { - profileInfo.ssoGuid = ssoGuid - } - - if (typeof sessionService.session.gr_master_person_id !== 'undefined') { - profileInfo.grMasterPersonId = sessionService.session.gr_master_person_id - } + }), + setLoggedInStatus: suppressErrors(function () { + const profileInfo = {} + if (sessionService) { + let ssoGuid + if (sessionService?.session?.sso_guid) { + ssoGuid = sessionService.session.sso_guid + } else if (sessionService?.session?.sub) { + ssoGuid = sessionService.session.sub.split('|').pop() } - - if (isEmpty(profileInfo)) { - return + if (ssoGuid && ssoGuid !== 'cas') { + profileInfo.ssoGuid = ssoGuid } - // Use lodash merge to deep merge with existing data or new empty hash - $window.digitalData = merge($window.digitalData || {}, { - user: [{ profile: [{ profileInfo: profileInfo }] }] - }) - } catch (e) { - // Error caught in analyticsFactory.setLoggedInStatus + if (sessionService?.session?.gr_master_person_id) { + profileInfo.grMasterPersonId = sessionService.session.gr_master_person_id + } } - }, - setDonorDetails: function (donorDetails) { - try { - const profileInfo = {} - if (typeof sessionService !== 'undefined') { - if (typeof sessionService.session.sso_guid !== 'undefined') { - profileInfo.ssoGuid = sessionService.session.sso_guid - } else if (typeof sessionService.session.sub !== 'undefined') { - profileInfo.ssoGuid = sessionService.session.sub.split('|').pop() - } - if (typeof sessionService.session.gr_master_person_id !== 'undefined') { - profileInfo.grMasterPersonId = sessionService.session.gr_master_person_id - } + if (isEmpty(profileInfo)) { + return + } - if (donorDetails) { - profileInfo.donorType = donorDetails['donor-type'].toLowerCase() - profileInfo.donorAcct = donorDetails['donor-number'].toLowerCase() - } + // Use lodash merge to deep merge with existing data or new empty hash + $window.digitalData = merge($window.digitalData || {}, { + user: [{ profile: [{ profileInfo: profileInfo }] }] + }) + }), + setDonorDetails: suppressErrors(function (donorDetails) { + const profileInfo = {} + if (sessionService) { + if (sessionService?.session?.sso_guid) { + profileInfo.ssoGuid = sessionService.session.sso_guid + } else if (sessionService?.session?.sub) { + profileInfo.ssoGuid = sessionService.session.sub.split('|').pop() } - // Use lodash merge to deep merge with existing data or new empty hash - $window.digitalData = merge($window.digitalData || {}, { - user: [{ profile: [{ profileInfo: profileInfo }] }] - }) + if (sessionService?.session?.gr_master_person_id) { + profileInfo.grMasterPersonId = sessionService.session.gr_master_person_id + } - // Store data for use on following page load - localStorage.setItem('gaDonorType', $window.digitalData.user[0].profile[0].profileInfo.donorType) - localStorage.setItem('gaDonorAcct', $window.digitalData.user[0].profile[0].profileInfo.donorAcct) - } catch (e) { - // Error caught in analyticsFactory.setDonorDetails + if (donorDetails) { + profileInfo.donorType = donorDetails['donor-type'] ? donorDetails['donor-type'].toLowerCase() : undefined + profileInfo.donorAcct = donorDetails['donor-number'] ? donorDetails['donor-number'].toLowerCase() : undefined + } } - }, - setEvent: function (eventName) { - try { - const evt = { - eventInfo: { - eventName: eventName - } + + // Use lodash merge to deep merge with existing data or new empty hash + $window.digitalData = merge($window.digitalData || {}, { + user: [{ profile: [{ profileInfo: profileInfo }] }] + }) + + // Store data for use on following page load + localStorage.setItem('gaDonorType', $window.digitalData.user[0].profile[0].profileInfo.donorType) + localStorage.setItem('gaDonorAcct', $window.digitalData.user[0].profile[0].profileInfo.donorAcct) + }), + setEvent: suppressErrors(function (eventName) { + const evt = { + eventInfo: { + eventName: eventName } + } + if ($window?.digitalData) { $window.digitalData.event = [] - $window.digitalData.event.push(evt) - } catch (e) { - // Error caught in analyticsFactory.setEvent + } else { + $window.digitalData = { + event: [] + } } - }, - setPageNameObj: function (pageName) { - try { - if (typeof $window.digitalData.page !== 'undefined') { - if (typeof $window.digitalData.page.pageInfo !== 'undefined') { - $window.digitalData.page.pageInfo.pageName = pageName - } else { - $window.digitalData.page.pageInfo = { - pageName: pageName - } - } + $window.digitalData.event.push(evt) + }), + setPageNameObj: suppressErrors(function (pageName) { + if ($window?.digitalData?.page) { + if ($window.digitalData.page?.pageInfo) { + $window.digitalData.page.pageInfo.pageName = pageName } else { + $window.digitalData.page.pageInfo = { + pageName: pageName + } + } + } else { + if ($window?.digitalData) { $window.digitalData.page = { pageInfo: { pageName: pageName } } + } else { + $window.digitalData = { + page: { + pageInfo: { + pageName: pageName + } + } + } } - } catch (e) { - // Error caught in analyticsFactory.setPageNameObj } - }, - setSiteSections: function (path) { - try { - const primaryCat = 'give' + }), + setSiteSections: suppressErrors(function (path) { + const primaryCat = 'give' - if (!path) { - path = this.getPath() - } + if (!path) { + path = this.getPath() + } - if (typeof $window.digitalData !== 'undefined') { - if (typeof $window.digitalData.page !== 'undefined') { - $window.digitalData.page.category = { + if ($window?.digitalData) { + if ($window.digitalData?.page) { + $window.digitalData.page.category = { + primaryCategory: primaryCat + } + } else { + $window.digitalData.page = { + category: { primaryCategory: primaryCat } - } else { - $window.digitalData.page = { - category: { - primaryCategory: primaryCat - } - } } - } else { - $window.digitalData = { - page: { - category: { - primaryCategory: primaryCat - } + } + } else { + $window.digitalData = { + page: { + category: { + primaryCategory: primaryCat } } } + } - if (path.length >= 1) { - // Check if product page - if (/^\d+$/.test(path[0])) { - this.getSetProductCategory(path) - $window.digitalData.page.category.subCategory1 = 'designation detail' - } else { - $window.digitalData.page.category.subCategory1 = path[0] === '/' ? '' : path[0] - } + if (path?.length) { + // Check if product page + if (/^\d+$/.test(path[0])) { + this.getSetProductCategory(path) + $window.digitalData.page.category.subCategory1 = 'designation detail' + } else { + $window.digitalData.page.category.subCategory1 = path[0] === '/' ? '' : path[0] + } - if (path.length >= 2) { - $window.digitalData.page.category.subCategory2 = path[1] + if (path.length >= 2) { + $window.digitalData.page.category.subCategory2 = path[1] - if (path.length >= 3) { - $window.digitalData.page.category.subCategory3 = path[2] - } + if (path.length >= 3) { + $window.digitalData.page.category.subCategory3 = path[2] } } - } catch (e) { - // Error caught in analyticsFactory.setSiteSections } - }, - track: function (eventName) { - try { - $window.dataLayer.push({ - event: eventName - }) - } catch (e) { - // Error caught in analyticsFactory.track - } - } + }), + track: suppressErrors(function (eventName) { + $window.dataLayer = $window.dataLayer || [] + $window.dataLayer.push({ + event: eventName + }) + }) } } diff --git a/src/app/analytics/analytics.factory.spec.js b/src/app/analytics/analytics.factory.spec.js index a7ed57efa..4acafce5b 100644 --- a/src/app/analytics/analytics.factory.spec.js +++ b/src/app/analytics/analytics.factory.spec.js @@ -3,7 +3,7 @@ import 'angular-mocks' import module from './analytics.factory' -describe('branded analytics factory', () => { +describe('analytics factory', () => { beforeEach(angular.mock.module(module.name, 'environment')) const self = {} @@ -12,6 +12,9 @@ describe('branded analytics factory', () => { self.envService = envService self.$window = $window self.$window.dataLayer = [] + + self.$window.sessionStorage.clear() + self.$window.localStorage.clear() })) describe('handleCheckoutFormErrors', () => { @@ -47,7 +50,7 @@ describe('branded analytics factory', () => { ['middleName', 'capitalized'], ['middleName', 'maxLength'] ]) - }) + }); it('does nothing when not checkout out', () => { jest.spyOn(self.analyticsFactory, 'checkoutFieldError') @@ -55,6 +58,632 @@ describe('branded analytics factory', () => { self.analyticsFactory.handleCheckoutFormErrors(form) expect(self.analyticsFactory.checkoutFieldError).not.toHaveBeenCalled() + }); + }); + + describe('cartAdd', () => { + const itemConfig = { + "campaign-page": "", + "jcr-title": "John Doe", + "recurring-day-of-month": "13", + "recurring-start-month": "09", + "amount": 50 + } + const productData = { + "uri": "items/crugive/a5t4fmspmfpwpqv3le7hgksifu=", + "frequencies": [ + { + "name": "SEMIANNUAL", + "display": "Semi-Annually", + "selectAction": "/itemselections/crugive/a5t4fmspmfpwpqv3le7hgksifu=/options/izzgk4lvmvxgg6i=/values/kncu2skbjzhfkqkm=/selector" + }, + { + "name": "QUARTERLY", + "display": "Quarterly", + "selectAction": "/itemselections/crugive/a5t4fmspmfpwpqv3le7hgksifu=/options/izzgk4lvmvxgg6i=/values/kfkucusuivjeywi=/selector" + }, + { + "name": "MON", + "display": "Monthly", + "selectAction": "/itemselections/crugive/a5t4fmspmfpwpqv3le7hgksifu=/options/izzgk4lvmvxgg6i=/values/jvhu4=/selector" + }, + { + "name": "ANNUAL", + "display": "Annually", + "selectAction": "/itemselections/crugive/a5t4fmspmfpwpqv3le7hgksifu=/options/izzgk4lvmvxgg6i=/values/ifhe4vkbjq=/selector" + }, + { + "name": "NA", + "display": "Single", + "selectAction": "" + } + ], + "frequency": "MON", + "displayName": "International Staff", + "designationType": "Staff", + "code": "0643021", + "designationNumber": "0643021", + "orgId": "STAFF" + } + + + it('should add an monthly item to the datalayer', () => { + jest.spyOn(self.envService, 'read').mockImplementation(name => name === 'isCheckout') + + self.analyticsFactory.cartAdd(itemConfig, productData) + + expect(self.$window.dataLayer.length).toEqual(1) + expect(self.$window.dataLayer[0].event).toEqual('add-to-cart') + expect(self.$window.dataLayer[0].ecommerce.add.products[0]).toEqual({ + item_id: productData.code, + item_name: productData.displayName, + item_brand: productData.orgId, + item_category: productData.designationType.toLowerCase(), + item_variant: 'monthly', + currency: 'USD', + price: itemConfig.amount.toString(), + quantity: '1', + recurring_date: 'September 13, 2023' + }) + }); + + it('should add an single item to the datalayer', () => { + productData.frequency = 'NA' + jest.spyOn(self.envService, 'read').mockImplementation(name => name === 'isCheckout') + + self.analyticsFactory.cartAdd(itemConfig, productData) + + expect(self.$window.dataLayer.length).toEqual(1) + expect(self.$window.dataLayer[0].event).toEqual('add-to-cart') + expect(self.$window.dataLayer[0].ecommerce.add.products[0]).toEqual({ + item_id: productData.code, + item_name: productData.displayName, + item_brand: productData.orgId, + item_category: productData.designationType.toLowerCase(), + item_variant: 'single', + currency: 'USD', + price: itemConfig.amount.toString(), + quantity: '1', + recurring_date: undefined + }) }) - }) -}) + }); + + + describe('buildProductVar', () => { + const cartData = { + "id": "geydmm3cgfsgiljygjtgeljumfstkllbgfrdkljvgf", + "items": [ + { + "uri": "/carts/crugive/geydmm3cgfsgiljygjtgeljumfstkllbgfrdkljvgf/lineitems/g44wcnzrhe3wgllcmezdmljugvqtgllc", + "code": "0048461_mon", + "orgId": "STAFF", + "displayName": "John Doe", + "designationType": "Staff", + "price": "$50.00", + "priceWithFees": "$51.20", + "config": { + "amount": 50, + "amount-with-fees": 51.2, + "campaign-code": "", + "donation-services-comments": "", + "premium-code": "", + "recipient-comments": "", + "recurring-day-of-month": "15", + "recurring-start-month": "09" + }, + "frequency": "Monthly", + "amount": 50, + "amountWithFees": 51.2, + "designationNumber": "0048461", + "productUri": "/items/crugive/a5t4fmspmhbkez6cvfmucmrkykwc7q4mykr4fps5ee=", + "giftStartDate": "2024-09-15T04:00:00.000Z", + "giftStartDateDaysFromNow": 361, + "giftStartDateWarning": true, + "$$hashKey": "object:506" + } + ], + "frequencyTotals": [ + { + "frequency": "Monthly", + "amount": 50, + "amountWithFees": 51.2, + "total": "$50.00", + "totalWithFees": "$51.20" + }, + { + "frequency": "Single", + "amount": 0, + "amountWithFees": 0, + "total": "$0.00", + "totalWithFees": "$0.00" + } + ], + "cartTotal": 0, + "cartTotalDisplay": "$0.00" + } + + + it('should add data to DataLayer', () => { + self.analyticsFactory.buildProductVar(cartData) + + expect(self.$window.digitalData.cart.id).toEqual(cartData.id) + expect(self.$window.digitalData.cart.hash).not.toEqual(null) + expect(self.$window.digitalData.cart.price.cartTotal).toEqual(cartData.cartTotal) + expect(self.$window.digitalData.cart.item.length).toEqual(1) + }); + }); + + describe('cartRemove', () => { + const item = { + "uri": "/carts/crugive/geydmm3cgfsgiljygjtgeljumfstkllbgfrdkljvgf/lineitems/g44wcnzrhe3wgllcmezdmljugvqtgllcmeztol", + "code": "0048461_mon", + "orgId": "STAFF", + "displayName": "John Doe", + "designationType": "Staff", + "price": "$50.00", + "priceWithFees": "$51.20", + "config": { + "amount": 50, + "amount-with-fees": 51.2, + "campaign-code": "CAMPAIGN", + "donation-services-comments": "", + "premium-code": "", + "recipient-comments": "", + "recurring-day-of-month": "15", + "recurring-start-month": "09" + }, + "frequency": "Monthly", + "amount": 50, + "amountWithFees": 51.2, + "designationNumber": "0048461", + "productUri": "/items/crugive/a5t4fmspmhbkez6cv", + "giftStartDate": "2024-09-15T04:00:00.000Z", + "giftStartDateDaysFromNow": 361, + "giftStartDateWarning": true, + "$$hashKey": "object:22", + "removing": true + } + + it('should remove item from dataLayer and fire event', () => { + jest.spyOn(self.envService, 'read').mockImplementation(name => name === 'isCheckout') + + self.analyticsFactory.cartRemove(item) + + expect(self.$window.digitalData.cart.item[0].attributes).toEqual({ + donationType: 'recurring donation', + donationFrequency: item.frequency.toLowerCase(), + siebel: { + productType: 'designation', + campaignCode: 'CAMPAIGN' + } + }) + expect(self.$window.digitalData.cart.item.length).toEqual(1) + + expect(self.$window.dataLayer[0].event).toEqual('remove-from-cart') + expect(self.$window.dataLayer[0].ecommerce).toEqual({ + currencyCode: 'USD', + remove: { + products: [{ + item_id: item.designationNumber, + item_name: item.displayName, + item_brand: item.orgId, + item_category: item.designationType.toLowerCase(), + item_variant: 'monthly', + currency: 'USD', + price: item.amount.toString(), + quantity: '1', + recurring_date: 'September 15, 2024' + }] + } + }) + }); + }); + + describe('checkoutStepEvent', () => { + const cart = { + "id": "g5stanjsgzswkllbmjtdaljuga4wmljzgnqw=", + "items": [ + { + "uri": "/carts/crugive/g5stanjsgzswkllbmjtdaljuga4wmljzgnqw/lineitems/he4wcnzvgfswgllgmizdqljumi2wkllbmjrdqljw", + "code": "0643021", + "orgId": "STAFF", + "displayName": "John Doe", + "designationType": "Staff", + "price": "$50.00", + "priceWithFees": "$51.20", + "config": { + "amount": 50, + "amount-with-fees": 51.2, + "campaign-code": "", + "donation-services-comments": "", + "premium-code": "", + "recipient-comments": "", + "recurring-day-of-month": "", + "recurring-start-month": "" + }, + "frequency": "Single", + "amount": 50, + "amountWithFees": 51.2, + "designationNumber": "0643021", + "productUri": "/items/crugive/a5t4fmspm", + "giftStartDate": null, + "giftStartDateDaysFromNow": 0, + "giftStartDateWarning": false + } + ], + "frequencyTotals": [ + { + "frequency": "Single", + "amount": 50, + "amountWithFees": 51.2, + "total": "$50.00", + "totalWithFees": "$51.20" + } + ], + "cartTotal": 50, + "cartTotalDisplay": "$50.00" + } + const item = cart.items[0] + const formattedItem = { + item_id: item.designationNumber, + item_name: item.displayName, + item_brand: item.orgId, + item_category: item.designationType.toLowerCase(), + item_variant: 'single', + currency: 'USD', + price: item.amount.toString(), + quantity: '1', + recurring_date: undefined + } + + it('should create begining checkout and checkout step DataLayer events', () => { + self.analyticsFactory.checkoutStepEvent('contact', cart) + + expect(self.$window.dataLayer.length).toEqual(2) + expect(self.$window.dataLayer[0]).toEqual({ + event: 'begin_checkout', + ecommerce: { + items: [formattedItem] + } + }) + + expect(self.$window.dataLayer[1].event).toEqual('checkout-step') + expect(self.$window.dataLayer[1].cartId).toEqual(cart.id) + expect(self.$window.dataLayer[1].ecommerce).toEqual({ + currencyCode: 'USD', + checkout: { + actionField: { + step: 1, + option: '' + }, + products: [formattedItem] + } + }) + }); + + it('should create payment info and checkout step DataLayer events', () => { + self.analyticsFactory.checkoutStepEvent('payment', cart) + + expect(self.$window.dataLayer.length).toEqual(2) + expect(self.$window.dataLayer[0]).toEqual({ + event: 'add_payment_info' + }) + + expect(self.$window.dataLayer[1].event).toEqual('checkout-step') + expect(self.$window.dataLayer[1].cartId).toEqual(cart.id) + expect(self.$window.dataLayer[1].ecommerce).toEqual({ + currencyCode: 'USD', + checkout: { + actionField: { + step: 2, + option: '' + }, + products: [formattedItem] + } + }) + }); + + it('should create review order and checkout step DataLayer events', () => { + self.analyticsFactory.checkoutStepEvent('review', cart) + + expect(self.$window.dataLayer.length).toEqual(2) + expect(self.$window.dataLayer[0]).toEqual({ + event: 'review_order', + }) + + expect(self.$window.dataLayer[1].event).toEqual('checkout-step') + expect(self.$window.dataLayer[1].cartId).toEqual(cart.id) + expect(self.$window.dataLayer[1].ecommerce).toEqual({ + currencyCode: 'USD', + checkout: { + actionField: { + step: 3, + option: '' + }, + products: [formattedItem] + } + }) + }) + }); + + describe('checkoutStepOptionEvent', () => { + it('should add contact checkout option event to DataLayer', () => { + self.analyticsFactory.checkoutStepOptionEvent('Household', 'contact') + expect(self.$window.dataLayer.length).toEqual(1) + expect(self.$window.dataLayer[0].event).toEqual('checkout-option') + expect(self.$window.dataLayer[0].ecommerce).toEqual({ + checkout_option: { + actionField: { + step: 1, + option: 'household' + } + } + }) + }); + it('should add payment checkout option event to DataLayer', () => { + self.analyticsFactory.checkoutStepOptionEvent('Visa', 'payment') + expect(self.$window.dataLayer.length).toEqual(1) + expect(self.$window.dataLayer[0].event).toEqual('checkout-option') + expect(self.$window.dataLayer[0].ecommerce).toEqual({ + checkout_option: { + actionField: { + step: 2, + option: 'visa' + } + } + }) + }); + it('should add review checkout option event to DataLayer', () => { + self.analyticsFactory.checkoutStepOptionEvent('Visa', 'review') + expect(self.$window.dataLayer.length).toEqual(1) + expect(self.$window.dataLayer[0].event).toEqual('checkout-option') + expect(self.$window.dataLayer[0].ecommerce).toEqual({ + checkout_option: { + actionField: { + step: 3, + option: 'visa' + } + } + }) + }); + }); + + describe('giveGiftModal', () => { + const productData = { + "uri": "items/crugive/a5t4fmspmfpwpqv3le7hgksifu=", + "frequencies": [ + { + "name": "SEMIANNUAL", + "display": "Semi-Annually", + "selectAction": "/itemselections/crugive/a5t4fmspmfpwpqv3le7hgksifu=/options/izzgk4lvmvxgg6i=/values/kncu2skbjzhfkqkm=/selector" + }, + { + "name": "QUARTERLY", + "display": "Quarterly", + "selectAction": "/itemselections/crugive/a5t4fmspmfpwpqv3le7hgksifu=/options/izzgk4lvmvxgg6i=/values/kfkucusuivjeywi=/selector" + }, + { + "name": "MON", + "display": "Monthly", + "selectAction": "/itemselections/crugive/a5t4fmspmfpwpqv3le7hgksifu=/options/izzgk4lvmvxgg6i=/values/jvhu4=/selector" + }, + { + "name": "ANNUAL", + "display": "Annually", + "selectAction": "/itemselections/crugive/a5t4fmspmfpwpqv3le7hgksifu=/options/izzgk4lvmvxgg6i=/values/ifhe4vkbjq=/selector" + }, + { + "name": "NA", + "display": "Single", + "selectAction": "" + } + ], + "frequency": "NA", + "displayName": "International Staff", + "designationType": "Staff", + "code": "0643021", + "designationNumber": "0643021", + "orgId": "STAFF" + } + + it('should push give-gift-modal event to the DataLayer', () => { + self.analyticsFactory.giveGiftModal(productData) + expect(self.$window.dataLayer.length).toEqual(1) + expect(self.$window.dataLayer[0].event).toEqual('give-gift-modal') + expect(self.$window.dataLayer[0].ecommerce).toEqual({ + currencyCode: 'USD', + detail: { + products: [{ + item_id: '0643021', + item_name: 'International Staff', + item_brand: 'STAFF', + item_category: 'staff', + item_variant: undefined, + price: undefined, + currency: 'USD', + quantity: '1', + recurring_date: undefined, + }] + } + }) + }); + }); + + + describe('productViewDetailsEvent', () => { + const productData = { + "path": "https://give-stage-cloud.cru.org/designations/0/6/4/3/0/0643021.html", + "designationNumber": "0643021", + "campaignPage": null, + "replacementDesignationNumber": null, + "name": "John Doe", + "type": "Staff", + "facet": "person", + "startMonth": null, + "ministry": "Staff Giving", + "orgId": "1-TG-11", + "$$hashKey": "object:26" + } + it('should add product-detail-click event', () => { + self.analyticsFactory.productViewDetailsEvent(productData) + expect(self.$window.dataLayer.length).toEqual(1) + expect(self.$window.dataLayer[0].event).toEqual('product-detail-click') + expect(self.$window.dataLayer[0].ecommerce).toEqual({ + currencyCode: 'USD', + click: { + actionField: { + list: 'search results' + }, + products: [ + { + item_id: '0643021', + item_name: 'John Doe', + item_brand: '1-TG-11', + item_category: 'staff', + item_variant: undefined, + price: undefined, + currency: 'USD', + quantity: '1', + recurring_date: undefined, + } + ] + } + }) + }); + }); + + describe('transactionEvent', () => { + const item = { + "uri": "/carts/crugive/grsgezrxhfqtsljrga3gkljugvtdaljygjqtc;/lineitems/hezwgntcmrsgmllcgu3dsljumuygcljzmjsgcljwgqzdkyr", + "code": "0643021", + "orgId": "STAFF", + "displayName": "John Doe", + "designationType": "Staff", + "price": "$50.00", + "priceWithFees": "$51.20", + "config": { + "amount": 50, + "amount-with-fees": 51.2, + "campaign-code": "", + "donation-services-comments": "", + "premium-code": "", + "recipient-comments": "", + "recurring-day-of-month": "", + "recurring-start-month": "" + }, + "frequency": "Single", + "amount": 50, + "amountWithFees": 51.2, + "designationNumber": "0643021", + "productUri": "/items/crugive/a5t4fmspmfpwpqv", + "giftStartDate": null, + "giftStartDateDaysFromNow": 0, + "giftStartDateWarning": false, + "$$hashKey": "object:66" + } + const transactionCart = { + "id": "grsgezrxhfqtsljrga3gkljugvtdaljygjqtcl", + "items": [item], + "frequencyTotals": [ + { + "frequency": "Single", + "amount": 50, + "amountWithFees": 51.2, + "total": "$50.00", + "totalWithFees": "$51.20", + "$$hashKey": "object:68" + } + ], + "cartTotal": 50, + "cartTotalDisplay": "$50.00" + } + const purchaseData = { + "donorDetails": { + "donor-type": "Household", + }, + "paymentMeans": { + "card-type": "Visa", + }, + "lineItems": [], + "rateTotals": [], + "rawData": { + "purchase-number": "23032", + } + } + it('should add purchase event', async () => { + self.$window.sessionStorage.setItem('coverFeeDecision', null) + self.$window.localStorage.setItem('transactionCart', JSON.stringify(transactionCart)) + self.$window.sessionStorage.setItem('transactionId', 23031) + + expect(self.$window.sessionStorage.getItem('transactionId')).toEqual('23031') + + self.analyticsFactory.transactionEvent(purchaseData) + + expect(self.$window.sessionStorage.getItem('transactionId')).toEqual(purchaseData.rawData['purchase-number']) + + expect(self.$window.dataLayer.length).toEqual(1) + expect(self.$window.dataLayer[0].event).toEqual('purchase') + expect(self.$window.dataLayer[0].paymentType).toEqual('credit card') + expect(self.$window.dataLayer[0].ecommerce).toEqual({ + currency: 'USD', + payment_type: 'credit card', + donator_type: 'Household', + pays_processing: 'no', + value: '50.00', + processing_fee: undefined, + transaction_id: purchaseData.rawData['purchase-number'], + items: [ + { + item_id: '0643021', + item_name: 'John Doe', + item_brand: '1-TG-11', + item_category: 'staff', + item_variant: 'single', + price: '50', + currency: 'USD', + quantity: '1', + recurring_date: undefined, + ga_donator_type: null, + donation_type: 'one-time', + donation_frequency: 'single', + payment_type: 'credit card', + purchase_number: '23032', + campaign_code: undefined, + designation: 'designation', + item_brand: 'STAFF', + processingFee: undefined, + + } + ] + }) + }); + + it('should ignore and not send event', async () => { + self.$window.sessionStorage.setItem('coverFeeDecision', null) + self.$window.localStorage.setItem('transactionCart', JSON.stringify(transactionCart)) + self.$window.sessionStorage.setItem('transactionId', 23032) + self.analyticsFactory.transactionEvent(purchaseData) + + expect(self.$window.dataLayer.length).toEqual(0) + }); + + it('should add up totals correctly purchase event', async () => { + // Adding three items to the cart + transactionCart.items = [item,item,item] + self.$window.sessionStorage.setItem('coverFeeDecision', true) + self.$window.localStorage.setItem('transactionCart', JSON.stringify(transactionCart)) + self.$window.sessionStorage.setItem('transactionId', 23031) + + self.analyticsFactory.transactionEvent(purchaseData) + + const totalWithFees = 51.2 * 3 + const totalWithoutFees = 50 * 3 + + expect(self.$window.dataLayer[0].ecommerce.processing_fee).toEqual((totalWithFees - totalWithoutFees).toFixed(2)) + expect(self.$window.dataLayer[0].ecommerce.value).toEqual((totalWithFees).toFixed(2)) + expect(self.$window.dataLayer[0].ecommerce.pays_processing).toEqual('yes') + expect(self.$window.dataLayer[0].ecommerce.items[0].processingFee).toEqual((51.2 - 50).toFixed(2)) + }); + }); +}); diff --git a/src/app/productConfig/productConfigForm/productConfigForm.component.js b/src/app/productConfig/productConfigForm/productConfigForm.component.js index 4d6e3a604..869f0b486 100644 --- a/src/app/productConfig/productConfigForm/productConfigForm.component.js +++ b/src/app/productConfig/productConfigForm/productConfigForm.component.js @@ -309,7 +309,9 @@ class ProductConfigFormController { this.onStateChange({ state: 'submitting' }) const comment = this.itemConfig['donation-services-comments'] - this.brandedAnalyticsFactory.saveTestingTransaction(comment ? comment.toLowerCase().includes('test') : false) + const isTestingTransaction = comment ? comment.toLowerCase().includes('test') : false + this.brandedAnalyticsFactory.saveTestingTransaction(isTestingTransaction) + this.analyticsFactory.saveTestingTransaction(this.productData, isTestingTransaction) const data = this.productData.frequency === 'NA' ? omit(this.itemConfig, ['recurring-start-month', 'recurring-day-of-month']) : this.itemConfig From 9c44250a47c19267cf9138ab5908b01d3e3e7810 Mon Sep 17 00:00:00 2001 From: Bill Randall Date: Thu, 5 Oct 2023 13:45:25 -0400 Subject: [PATCH 2/3] Fixes for 8.1 --- src/app/analytics/analytics.factory.js | 2 +- src/app/analytics/analytics.factory.spec.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/analytics/analytics.factory.js b/src/app/analytics/analytics.factory.js index 9b9a77fe4..ac41ef36e 100644 --- a/src/app/analytics/analytics.factory.js +++ b/src/app/analytics/analytics.factory.js @@ -576,7 +576,7 @@ const analyticsFactory = /* @ngInject */ function ($window, $timeout, envService $window.dataLayer = $window.dataLayer || [] $window.dataLayer.push({ event: 'purchase', - paymentType: purchaseData.paymentInstruments['account-type'] ? 'bank account' : 'credit card', + paymentType: purchaseData.paymentInstruments['account-type'] ? 'bank account' : 'credit card', ecommerce: { currency: 'USD', payment_type: purchaseData.paymentInstruments['account-type'] ? 'bank account' : 'credit card', diff --git a/src/app/analytics/analytics.factory.spec.js b/src/app/analytics/analytics.factory.spec.js index 4acafce5b..d3a17fe59 100644 --- a/src/app/analytics/analytics.factory.spec.js +++ b/src/app/analytics/analytics.factory.spec.js @@ -602,7 +602,7 @@ describe('analytics factory', () => { "donorDetails": { "donor-type": "Household", }, - "paymentMeans": { + "paymentInstruments": { "card-type": "Visa", }, "lineItems": [], From 2d308462ac5540824be729bcd698ebcf92130706 Mon Sep 17 00:00:00 2001 From: Bill Randall Date: Thu, 5 Oct 2023 13:47:58 -0400 Subject: [PATCH 3/3] Factor out a const --- src/app/analytics/analytics.factory.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/app/analytics/analytics.factory.js b/src/app/analytics/analytics.factory.js index ac41ef36e..4b1226586 100644 --- a/src/app/analytics/analytics.factory.js +++ b/src/app/analytics/analytics.factory.js @@ -552,8 +552,9 @@ const analyticsFactory = /* @ngInject */ function ($window, $timeout, envService let purchaseTotalWithFees = 0 // If the lastTransactionId and the current one do not match, we need to send an analytics event for the transaction if (purchaseData && lastTransactionId !== currentTransactionId) { - // Set the transactionId in localStorage to be the one that is passed in + const paymentType = purchaseData.paymentInstruments['account-type'] ? 'bank account' : 'credit card' + // Set the transactionId in localStorage to be the one that is passed in sessionStorage.setItem('transactionId', currentTransactionId) const cartObject = transactionCart.items.map((cartItem) => { const { amount, amountWithFees } = cartItem @@ -566,7 +567,7 @@ const analyticsFactory = /* @ngInject */ function ($window, $timeout, envService ga_donator_type: localStorage.getItem('gaDonorType'), donation_type: frequency === 'single' ? 'one-time' : 'recurring', donation_frequency: frequency, - payment_type: purchaseData.paymentInstruments['account-type'] ? 'bank account' : 'credit card', + payment_type: paymentType, purchase_number: purchaseData.rawData['purchase-number'], campaign_code: cartItem.config['campaign-code'] !== '' ? cartItem.config['campaign-code'] : undefined, designation: 'designation' @@ -576,10 +577,10 @@ const analyticsFactory = /* @ngInject */ function ($window, $timeout, envService $window.dataLayer = $window.dataLayer || [] $window.dataLayer.push({ event: 'purchase', - paymentType: purchaseData.paymentInstruments['account-type'] ? 'bank account' : 'credit card', + paymentType: paymentType, ecommerce: { currency: 'USD', - payment_type: purchaseData.paymentInstruments['account-type'] ? 'bank account' : 'credit card', + payment_type: paymentType, donator_type: purchaseData.donorDetails['donor-type'], pays_processing: purchaseTotalWithFees && coverFeeDecision ? 'yes' : 'no', value: purchaseTotalWithFees && coverFeeDecision ? purchaseTotalWithFees.toFixed(2).toString() : purchaseTotal.toFixed(2).toString(),