Merge pull request #1608 from Safelite/feature/CSR-1856

Feature/csr 1856 TO 12/7
This commit is contained in:
chloeherdsafelite 2023-12-01 12:58:09 -05:00 committed by GitHub
commit a5e40c0da6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 234 additions and 64 deletions

View file

@ -6,6 +6,7 @@ const applicationConfig = {
COOKIE_PATH: "/", COOKIE_PATH: "/",
CURRENT_ENVIRONMENT: process.env.VUE_APP_CURRENT_ENVIRONMENT, // "Localhost", "Dev", "QA", and "Prod" CURRENT_ENVIRONMENT: process.env.VUE_APP_CURRENT_ENVIRONMENT, // "Localhost", "Dev", "QA", and "Prod"
APPLICATION_NAME: "FixMyGlass", APPLICATION_NAME: "FixMyGlass",
ANALYTICS_APPLICATION_NAME: "FixMyGlassNextGen",
APPLICATION_ABBREVIATION: "fmg", APPLICATION_ABBREVIATION: "fmg",
PAGE_QUERYSTRING: "fmgPage", PAGE_QUERYSTRING: "fmgPage",
SITE_ENTRY_TRIGGER_VALUE: "FixMyGlass", SITE_ENTRY_TRIGGER_VALUE: "FixMyGlass",

View file

@ -3,10 +3,32 @@ import { applicationConfig } from "@/constants/application-config.js";
const cookieNames = { const cookieNames = {
FUNNEL_SESSION_INFO: `FunnelSessionInfo-${applicationConfig.CURRENT_ENVIRONMENT}`, FUNNEL_SESSION_INFO: `FunnelSessionInfo-${applicationConfig.CURRENT_ENVIRONMENT}`,
FUNNEL_SESSION_KEY: `FunnelSessionKey-${applicationConfig.CURRENT_ENVIRONMENT}`,
FUNNEL_USER_ID: `FunnelUserId-${applicationConfig.CURRENT_ENVIRONMENT}`,
// Existing Safelite.com cookies // Existing Safelite.com cookies
DXDEV: "dxdev", DXDEV: "dxdev",
SESSION_ID: "sid", SESSION_ID: "sid",
SESSION_KEY: "skey", SESSION_KEY: "skey",
}; };
export { cookieNames }; const cookieExpirations = {
SESSION_ID: convertToSeconds({ minutes: 30 }),
DXDEV: convertToSeconds({ years: 1 }),
FUNNEL_USER_ID: convertToSeconds({ weeks: 1 }),
FUNNEL_SESSION_KEY: convertToSeconds({ minutes: 30 }),
};
export { cookieNames, cookieExpirations };
function convertToSeconds({ years, months, weeks, days, hours, minutes, seconds }) {
let total = seconds ?? 0;
total += (minutes ?? 0) * 60;
total += (hours ?? 0) * 60 * 60;
total += (days ?? 0) * 24 * 60 * 60;
total += (weeks ?? 0) * 7 * 24 * 60 * 60;
total += (months ?? 0) * 30 * 24 * 60 * 60;
total += (years ?? 0) * 365 * 24 * 60 * 60;
return total;
}

View file

@ -1,4 +1,4 @@
import { cookieNames } from "@/constants/cookie-names"; import { cookieNames, cookieExpirations } from "@/constants/cookie-names";
import store from "@/store"; import store from "@/store";
import { applicationConfig } from "@/constants/application-config"; import { applicationConfig } from "@/constants/application-config";
@ -78,11 +78,43 @@ export function getDeviceIdValue() {
return "00000000-0000-0000-0000-000000000000"; return "00000000-0000-0000-0000-000000000000";
} }
export function regenerateDeviceId() {
if (!isCookieSet(cookieNames.DXDEV)) {
setCookieProperties(
{
[cookieNames.DXDEV]: `did=${crypto.randomUUID()}`,
},
{ maxAge: cookieExpirations.DXDEV }
);
}
}
export function getUserIdValue() {
const cookieValue = getCookieValueByName(cookieNames.FUNNEL_USER_ID);
if (cookieValue) {
return cookieValue;
}
return "00000000-0000-0000-0000-000000000000";
}
export function regenerateUserId() {
if (!isCookieSet(cookieNames.FUNNEL_USER_ID)) {
setCookieProperties(
{
[cookieNames.FUNNEL_USER_ID]: crypto.randomUUID(),
},
{ maxAge: cookieExpirations.FUNNEL_USER_ID }
);
}
}
/* /*
Gets value of skey cookie, returns 0 if not found. Gets value of skey cookie, returns 0 if not found.
*/ */
export function getSessionKeyValue() { export function getSessionKeyValue() {
const cookieValue = getCookieValueByName(cookieNames.SESSION_KEY); const cookieValue = getCookieValueByName(cookieNames.FUNNEL_SESSION_KEY);
if (cookieValue) { if (cookieValue) {
return cookieValue; return cookieValue;
@ -91,6 +123,17 @@ export function getSessionKeyValue() {
return 0; return 0;
} }
export function setSessionKeyIfUnset(value) {
if (!isCookieSet(cookieNames.FUNNEL_SESSION_KEY)) {
setCookieProperties(
{
[cookieNames.FUNNEL_SESSION_KEY]: value,
},
{ maxAge: cookieExpirations.FUNNEL_SESSION_KEY }
);
}
}
/* /*
Gets value of skey cookie, returns 0 if not found. Gets value of skey cookie, returns 0 if not found.
*/ */
@ -104,11 +147,24 @@ export function getSessionIdValue() {
return "00000000-0000-0000-0000-000000000000"; return "00000000-0000-0000-0000-000000000000";
} }
export function setSessionIdIfUnset(value) {
if (!isCookieSet(cookieNames.SESSION_ID)) {
setCookieProperties(
{
[cookieNames.SESSION_ID]: value,
},
{ maxAge: cookieExpirations.SESSION_ID }
);
}
}
/* /*
Updates session ID cookie with new expiration date Updates session ID cookie with new expiration date
*/ */
export function updateSessionIdCookie() { export function updateSessionIdCookie() {
createOrUpdateCookie(cookieNames.SESSION_ID, getSessionIdValue(), { maxAge: 60 * 30 }); createOrUpdateCookie(cookieNames.SESSION_ID, getSessionIdValue(), {
maxAge: cookieExpirations.SESSION_ID,
});
} }
export function setCookieProperties( export function setCookieProperties(
@ -126,6 +182,34 @@ export function setCookieProperties(
} }
} }
export function isCookieSet(name) {
const val = getCookieValueByName(name);
return !!val;
}
export function areAllSessionCookiesSet() {
return (
isCookieSet(cookieNames.SESSION_ID) &&
isCookieSet(cookieNames.DXDEV) &&
isCookieSet(cookieNames.FUNNEL_SESSION_KEY) &&
isCookieSet(cookieNames.FUNNEL_USER_ID)
);
}
export function refreshSessionExpiration() {
refreshCookieExpiration(cookieNames.SESSION_ID, cookieExpirations.SESSION_ID);
refreshCookieExpiration(cookieNames.DXDEV, cookieExpirations.DXDEV);
refreshCookieExpiration(cookieNames.FUNNEL_USER_ID, cookieExpirations.FUNNEL_USER_ID);
refreshCookieExpiration(cookieNames.FUNNEL_SESSION_KEY, cookieExpirations.FUNNEL_SESSION_KEY);
}
export function refreshCookieExpiration(name, expirationTime) {
if (isCookieSet(name)) {
createOrUpdateCookie(name, getCookieValueByName(name), { maxAge: expirationTime });
}
}
/* /*
=========================== ===========================
= PRIVATE FUNCTIONS = = PRIVATE FUNCTIONS =

View file

@ -100,7 +100,8 @@ export const cookies = {
someOtherCookie: "{}", someOtherCookie: "{}",
dxdev: "did=21b9b94a-ec23-42c1-aaac-e2ae4e4dbffe", dxdev: "did=21b9b94a-ec23-42c1-aaac-e2ae4e4dbffe",
sid: "cba0c3d1-3c1b-4305-bb56-31aa50f58e27", sid: "cba0c3d1-3c1b-4305-bb56-31aa50f58e27",
skey: "12345", [cookieNames.FUNNEL_SESSION_KEY]: "12345",
[cookieNames.FUNNEL_USER_ID]: "11aec5e8-92ba-4dc9-a8b6-179a916d8d7a",
}; };
// Removes test cookies for testing cookie-helper and order-helper // Removes test cookies for testing cookie-helper and order-helper

View file

@ -1,9 +1,15 @@
import { storeActions } from "@/constants/store-actions"; import { storeActions } from "@/constants/store-actions";
import { import {
setCookieProperties,
getDeviceIdValue, getDeviceIdValue,
getSessionIdValue, getSessionIdValue,
getSessionKeyValue, getSessionKeyValue,
getUserIdValue,
regenerateDeviceId,
regenerateUserId,
refreshSessionExpiration,
areAllSessionCookiesSet,
setSessionIdIfUnset,
setSessionKeyIfUnset,
} from "@/helpers/heritage-integration/cookie-helper"; } from "@/helpers/heritage-integration/cookie-helper";
import { queryStrings } from "@/constants/query-strings"; import { queryStrings } from "@/constants/query-strings";
import { experimentSettings } from "@/constants/experiments"; import { experimentSettings } from "@/constants/experiments";
@ -15,7 +21,6 @@ import {
GaEvents, GaEvents,
ValueToLogTypes, ValueToLogTypes,
} from "@/constants/analytics"; } from "@/constants/analytics";
import { cookieNames } from "@/constants/cookie-names";
import store from "@/store"; import store from "@/store";
import baseMixin from "@/mixins/base-mixin"; import baseMixin from "@/mixins/base-mixin";
@ -27,10 +32,12 @@ export default {
return getPageNameByQueryString(); return getPageNameByQueryString();
}, },
logPageView(pageEvent) { async logPageView(pageEvent) {
const currentPageName = getPageNameByQueryString(); const currentPageName = getPageNameByQueryString();
await this.validateSession();
var payload = { var payload = {
userId: getDeviceIdValue(), userId: getUserIdValue(),
sessionKey: getSessionKeyValue(), sessionKey: getSessionKeyValue(),
pageName: currentPageName, pageName: currentPageName,
sessionId: getSessionIdValue(), sessionId: getSessionIdValue(),
@ -42,14 +49,15 @@ export default {
parentAccountNumber: store.getters.order.payment.parentAccountNumber, parentAccountNumber: store.getters.order.payment.parentAccountNumber,
}; };
baseMixin.methods.dispatchStoreAction(storeActions.LOG_PAGE_VIEW, payload, false); await baseMixin.methods.dispatchStoreAction(storeActions.LOG_PAGE_VIEW, payload, false);
}, },
logCustomEvent(category, action, label, value) { async logCustomEvent(category, action, label, value) {
const currentPageName = getPageNameByQueryString(); const currentPageName = getPageNameByQueryString();
await this.validateSession();
var payload = { var payload = {
userId: getDeviceIdValue(), userId: getUserIdValue(),
sessionKey: getSessionKeyValue(), sessionKey: getSessionKeyValue(),
pageName: currentPageName, pageName: currentPageName,
sessionId: getSessionIdValue(), sessionId: getSessionIdValue(),
@ -63,10 +71,14 @@ export default {
parentAccountNumber: store.getters.order.payment.parentAccountNumber, parentAccountNumber: store.getters.order.payment.parentAccountNumber,
}; };
baseMixin.methods.dispatchStoreAction(storeActions.LOG_CUSTOM_EVENT, payload, false); await baseMixin.methods.dispatchStoreAction(
storeActions.LOG_CUSTOM_EVENT,
payload,
false
);
}, },
pushEventToGA(category, action, label, pushToLogApp = false, valueToLogType = null) { async pushEventToGA(category, action, label, pushToLogApp = false, valueToLogType = null) {
const currentPageName = getPageNameByQueryString(); const currentPageName = getPageNameByQueryString();
const labelToLog = getValueToLog(label, valueToLogType); const labelToLog = getValueToLog(label, valueToLogType);
@ -82,11 +94,11 @@ export default {
pushToDataLayerIfDefined(eventToBePushed); pushToDataLayerIfDefined(eventToBePushed);
if (pushToLogApp) { if (pushToLogApp) {
this.logCustomEvent(category, action, labelToLog, undefined); await this.logCustomEvent(category, action, labelToLog, undefined);
} }
}, },
pushPageViewToGA() { async pushPageViewToGA() {
const currentPageName = getPageNameByQueryString(); const currentPageName = getPageNameByQueryString();
const pageViewEvent = { const pageViewEvent = {
event: GaEvents.PAGE_VIEW_EVENT, event: GaEvents.PAGE_VIEW_EVENT,
@ -96,7 +108,7 @@ export default {
pushToDataLayerIfDefined(pageViewEvent); pushToDataLayerIfDefined(pageViewEvent);
this.logPageView(analyticsPageEvents.ENTRY); await this.logPageView(analyticsPageEvents.ENTRY);
}, },
pushExperimentsToDataLayer() { pushExperimentsToDataLayer() {
@ -136,16 +148,22 @@ export default {
}, },
async initSession() { async initSession() {
const sid = getSessionIdValue(); regenerateDeviceId();
const skey = getSessionKeyValue(); regenerateUserId();
const referrer =
applicationConfig.CURRENT_ENVIRONMENT != "Localhost" ? document.referrer : null;
var payload = { const userId = getUserIdValue(); // cookieNames.FUNNEL_USER_ID
userId: getDeviceIdValue(), const deviceId = getDeviceIdValue(); // cookieNames.DXDEV
sessionId: sid, const sessionId = getSessionIdValue(); // cookieNames.SESSION_ID
userAgent: navigator.userAgent, const userAgent = navigator.userAgent; // navigator.userAgent
referrer: referrer, const refferer =
applicationConfig.CURRENT_ENVIRONMENT != "Localhost" ? document.referrer : null; // see above
const payload = {
userId: userId,
deviceId: deviceId,
sessionId: sessionId,
userAgent: userAgent,
refferer: refferer,
}; };
const response = await baseMixin.methods.dispatchStoreAction( const response = await baseMixin.methods.dispatchStoreAction(
@ -155,30 +173,26 @@ export default {
); );
if (response?.data) { if (response?.data) {
if (response?.data.sessionKey && skey === 0) { if (response.data.sessionKey) {
setCookieProperties( setSessionKeyIfUnset(response.data.sessionKey);
{ [cookieNames.SESSION_KEY]: response?.data.sessionKey },
{
useDefaultFunnelCookieAttributes: false,
}
);
} }
if (response?.data.sessionId && sid === "00000000-0000-0000-0000-000000000000") {
setCookieProperties( if (response.data.sessionId) {
{ [cookieNames.SESSION_ID]: response?.data.sessionId }, setSessionIdIfUnset(response.data.sessionId);
{
maxAge: 60 * 30, // 30 minutes
}
);
} }
} }
}, },
noSession() { noSession() {
return ( return !areAllSessionCookiesSet();
getSessionKeyValue() === 0 || },
getSessionIdValue() === "00000000-0000-0000-0000-000000000000"
); async validateSession() {
if (this.noSession()) {
await this.initSession();
}
refreshSessionExpiration();
}, },
removeParamsFromEndpoint(endpoint) { removeParamsFromEndpoint(endpoint) {

View file

@ -11,8 +11,20 @@ import {
} from "@/constants/analytics"; } from "@/constants/analytics";
import store from "@/store"; import store from "@/store";
// Mock only the regenerate functions; window.crypto is not available in testing.
jest.mock("@/helpers/heritage-integration/cookie-helper", () => {
const originalModule = jest.requireActual("@/helpers/heritage-integration/cookie-helper");
return {
__esModule: true,
...originalModule,
regenerateDeviceId: jest.fn(),
regenerateUserId: jest.fn(),
};
});
describe("analyticsMixin.js", () => { describe("analyticsMixin.js", () => {
test("logPageView: calls dispatch with type and payload", () => { test("logPageView: calls dispatch with type and payload", async () => {
const type = ""; const type = "";
const payload = {}; const payload = {};
@ -21,6 +33,9 @@ describe("analyticsMixin.js", () => {
{ {
actionName: storeActions.LOG_PAGE_VIEW, actionName: storeActions.LOG_PAGE_VIEW,
}, },
{
actionName: storeActions.INITIALIZE_SESSION,
},
], ],
}; };
const mocks = setupMocksForJsFiles(mockData); const mocks = setupMocksForJsFiles(mockData);
@ -31,27 +46,35 @@ describe("analyticsMixin.js", () => {
setupCookies({ funnelCookieValue: JSON.stringify(testCookieValue) }); setupCookies({ funnelCookieValue: JSON.stringify(testCookieValue) });
analyticsMixin.methods.logPageView(type, payload); await analyticsMixin.methods.logPageView(type, payload);
expect(mocks.baseMixin.methods.dispatchStoreAction).toBeCalled(); expect(mocks.baseMixin.methods.dispatchStoreAction).toBeCalled();
}); });
test("logCustomEvent: calls dispatch with type and payload", () => { test("logCustomEvent: calls dispatch with type and payload", async () => {
const mockData = { const mockData = {
actionList: [ actionList: [
{ {
actionName: storeActions.LOG_CUSTOM_EVENT, actionName: storeActions.LOG_CUSTOM_EVENT,
}, },
{
actionName: storeActions.INITIALIZE_SESSION,
},
], ],
}; };
const mocks = setupMocksForJsFiles(mockData); const mocks = setupMocksForJsFiles(mockData);
analyticsMixin.methods.logCustomEvent("someCat", "someAction", "someLabel", "someVal"); await analyticsMixin.methods.logCustomEvent(
"someCat",
"someAction",
"someLabel",
"someVal"
);
expect(mocks.baseMixin.methods.dispatchStoreAction).toBeCalled(); expect(mocks.baseMixin.methods.dispatchStoreAction).toBeCalled();
}); });
test("pushEventToGA, should call dataLayer push and logCustomEvent too", () => { test("pushEventToGA, should call dataLayer push and logCustomEvent too", async () => {
// Arrange // Arrange
window.dataLayer = []; window.dataLayer = [];
const mockData = { const mockData = {
@ -59,6 +82,9 @@ describe("analyticsMixin.js", () => {
{ {
actionName: storeActions.LOG_CUSTOM_EVENT, actionName: storeActions.LOG_CUSTOM_EVENT,
}, },
{
actionName: storeActions.INITIALIZE_SESSION,
},
], ],
}; };
const mocks = setupMocksForJsFiles(mockData); const mocks = setupMocksForJsFiles(mockData);
@ -73,14 +99,14 @@ describe("analyticsMixin.js", () => {
}); });
// Act // Act
analyticsMixin.methods.pushEventToGA("category", "action", "label", true); await analyticsMixin.methods.pushEventToGA("category", "action", "label", true);
// Assert // Assert
expect(mockDataLayer).toEqual(expect.arrayContaining(window.dataLayer)); expect(mockDataLayer).toEqual(expect.arrayContaining(window.dataLayer));
expect(mocks.baseMixin.methods.dispatchStoreAction).toBeCalled(); expect(mocks.baseMixin.methods.dispatchStoreAction).toBeCalled();
}); });
test("pushEventToGA, should call dataLayer push and ValueToLogTypes.LAST_5 only logs last 5 of label", () => { test("pushEventToGA, should call dataLayer push and ValueToLogTypes.LAST_5 only logs last 5 of label", async () => {
// Arrange // Arrange
window.dataLayer = []; window.dataLayer = [];
var expectedDataLayer = []; var expectedDataLayer = [];
@ -93,8 +119,21 @@ describe("analyticsMixin.js", () => {
path: "/fmg/?fmgPage=", path: "/fmg/?fmgPage=",
}); });
const mockData = {
actionList: [
{
actionName: storeActions.LOG_CUSTOM_EVENT,
},
{
actionName: storeActions.INITIALIZE_SESSION,
},
],
};
const mocks = setupMocksForJsFiles(mockData);
// Act // Act
analyticsMixin.methods.pushEventToGA( await analyticsMixin.methods.pushEventToGA(
"category", "category",
"action", "action",
"1111122222333333", "1111122222333333",
@ -106,7 +145,7 @@ describe("analyticsMixin.js", () => {
expect(expectedDataLayer).toEqual(expect.arrayContaining(window.dataLayer)); expect(expectedDataLayer).toEqual(expect.arrayContaining(window.dataLayer));
}); });
test("pushEventToGA, should call dataLayer push and ValueToLogTypes.LAST_5 logs only the last 3 characters for a 3 character string", () => { test("pushEventToGA, should call dataLayer push and ValueToLogTypes.LAST_5 logs only the last 3 characters for a 3 character string", async () => {
// Arrange // Arrange
window.dataLayer = []; window.dataLayer = [];
var expectedDataLayer = []; var expectedDataLayer = [];
@ -119,8 +158,21 @@ describe("analyticsMixin.js", () => {
path: "/fmg/?fmgPage=", path: "/fmg/?fmgPage=",
}); });
const mockData = {
actionList: [
{
actionName: storeActions.LOG_CUSTOM_EVENT,
},
{
actionName: storeActions.INITIALIZE_SESSION,
},
],
};
const mocks = setupMocksForJsFiles(mockData);
// Act // Act
analyticsMixin.methods.pushEventToGA( await analyticsMixin.methods.pushEventToGA(
"category", "category",
"action", "action",
"111", "111",

View file

@ -38,11 +38,7 @@ const routes = [
async beforeEnter(to, from, next) { async beforeEnter(to, from, next) {
// If we have no query string, or we don't have the FmgPage query string. // If we have no query string, or we don't have the FmgPage query string.
try { try {
if (analyticsMixin.methods.noSession()) { await analyticsMixin.methods.validateSession();
await analyticsMixin.methods.initSession();
} else {
updateSessionIdCookie();
}
if (getFunnelCookie()?.SuppressConceptFunnel) { if (getFunnelCookie()?.SuppressConceptFunnel) {
await navigateToHeritageFunnel({ shouldSaveSession: false }); await navigateToHeritageFunnel({ shouldSaveSession: false });

View file

@ -1076,7 +1076,7 @@ export const actions = {
sessionKey: sessionKey, sessionKey: sessionKey,
sessionId: sessionId, sessionId: sessionId,
pageName: pageName, pageName: pageName,
applicationName: applicationConfig.APPLICATION_NAME, applicationName: applicationConfig.ANALYTICS_APPLICATION_NAME,
action: action, action: action,
event: event, event: event,
shouldUseSessionId: shouldUseSessionId, shouldUseSessionId: shouldUseSessionId,
@ -1124,7 +1124,7 @@ export const actions = {
sessionKey: sessionKey, sessionKey: sessionKey,
sessionId: sessionId, sessionId: sessionId,
pageName: pageName, pageName: pageName,
applicationName: applicationConfig.APPLICATION_NAME, applicationName: applicationConfig.ANALYTICS_APPLICATION_NAME,
category: category, category: category,
action: action, action: action,
label: label, label: label,
@ -1151,11 +1151,11 @@ export const actions = {
} }
); );
}, },
initializeSession(context, { userId, sessionId, userAgent, referrer }) { initializeSession(context, { userId, deviceId, sessionId, userAgent, referrer }) {
var payload = { var payload = {
applicationName: applicationConfig.APPLICATION_NAME, applicationName: applicationConfig.ANALYTICS_APPLICATION_NAME,
userId: userId, userId: userId,
deviceId: userId, deviceId: deviceId,
sessionId: sessionId, sessionId: sessionId,
userAgent: userAgent, userAgent: userAgent,
operatorId: "WEB", operatorId: "WEB",