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: "/",
CURRENT_ENVIRONMENT: process.env.VUE_APP_CURRENT_ENVIRONMENT, // "Localhost", "Dev", "QA", and "Prod"
APPLICATION_NAME: "FixMyGlass",
ANALYTICS_APPLICATION_NAME: "FixMyGlassNextGen",
APPLICATION_ABBREVIATION: "fmg",
PAGE_QUERYSTRING: "fmgPage",
SITE_ENTRY_TRIGGER_VALUE: "FixMyGlass",

View file

@ -3,10 +3,32 @@ import { applicationConfig } from "@/constants/application-config.js";
const cookieNames = {
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
DXDEV: "dxdev",
SESSION_ID: "sid",
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 { applicationConfig } from "@/constants/application-config";
@ -78,11 +78,43 @@ export function getDeviceIdValue() {
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.
*/
export function getSessionKeyValue() {
const cookieValue = getCookieValueByName(cookieNames.SESSION_KEY);
const cookieValue = getCookieValueByName(cookieNames.FUNNEL_SESSION_KEY);
if (cookieValue) {
return cookieValue;
@ -91,6 +123,17 @@ export function getSessionKeyValue() {
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.
*/
@ -104,11 +147,24 @@ export function getSessionIdValue() {
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
*/
export function updateSessionIdCookie() {
createOrUpdateCookie(cookieNames.SESSION_ID, getSessionIdValue(), { maxAge: 60 * 30 });
createOrUpdateCookie(cookieNames.SESSION_ID, getSessionIdValue(), {
maxAge: cookieExpirations.SESSION_ID,
});
}
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 =

View file

@ -100,7 +100,8 @@ export const cookies = {
someOtherCookie: "{}",
dxdev: "did=21b9b94a-ec23-42c1-aaac-e2ae4e4dbffe",
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

View file

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

View file

@ -11,8 +11,20 @@ import {
} from "@/constants/analytics";
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", () => {
test("logPageView: calls dispatch with type and payload", () => {
test("logPageView: calls dispatch with type and payload", async () => {
const type = "";
const payload = {};
@ -21,6 +33,9 @@ describe("analyticsMixin.js", () => {
{
actionName: storeActions.LOG_PAGE_VIEW,
},
{
actionName: storeActions.INITIALIZE_SESSION,
},
],
};
const mocks = setupMocksForJsFiles(mockData);
@ -31,27 +46,35 @@ describe("analyticsMixin.js", () => {
setupCookies({ funnelCookieValue: JSON.stringify(testCookieValue) });
analyticsMixin.methods.logPageView(type, payload);
await analyticsMixin.methods.logPageView(type, payload);
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 = {
actionList: [
{
actionName: storeActions.LOG_CUSTOM_EVENT,
},
{
actionName: storeActions.INITIALIZE_SESSION,
},
],
};
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();
});
test("pushEventToGA, should call dataLayer push and logCustomEvent too", () => {
test("pushEventToGA, should call dataLayer push and logCustomEvent too", async () => {
// Arrange
window.dataLayer = [];
const mockData = {
@ -59,6 +82,9 @@ describe("analyticsMixin.js", () => {
{
actionName: storeActions.LOG_CUSTOM_EVENT,
},
{
actionName: storeActions.INITIALIZE_SESSION,
},
],
};
const mocks = setupMocksForJsFiles(mockData);
@ -73,14 +99,14 @@ describe("analyticsMixin.js", () => {
});
// Act
analyticsMixin.methods.pushEventToGA("category", "action", "label", true);
await analyticsMixin.methods.pushEventToGA("category", "action", "label", true);
// Assert
expect(mockDataLayer).toEqual(expect.arrayContaining(window.dataLayer));
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
window.dataLayer = [];
var expectedDataLayer = [];
@ -93,8 +119,21 @@ describe("analyticsMixin.js", () => {
path: "/fmg/?fmgPage=",
});
const mockData = {
actionList: [
{
actionName: storeActions.LOG_CUSTOM_EVENT,
},
{
actionName: storeActions.INITIALIZE_SESSION,
},
],
};
const mocks = setupMocksForJsFiles(mockData);
// Act
analyticsMixin.methods.pushEventToGA(
await analyticsMixin.methods.pushEventToGA(
"category",
"action",
"1111122222333333",
@ -106,7 +145,7 @@ describe("analyticsMixin.js", () => {
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
window.dataLayer = [];
var expectedDataLayer = [];
@ -119,8 +158,21 @@ describe("analyticsMixin.js", () => {
path: "/fmg/?fmgPage=",
});
const mockData = {
actionList: [
{
actionName: storeActions.LOG_CUSTOM_EVENT,
},
{
actionName: storeActions.INITIALIZE_SESSION,
},
],
};
const mocks = setupMocksForJsFiles(mockData);
// Act
analyticsMixin.methods.pushEventToGA(
await analyticsMixin.methods.pushEventToGA(
"category",
"action",
"111",

View file

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

View file

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