Merge branch 'develop' into feature/SSR-113
This commit is contained in:
commit
2c215e23a9
27 changed files with 16582 additions and 106 deletions
15261
package-lock.json
generated
15261
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,5 +1,5 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<script>
|
||||
window.dataLayer = [{}];
|
||||
|
|
|
|||
|
|
@ -13,4 +13,4 @@
|
|||
@import "@/styles/common-typography-styles.scss";
|
||||
@import "@/styles/common-error-styles.scss";
|
||||
@import "@/styles/common-animations.scss";
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
36
src/constants/analytics.js
Normal file
36
src/constants/analytics.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
const analyticsPageEvents = {
|
||||
ENTRY: "ENTRY",
|
||||
EVENT: "EVENT",
|
||||
};
|
||||
|
||||
// GA Constants
|
||||
const GaEvents = {
|
||||
GENERIC_EVENT: "event",
|
||||
PAGE_VIEW_EVENT: "logPageview",
|
||||
};
|
||||
|
||||
const GaCategories = {
|
||||
API_RESPONSE: "Api_Response",
|
||||
EVOX: "Evox",
|
||||
};
|
||||
|
||||
const GaActions = {
|
||||
RESULT: "Result",
|
||||
CLICKED: "Clicked",
|
||||
VIF: "vif",
|
||||
SUBMITTED: "Submitted",
|
||||
};
|
||||
|
||||
const GaLabels = {
|
||||
SUCCESS: "Success",
|
||||
ERROR: "Error",
|
||||
LICENSE_PLATE_LOOKUP: "License_Plate_Look_Up",
|
||||
VIN_LOOKUP: "Vin_Look_Up",
|
||||
ADDRESS_LOOKUP: "Address_Look_up",
|
||||
};
|
||||
|
||||
const ValueToLogTypes = {
|
||||
LAST_5: "last_5",
|
||||
};
|
||||
|
||||
export { analyticsPageEvents, GaCategories, GaActions, GaLabels, GaEvents, ValueToLogTypes };
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
const applicationConfig = {
|
||||
CURRENT_ENVIRONMENT: process.env.VUE_APP_CURRENT_ENVIRONMENT, // "Localhost", "Dev", "QA", and "Prod"
|
||||
CONSUMER_CF_DISTRO: process.env.VUE_APP_CONSUMER_CF_DISTRO,
|
||||
ANALYTICS_SESSION_TIMEOUT_MINUTES: 30,
|
||||
SAVED_SESSION_TIMEOUT_DAYS: 45,
|
||||
COOKIE_PATH: "/",
|
||||
APPLICATION_NAME: "SelfService",
|
||||
SITE_ENTRY_TRIGGER_VALUE: "SelfService",
|
||||
APPLICATION_ABBREVIATION: "iss",
|
||||
|
|
|
|||
12
src/constants/cookie-names.js
Normal file
12
src/constants/cookie-names.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { applicationConfig } from "@/constants/application-config.js";
|
||||
|
||||
const cookieNames = {
|
||||
ISS_SESSION_INFO: `ISSSessionInfo-${applicationConfig.CURRENT_ENVIRONMENT}`,
|
||||
|
||||
// Existing Safelite.com cookies
|
||||
DXDEV: "dxdev",
|
||||
SESSION_ID: "sid",
|
||||
SESSION_KEY: "skey",
|
||||
};
|
||||
|
||||
export { cookieNames };
|
||||
|
|
@ -31,6 +31,30 @@ const endpoints = {
|
|||
url: "/parts/api/v1/parts/damage-options",
|
||||
method: "GET",
|
||||
},
|
||||
LogExperimentExposureIfAssigned: {
|
||||
url: "/experiments/api/v1/experiments/log-exposure",
|
||||
method: "POST",
|
||||
},
|
||||
LogPageView: {
|
||||
url: "/analytics/api/v1/analytics/log-page-view",
|
||||
method: "POST",
|
||||
},
|
||||
LogCustomEvent: {
|
||||
url: "/analytics/api/v1/analytics/log-custom-event",
|
||||
method: "POST",
|
||||
},
|
||||
InitializeSession: {
|
||||
url: "/analytics/api/v1/analytics/initialize",
|
||||
method: "POST",
|
||||
},
|
||||
GetExperimentsByUser: {
|
||||
url: "/analytics/api/v1/analytics/get-experiments",
|
||||
method: "GET",
|
||||
},
|
||||
RunExperimentsForTrigger: {
|
||||
url: "/experiments/api/v1/experiments/run",
|
||||
method: "POST",
|
||||
},
|
||||
};
|
||||
|
||||
export { endpoints };
|
||||
|
|
|
|||
14
src/constants/experiments.js
Normal file
14
src/constants/experiments.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
const experimentUniverses = {
|
||||
ISS_FUNNEL: "ISSFunnel",
|
||||
};
|
||||
|
||||
const experimentSettings = {
|
||||
GOOGLE_CUSTOM_DIMENSION_INDEX: "Google Custom Dimension Index",
|
||||
};
|
||||
|
||||
const experimentTriggers = {
|
||||
SITE_ENTRY: "SiteEntry",
|
||||
PAGE_ENTRY: "PageEntry",
|
||||
};
|
||||
|
||||
export { experimentUniverses, experimentSettings, experimentTriggers };
|
||||
3
src/constants/header-keys.js
Normal file
3
src/constants/header-keys.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export const headerKeys = {
|
||||
EXPERIMENT: "X-Experiment-Data",
|
||||
};
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
const storeActions = {
|
||||
// Content Actions
|
||||
GET_ROUTE_INFO_ACTION: "getRouteInfo",
|
||||
GET_HOMEPAGE_NAME: "getHomepageName",
|
||||
GET_PAGE_DATA: "getPageData",
|
||||
};
|
||||
|
||||
export { storeActions };
|
||||
|
||||
|
|
@ -1,14 +1,39 @@
|
|||
import axios from "axios";
|
||||
import analyticsMixIn from "@/mixins/analytics-mixin.js";
|
||||
import { useMainStore } from "@/store";
|
||||
|
||||
import { applicationConfig } from "@/constants/application-config.js";
|
||||
import { GaCategories, GaActions, GaLabels } from "@/constants/analytics";
|
||||
import { headerKeys } from "@/constants/header-keys";
|
||||
|
||||
export default {
|
||||
callHttpClient({ method, endpoint, payload}) {
|
||||
callHttpClient({ method, endpoint, payload, logApiCall = true}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const store = useMainStore();
|
||||
const cfDistroUrl = applicationConfig.CONSUMER_CF_DISTRO;
|
||||
const payloadAndAnalyticsData = Object.assign({}, payload, { AppName: "SelfService" });
|
||||
const headers = {
|
||||
[headerKeys.EXPERIMENT]: JSON.stringify(store.experimentSettings),
|
||||
};
|
||||
|
||||
axios({ method: method, url: cfDistroUrl + endpoint, data: payloadAndAnalyticsData, crossDomain: true, responseType: {}})
|
||||
axios({
|
||||
method: method,
|
||||
url: cfDistroUrl + endpoint,
|
||||
data: payloadAndAnalyticsData,
|
||||
crossDomain: true,
|
||||
responseType: {},
|
||||
headers: headers,
|
||||
})
|
||||
.then((response) => {
|
||||
if (logApiCall) {
|
||||
analyticsMixIn.methods.pushEventToGA(
|
||||
GaCategories.API_RESPONSE,
|
||||
GaActions.RESULT,
|
||||
`${GaLabels.SUCCESS}_${endpoint}`,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
return resolve(response);
|
||||
},
|
||||
error => {
|
||||
|
|
@ -22,7 +47,12 @@ export default {
|
|||
//used for mocked services
|
||||
async mockCallHttpClient(method, endpoint) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios({ method: method, url: endpoint, crossDomain: true, responseType: {}})
|
||||
axios({
|
||||
method: method,
|
||||
url: endpoint,
|
||||
crossDomain: true,
|
||||
responseType: {}
|
||||
})
|
||||
.then((response) => {
|
||||
return resolve(response);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
import axios from 'axios';
|
||||
import globalMethods from "@/global-methods";
|
||||
import analyticsMixIn from "@/mixins/analytics-mixin";
|
||||
import { getMountOptions } from "@/helpers/unit-test-helper.js";
|
||||
|
||||
//Mock external dependencies
|
||||
jest.mock("axios");
|
||||
jest.mock("@/mixins/analytics-mixin");
|
||||
|
||||
it("Global Methods - Call Http Client - Should Resolve Promise", () => {
|
||||
//Arrange
|
||||
|
|
@ -25,6 +28,7 @@ it("Global Methods - Call Http Client - Should Resolve Promise", () => {
|
|||
endpoint: endpoint,
|
||||
isError: true,
|
||||
});
|
||||
analyticsMixIn.methods.pushEventToGA = jest.fn();
|
||||
|
||||
//Act
|
||||
globalMethods.callHttpClient(httpArgs).catch((err) => {
|
||||
|
|
@ -42,6 +46,8 @@ it("Global Methods - Call Http Client - Should Resolve Promise", () => {
|
|||
}) {
|
||||
//Clear node module
|
||||
axios.mockClear();
|
||||
|
||||
const mountOptions = getMountOptions();
|
||||
|
||||
// Success Response
|
||||
const response = {
|
||||
|
|
|
|||
205
src/helpers/cookie-helper.js
Normal file
205
src/helpers/cookie-helper.js
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
import { cookieNames } from "@/constants/cookie-names";
|
||||
import { applicationConfig } from "@/constants/application-config";
|
||||
import { useMainStore } from "@/store";
|
||||
|
||||
/*
|
||||
Will update the cookie if present, or create a new one if not.
|
||||
*/
|
||||
export function updateOrCreateISSCookie() {
|
||||
const store = useMainStore();
|
||||
|
||||
// Set up cookie with all the props.
|
||||
setISSCookieProperties({
|
||||
LastTouched: new Date().toUTCString(),
|
||||
SavedSessionTimeoutDate: store.applicationUser.savedSessionTimeout,
|
||||
ShouldResetState: false,
|
||||
ReferralNumber: store.order.referralNumber,
|
||||
ReferralDate: store.order.referralDate,
|
||||
ReferralCorrelationId: store.order.referralCorrelationId,
|
||||
ReferralParentAccountNumber: store.order.accountNumber,
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
Gets the current instance of the ISS cookie.
|
||||
Returns null if cookie isn't valid JSON.
|
||||
*/
|
||||
export function getISSCookie() {
|
||||
const cookieJson = document.cookie
|
||||
?.split("; ")
|
||||
?.find((row) => row.startsWith(`${cookieNames.ISS_SESSION_INFO}=`))
|
||||
?.split("=")[1];
|
||||
|
||||
try {
|
||||
return JSON.parse(cookieJson);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Removes cookie from browser.
|
||||
*/
|
||||
export function deleteISSCookie() {
|
||||
createOrUpdateCookie(cookieNames.ISS_SESSION_INFO, undefined, { maxAge: 0 });
|
||||
}
|
||||
|
||||
/*
|
||||
Gets cookie domain value. Localhost will be empty "".
|
||||
*/
|
||||
export function getCookieDomainValue() {
|
||||
return isLocalhost() ? "" : `domain=${getDomainWithoutSubdomain()};`;
|
||||
}
|
||||
|
||||
/*
|
||||
Gets value of dxdev cookie, and then extracts "did" value from it.
|
||||
Returns empty string if cookie not found or "did" string not present.
|
||||
*/
|
||||
export function getDeviceIdValue() {
|
||||
// Sometimes these cookie contains more than the device ID.
|
||||
const cookieValue = getCookieValueByName(cookieNames.DXDEV);
|
||||
const cookieValuesSplit = cookieValue.split("=");
|
||||
|
||||
// If this is the only value, just use that.
|
||||
if (cookieValuesSplit.length === 2 && cookieValuesSplit[0] === "did") {
|
||||
return cookieValuesSplit[1];
|
||||
}
|
||||
|
||||
const cookieValueMatch = cookieValue.match("^did=[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}");
|
||||
if (cookieValueMatch) {
|
||||
return cookieValueMatch[0].split("=")[1];
|
||||
}
|
||||
|
||||
return "00000000-0000-0000-0000-000000000000";
|
||||
}
|
||||
|
||||
/*
|
||||
Gets value of skey cookie, returns 0 if not found.
|
||||
*/
|
||||
export function getSessionKeyValue() {
|
||||
const cookieValue = getCookieValueByName(cookieNames.SESSION_KEY);
|
||||
|
||||
if (cookieValue) {
|
||||
return cookieValue;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
Gets value of skey cookie, returns 0 if not found.
|
||||
*/
|
||||
export function getSessionIdValue() {
|
||||
const cookieValue = getCookieValueByName(cookieNames.SESSION_ID);
|
||||
|
||||
if (cookieValue) {
|
||||
return cookieValue;
|
||||
}
|
||||
|
||||
return "00000000-0000-0000-0000-000000000000";
|
||||
}
|
||||
|
||||
/*
|
||||
Updates session ID cookie with new expiration date
|
||||
*/
|
||||
export function updateSessionIdCookie() {
|
||||
createOrUpdateCookie(cookieNames.SESSION_ID, getSessionIdValue(), { maxAge: 60 * 30 });
|
||||
}
|
||||
|
||||
export function setCookieProperties(
|
||||
properties,
|
||||
{ useDefaultISSCookieAttributes = true, maxAge, isSecure }
|
||||
) {
|
||||
if (typeof properties == "object") {
|
||||
Object.keys(properties).forEach((key) => {
|
||||
createOrUpdateCookie(key, properties[key], {
|
||||
useDefaultISSCookieAttributes,
|
||||
maxAge,
|
||||
isSecure,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
===========================
|
||||
= PRIVATE FUNCTIONS =
|
||||
===========================
|
||||
*/
|
||||
|
||||
/*
|
||||
Used to set properties on the ISS cookie.
|
||||
Takes an object with properties to set. Will overwrite existing properties.
|
||||
*/
|
||||
function setISSCookieProperties(properties) {
|
||||
if (typeof properties == "object") {
|
||||
let cookie = getISSCookie();
|
||||
|
||||
if (cookie !== null) {
|
||||
Object.keys(properties).forEach((key) => {
|
||||
cookie[key] = properties[key];
|
||||
});
|
||||
}
|
||||
|
||||
const cookieValueJson = JSON.stringify(cookie ?? {});
|
||||
createOrUpdateCookie(cookieNames.ISS_SESSION_INFO, cookieValueJson, {});
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Used to create a cookie.
|
||||
`useDefaultISSCookieAttributes` will set the path and domain to our defaults
|
||||
*/
|
||||
function createOrUpdateCookie(
|
||||
key,
|
||||
value = "",
|
||||
{ useDefaultISSCookieAttributes = true, maxAge, isSecure = true }
|
||||
) {
|
||||
let cookieToAdd = `${key}=${value}; `;
|
||||
|
||||
if (useDefaultISSCookieAttributes) {
|
||||
cookieToAdd += `path=${applicationConfig.COOKIE_PATH}; ${getCookieDomainValue()} `;
|
||||
}
|
||||
if (isSecure && !isLocalhost()) {
|
||||
cookieToAdd += `secure; `;
|
||||
}
|
||||
if (!isNaN(maxAge)) {
|
||||
cookieToAdd += `max-age=${maxAge};`;
|
||||
}
|
||||
|
||||
document.cookie = cookieToAdd;
|
||||
}
|
||||
|
||||
/*
|
||||
Gets current domain without the subdomain for cookie.
|
||||
*/
|
||||
function getDomainWithoutSubdomain() {
|
||||
let url = location.hostname;
|
||||
if (isLocalhost()) {
|
||||
return "localhost";
|
||||
}
|
||||
|
||||
const urlParts = url.split(".");
|
||||
|
||||
return `.${urlParts
|
||||
.slice(0)
|
||||
.slice(-(urlParts.length === 4 ? 3 : 2))
|
||||
.join(".")}`;
|
||||
}
|
||||
|
||||
/*
|
||||
Gets cookie value by name, returns empty string if not found.
|
||||
*/
|
||||
function getCookieValueByName(name) {
|
||||
const value = "; " + document.cookie;
|
||||
const parts = value.split("; " + name + "=");
|
||||
|
||||
if (parts.length === 2) {
|
||||
return parts.pop().split(";").shift();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function isLocalhost() {
|
||||
return location.hostname.includes("localhost");
|
||||
}
|
||||
48
src/helpers/session-helper.js
Normal file
48
src/helpers/session-helper.js
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { applicationConfig } from "@/constants/application-config";
|
||||
import { getISSCookie } from "@/helpers/cookie-helper.js";
|
||||
|
||||
/*
|
||||
Method to determine if our analytics session has timed out or not.
|
||||
Amount used for timeout is configurable in application-config.js
|
||||
*/
|
||||
export function isAnalyticsSessionStillActive() {
|
||||
if (getISSCookie() !== null) {
|
||||
const lastTouchedValue = getISSCookie().LastTouched;
|
||||
const timeoutAmount = applicationConfig.ANALYTICS_SESSION_TIMEOUT_MINUTES;
|
||||
|
||||
const isMoreThanHalfHourAgo =
|
||||
(new Date() - new Date(lastTouchedValue)) / 60000 > timeoutAmount;
|
||||
|
||||
if (isMoreThanHalfHourAgo) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Method to determine if the users 'saved' session is still active.
|
||||
When user state is created, there is a date that is saved into state
|
||||
this method checks against that date.
|
||||
|
||||
Note: That time for saved session timeout is configurable in application-config.js
|
||||
*/
|
||||
export function isSavedSessionStillActive() {
|
||||
if (getISSCookie() !== null) {
|
||||
const savedSessionTimeStamp = new Date(getISSCookie().SavedSessionTimeoutDate);
|
||||
const isSavedSessionTimedOut = new Date(new Date().toUTCString()) > savedSessionTimeStamp;
|
||||
|
||||
return !isSavedSessionTimedOut;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Function to calculate the date for the saved session timeout.
|
||||
*/
|
||||
|
||||
export function getDateForSavedSessionTimeout() {
|
||||
const currentDate = new Date(new Date().toUTCString());
|
||||
currentDate.setDate(currentDate.getDate() + applicationConfig.SAVED_SESSION_TIMEOUT_DAYS);
|
||||
return currentDate.toUTCString();
|
||||
}
|
||||
89
src/helpers/session-helper.spec.js
Normal file
89
src/helpers/session-helper.spec.js
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import * as cookieHelper from "@/helpers/cookie-helper";
|
||||
import {
|
||||
isAnalyticsSessionStillActive,
|
||||
isSavedSessionStillActive,
|
||||
getDateForSavedSessionTimeout,
|
||||
} from "@/helpers/session-helper";
|
||||
import { applicationConfig } from "@/constants/application-config";
|
||||
|
||||
describe("isAnalyticsSessionStillActive", () => {
|
||||
test("isAnalyticsSessionStillActive, should return true", () => {
|
||||
// Arrange
|
||||
const mockDate = new Date(new Date().toUTCString());
|
||||
mockDate.setDate(mockDate.getDate() + 1);
|
||||
|
||||
cookieHelper.getISSCookie = jest
|
||||
.spyOn(cookieHelper, "getISSCookie")
|
||||
.mockReturnValue({ LastTouched: mockDate });
|
||||
|
||||
// Act
|
||||
const result = isAnalyticsSessionStillActive();
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("isAnalyticsSessionStillActive, should return false", () => {
|
||||
// Arrange
|
||||
const mockDate = new Date(new Date().toUTCString());
|
||||
mockDate.setDate(mockDate.getDate() - 1);
|
||||
|
||||
cookieHelper.getISSCookie = jest
|
||||
.spyOn(cookieHelper, "getISSCookie")
|
||||
.mockReturnValue({ LastTouched: mockDate });
|
||||
|
||||
// Act
|
||||
const result = isAnalyticsSessionStillActive();
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isSavedSessionStillActive", () => {
|
||||
test("isSavedSessionStillActive, should return true", () => {
|
||||
// Arrange
|
||||
const mockDate = new Date(new Date().toUTCString());
|
||||
mockDate.setDate(mockDate.getDate() + 1);
|
||||
|
||||
cookieHelper.getISSCookie = jest
|
||||
.spyOn(cookieHelper, "getISSCookie")
|
||||
.mockReturnValue({ SavedSessionTimeoutDate: mockDate });
|
||||
|
||||
// Act
|
||||
const result = isSavedSessionStillActive();
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("isSavedSessionStillActive, should return false", () => {
|
||||
// Arrange
|
||||
const mockDate = new Date(new Date().toUTCString());
|
||||
mockDate.setDate(mockDate.getDate() - 1);
|
||||
|
||||
cookieHelper.getISSCookie = jest
|
||||
.spyOn(cookieHelper, "getISSCookie")
|
||||
.mockReturnValue({ SavedSessionTimeoutDate: mockDate });
|
||||
|
||||
// Act
|
||||
const result = isSavedSessionStillActive();
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getDateForSavedSessionTimeout", () => {
|
||||
test("getDateForSavedSessionTimeout, should equal application config setting", () => {
|
||||
// Arrange
|
||||
const currentDate = new Date(new Date().toUTCString());
|
||||
currentDate.setDate(currentDate.getDate() + applicationConfig.SAVED_SESSION_TIMEOUT_DAYS);
|
||||
|
||||
// Act
|
||||
const result = getDateForSavedSessionTimeout();
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual(currentDate.toUTCString());
|
||||
});
|
||||
});
|
||||
|
|
@ -1,8 +1,22 @@
|
|||
import { storeActions } from "@/constants/store-actions";
|
||||
import { navigationScenarios } from "@/router/router-constants/navigation-scenarios.js";
|
||||
import { vehicleCategories } from "@/constants/vehicle-categories.js";
|
||||
import { issPageValues } from "@/router/router-constants/issPage-values";
|
||||
import { cookieNames } from "@/constants/cookie-names";
|
||||
import { Form } from "vee-validate";
|
||||
import baseMixin from "@/mixins/base-mixin";
|
||||
import {
|
||||
getCookieDomainValue,
|
||||
setCookieProperties,
|
||||
} from "@/helpers/cookie-helper";
|
||||
import {
|
||||
analyticsPageEvents,
|
||||
GaCategories,
|
||||
GaActions,
|
||||
GaLabels,
|
||||
GaEvents,
|
||||
ValueToLogTypes,
|
||||
} from "@/constants/analytics";
|
||||
import { routerParams } from "@/router/router-constants/router-params";
|
||||
import { queryStrings } from "@/constants/query-strings";
|
||||
import { useMainStore } from "@/store";
|
||||
import { mapStores } from "pinia";
|
||||
|
|
@ -20,27 +34,142 @@ const mockMixin = {
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
// Common methods
|
||||
export function getMountOptions(mockData) {
|
||||
// Define our mocks to attached to the 'global' object for Vue/Jest.
|
||||
const mocks = {};
|
||||
|
||||
|
||||
// Mock const files
|
||||
mocks.storeActions = storeActions;
|
||||
mocks.navigationScenarios = navigationScenarios;
|
||||
mocks.vehicleCategories = vehicleCategories;
|
||||
mocks.issPageValues = issPageValues;
|
||||
mocks.queryStrings = queryStrings;
|
||||
mocks.$router = mockData?.router;
|
||||
|
||||
const global = {
|
||||
mocks: mocks,
|
||||
mixins: [mockMixin],
|
||||
plugins: [pinia],
|
||||
stubs: { Form }
|
||||
};
|
||||
|
||||
return { global };
|
||||
// Define our mocks to attached to the 'global' object for Vue/Jest.
|
||||
const mocks = {};
|
||||
|
||||
|
||||
// Mock const files
|
||||
mocks.navigationScenarios = navigationScenarios;
|
||||
mocks.vehicleCategories = vehicleCategories;
|
||||
mocks.issPageValues = issPageValues;
|
||||
mocks.queryStrings = queryStrings;
|
||||
mocks.$router = mockData?.router;
|
||||
|
||||
const global = {
|
||||
mocks: mocks,
|
||||
mixins: [mockMixin],
|
||||
plugins: [pinia],
|
||||
stubs: { Form }
|
||||
};
|
||||
|
||||
return { global };
|
||||
}
|
||||
|
||||
// Heritage integration common methods
|
||||
export const cookies = {
|
||||
[cookieNames.ISS_SESSION_INFO]: `{"ReferralNumber":"1566818","ReferralDate":"2022-03-15T10:56:24.597","ReferralCorrelationId":"404d2b04-f86e-45c3-b373-127b6217b060","ShouldResetState":false}`,
|
||||
UNIQUE_SESSION_ID: "33756020-b58e-4ec7-b8b8-3f1576719c40",
|
||||
anotherCookie: "{}",
|
||||
someOtherCookie: "{}",
|
||||
dxdev: "did=21b9b94a-ec23-42c1-aaac-e2ae4e4dbffe",
|
||||
sid: "cba0c3d1-3c1b-4305-bb56-31aa50f58e27",
|
||||
skey: "12345",
|
||||
};
|
||||
|
||||
export function setupCookies({ ISSCookieValue = "", includeHeritageCookie = true }) {
|
||||
Object.keys(cookies).forEach((key) => {
|
||||
const cookieValue =
|
||||
key == cookieNames.ISS_SESSION_INFO ? ISSCookieValue : cookies[key];
|
||||
if (includeHeritageCookie || key != cookieNames.ISS_SESSION_INFO)
|
||||
setCookieProperties({ [key]: cookieValue }, { isSecure: false });
|
||||
});
|
||||
}
|
||||
|
||||
// Removes test cookies for testing cookie-helper and order-helper
|
||||
export function removeAllTestCookies() {
|
||||
Object.keys(cookies).forEach((key) => {
|
||||
document.cookie = `${key}=;Max-Age=0;`;
|
||||
document.cookie = `${key}=;Max-Age=0;path=/;${getCookieDomainValue()}`;
|
||||
});
|
||||
}
|
||||
|
||||
export function setupMocksForJsFiles() {
|
||||
return { baseMixin };
|
||||
}
|
||||
|
||||
// Private methods
|
||||
|
||||
/*
|
||||
// Common methods
|
||||
export function getMountOptions(mockData) {
|
||||
// Define our mocks to attached to the 'global' object for Vue/Jest.
|
||||
const mocks = {};
|
||||
|
||||
//this is mocking if you use the mixin directly(baseMixin.methods.dispatchStoreAction) vs this.dispatchStoreAction
|
||||
setupBaseMixinDispatchStoreAction(mockData);
|
||||
|
||||
mocks.pushEventToGA = jest.fn();
|
||||
mocks.pushPageViewToGA = jest.fn();
|
||||
mocks.logEvent = jest.fn();
|
||||
mocks.pushExperimentsToDataLayer = jest.fn();
|
||||
mocks.prependActionToMethod = jest.fn();
|
||||
mocks.dispatchStoreAction = jest.fn();
|
||||
mocks.dispatchStoreAction.mockImplementation((actionName) => {
|
||||
let actionFilterResult = mockData.actionList?.filter((x) => x.actionName == actionName);
|
||||
|
||||
if (actionFilterResult?.length === 1) {
|
||||
return Promise.resolve({
|
||||
data: actionFilterResult[0].data,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Mock const files
|
||||
mocks.storeActions = storeActions;
|
||||
mocks.storeMutations = storeMutations;
|
||||
mocks.navigationScenarios = navigationScenarios;
|
||||
mocks.vehicleCategories = vehicleCategories;
|
||||
mocks.issPageValues = issPageValues;
|
||||
mocks.analyticsPageEvents = analyticsPageEvents;
|
||||
mocks.GaCategories = GaCategories;
|
||||
mocks.GaActions = GaActions;
|
||||
mocks.GaLabels = GaLabels;
|
||||
mocks.GaEvents = GaEvents;
|
||||
mocks.ValueToLogTypes = ValueToLogTypes;
|
||||
mocks.queryStrings = queryStrings;
|
||||
mocks.routerParams = routerParams;
|
||||
|
||||
// Mock $store and $router when accessing this.$store/$router
|
||||
mocks.$store = mockData.store;
|
||||
mocks.$router = mockData.router;
|
||||
mocks.$route = mockData.route;
|
||||
mocks.$loadScript = mockData.loadScript;
|
||||
|
||||
const global = {
|
||||
mocks: mocks,
|
||||
mixins: mockData.mixins,
|
||||
plugins: [pinia],
|
||||
stubs: { Form }
|
||||
};
|
||||
|
||||
return { global };
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export function getMockOrderInfo(
|
||||
mockReferralNumber,
|
||||
mockCorrelationId,
|
||||
mockReferralDate,
|
||||
accountNumber = "0",
|
||||
savedSessionId,
|
||||
crmCustomerId
|
||||
) {
|
||||
return {
|
||||
referralNumber: mockReferralNumber,
|
||||
referralCorrelationId: mockCorrelationId,
|
||||
referralDate: mockReferralDate,
|
||||
accountNumber: accountNumber,
|
||||
savedSessionId: savedSessionId,
|
||||
crmCustomerId: crmCustomerId,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
*/
|
||||
|
|
@ -30,7 +30,6 @@ import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
|
|||
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||
import { settleAllPromises } from "@/helpers/layout-helper";
|
||||
|
||||
|
||||
export default {
|
||||
name: "vehicle-style",
|
||||
data() {
|
||||
|
|
|
|||
18
src/main.js
18
src/main.js
|
|
@ -8,6 +8,10 @@ import baseMixin from "@/mixins/base-mixin.js";
|
|||
import { createPinia } from 'pinia';
|
||||
import Maska from "maska";
|
||||
|
||||
import analyticsMixin from "@/mixins/analytics-mixin.js";
|
||||
import experimentMixin from "@/mixins/experiment-mixin.js";
|
||||
|
||||
// Vue App Setup
|
||||
const vueApp = createApp(App);
|
||||
|
||||
// Pinia
|
||||
|
|
@ -16,14 +20,12 @@ vueApp.use(pinia);
|
|||
pinia.use(piniaPluginPersistedstate);
|
||||
useMainStore().populateInitialState();
|
||||
|
||||
|
||||
// Additional Vue items to setup
|
||||
vueApp.mixin(baseMixin);
|
||||
vueApp.use(Maska);
|
||||
vueApp.use(router);
|
||||
vueApp.use(Maska);
|
||||
vueApp.mixin(baseMixin);
|
||||
vueApp.mixin(analyticsMixin);
|
||||
vueApp.mixin(experimentMixin);
|
||||
|
||||
vueApp.mount("#app");
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
205
src/mixins/analytics-mixin.js
Normal file
205
src/mixins/analytics-mixin.js
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
import {
|
||||
setCookieProperties,
|
||||
getDeviceIdValue,
|
||||
getSessionIdValue,
|
||||
getSessionKeyValue,
|
||||
} from "@/helpers/cookie-helper";
|
||||
import { queryStrings } from "@/constants/query-strings";
|
||||
import { experimentSettings } from "@/constants/experiments";
|
||||
import {
|
||||
analyticsPageEvents,
|
||||
GaCategories,
|
||||
GaActions,
|
||||
GaLabels,
|
||||
GaEvents,
|
||||
ValueToLogTypes,
|
||||
} from "@/constants/analytics";
|
||||
import { cookieNames } from "@/constants/cookie-names";
|
||||
import { useMainStore } from "@/store";
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
logPageView(pageEvent) {
|
||||
const currentPageName = getPageNameByQueryString();
|
||||
var payload = {
|
||||
userId: getDeviceIdValue(),
|
||||
sessionKey: getSessionKeyValue(),
|
||||
pageName: currentPageName,
|
||||
sessionId: getSessionIdValue(),
|
||||
action: "",
|
||||
event: pageEvent,
|
||||
shouldUseSessionId: false,
|
||||
experimentsForUser: useMainStore().applicationUser.experiments,
|
||||
};
|
||||
|
||||
useMainStore().logPageView(payload);
|
||||
},
|
||||
|
||||
logCustomEvent(category, action, label, value) {
|
||||
const currentPageName = getPageNameByQueryString();
|
||||
|
||||
var payload = {
|
||||
userId: getDeviceIdValue(),
|
||||
sessionKey: getSessionKeyValue(),
|
||||
pageName: currentPageName,
|
||||
sessionId: getSessionIdValue(),
|
||||
category: category,
|
||||
action: action,
|
||||
label: label,
|
||||
value: value,
|
||||
shouldUseSessionId: false,
|
||||
experimentsForUser: useMainStore().applicationUser.experiments,
|
||||
};
|
||||
|
||||
useMainStore().logCustomEvent(payload);
|
||||
},
|
||||
|
||||
pushEventToGA(category, action, label, pushToLogApp = false, valueToLogType = null) {
|
||||
const currentPageName = getPageNameByQueryString();
|
||||
const labelToLog = getValueToLog(label, valueToLogType);
|
||||
|
||||
const eventToBePushed = {
|
||||
event: GaEvents.GENERIC_EVENT,
|
||||
category: category,
|
||||
action: action,
|
||||
label: labelToLog,
|
||||
value: undefined,
|
||||
path: `/iss/?${queryStrings.ISS_PAGE}=${currentPageName}`,
|
||||
};
|
||||
|
||||
pushToDataLayerIfDefined(eventToBePushed);
|
||||
|
||||
if (pushToLogApp) {
|
||||
this.logCustomEvent(category, action, labelToLog, undefined);
|
||||
}
|
||||
},
|
||||
|
||||
pushPageViewToGA() {
|
||||
const currentPageName = getPageNameByQueryString();
|
||||
const pageViewEvent = {
|
||||
event: GaEvents.PAGE_VIEW_EVENT,
|
||||
pagePath: `/iss/?${queryStrings.ISS_PAGE}=${currentPageName}`,
|
||||
pageTitle: currentPageName,
|
||||
};
|
||||
|
||||
pushToDataLayerIfDefined(pageViewEvent);
|
||||
|
||||
this.logPageView(analyticsPageEvents.ENTRY);
|
||||
},
|
||||
|
||||
pushExperimentsToDataLayer() {
|
||||
const experiments = useMainStore().applicationUser.experiments;
|
||||
experiments?.forEach((exp) => {
|
||||
// Set Google Dimension Index based on experiment settings.
|
||||
let googleDimensionIndex = 99;
|
||||
|
||||
if (exp.settings[experimentSettings.GOOGLE_CUSTOM_DIMENSION_INDEX] !== undefined) {
|
||||
googleDimensionIndex =
|
||||
exp.settings[experimentSettings.GOOGLE_CUSTOM_DIMENSION_INDEX];
|
||||
}
|
||||
|
||||
// Create object with dimension index and value.
|
||||
const experimentWithDimension = {
|
||||
[`experimentId_${googleDimensionIndex}`]: exp.universeId,
|
||||
[`variationId_${googleDimensionIndex}`]: exp.variationId,
|
||||
[`experimentName_${googleDimensionIndex}`]: exp.universeName,
|
||||
[`variationName_${googleDimensionIndex}`]: exp.variationName,
|
||||
[`customDimension_${googleDimensionIndex}`]: `${exp.universeId}_${exp.variationId}_${exp.universeName}_${exp.variationName}`,
|
||||
};
|
||||
|
||||
// Push to the data layer with the Google Custom Dimension Index.
|
||||
pushToDataLayerIfDefined(experimentWithDimension);
|
||||
});
|
||||
},
|
||||
|
||||
prependActionToMethod(object, method, actionToPrepend) {
|
||||
const baseMethodName = method.name.startsWith("bound ")
|
||||
? method.name.substring(6)
|
||||
: method.name;
|
||||
const baseMethod = object[baseMethodName];
|
||||
object[baseMethodName] = function () {
|
||||
actionToPrepend.apply(this, arguments);
|
||||
return baseMethod.apply(object, arguments);
|
||||
};
|
||||
},
|
||||
|
||||
async initSession() {
|
||||
const sid = getSessionIdValue();
|
||||
const skey = getSessionKeyValue();
|
||||
var payload = {
|
||||
userId: getDeviceIdValue(),
|
||||
sessionId: sid,
|
||||
userAgent: navigator.userAgent,
|
||||
referrer: document.referrer,
|
||||
};
|
||||
|
||||
const response = await useMainStore().initializeSession(payload);
|
||||
|
||||
if (response.data) {
|
||||
if (response.data.sessionKey && skey === 0) {
|
||||
setCookieProperties(
|
||||
{ [cookieNames.SESSION_KEY]: response.data.sessionKey },
|
||||
{
|
||||
useDefaultFunnelCookieAttributes: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
if (response.data.sessionId && sid === "00000000-0000-0000-0000-000000000000") {
|
||||
setCookieProperties(
|
||||
{ [cookieNames.SESSION_ID]: response.data.sessionId },
|
||||
{
|
||||
maxAge: 60 * 30, // 30 minutes
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
noSession() {
|
||||
return (
|
||||
getSessionKeyValue() === 0 ||
|
||||
getSessionIdValue() === "00000000-0000-0000-0000-000000000000"
|
||||
);
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
analyticsPageEvents() {
|
||||
return analyticsPageEvents;
|
||||
},
|
||||
GaCategories() {
|
||||
return GaCategories;
|
||||
},
|
||||
GaActions() {
|
||||
return GaActions;
|
||||
},
|
||||
GaLabels() {
|
||||
return GaLabels;
|
||||
},
|
||||
ValueToLogTypes() {
|
||||
return ValueToLogTypes;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function pushToDataLayerIfDefined(data) {
|
||||
if (window.dataLayer !== undefined) {
|
||||
window.dataLayer.push(data);
|
||||
}
|
||||
}
|
||||
|
||||
function getPageNameByQueryString() {
|
||||
const params = new URLSearchParams(location.search);
|
||||
|
||||
if (params.has(queryStrings.ISS_PAGE)) {
|
||||
return params.get(queryStrings.ISS_PAGE);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function getValueToLog(value, valueToLogType) {
|
||||
if (valueToLogType != null && valueToLogType === ValueToLogTypes.LAST_5) {
|
||||
return value.slice(-5);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
280
src/mixins/analytics-mixin.spec.js
Normal file
280
src/mixins/analytics-mixin.spec.js
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
import analyticsMixin from "@/mixins/analytics-mixin";
|
||||
import { setupMocksForJsFiles, setupCookies } from "@/helpers/unit-test-helper.js";
|
||||
import {
|
||||
analyticsPageEvents,
|
||||
GaCategories,
|
||||
GaActions,
|
||||
GaLabels,
|
||||
GaEvents,
|
||||
ValueToLogTypes,
|
||||
} from "@/constants/analytics";
|
||||
import { useMainStore } from "@/store";
|
||||
|
||||
describe("analyticsMixin.js", () => {
|
||||
test("logPageView: calls dispatch with type and payload", () => {
|
||||
const type = "";
|
||||
const payload = {};
|
||||
|
||||
const mockData = {
|
||||
};
|
||||
const mocks = setupMocksForJsFiles(mockData);
|
||||
|
||||
const testCookieValue = {
|
||||
sid: "10000000-0000-0000-0000-000000000001",
|
||||
};
|
||||
|
||||
setupCookies({ funnelCookieValue: JSON.stringify(testCookieValue) });
|
||||
|
||||
useMainStore().logPageView(payload);
|
||||
|
||||
expect(useMainStore().logPageView).toBeCalled();
|
||||
});
|
||||
|
||||
test("logCustomEvent: calls dispatch with type and payload", () => {
|
||||
const mockData = {};
|
||||
const mocks = setupMocksForJsFiles(mockData);
|
||||
|
||||
useMainStore().logCustomEvent("someCat", "someAction", "someLabel", "someVal");
|
||||
|
||||
expect(useMainStore().logCustomEvent).toBeCalled();
|
||||
});
|
||||
|
||||
test("pushEventToGA, should call dataLayer push and logCustomEvent too", () => {
|
||||
// Arrange
|
||||
window.dataLayer = [];
|
||||
const mockData = {};
|
||||
const mocks = setupMocksForJsFiles(mockData);
|
||||
var mockDataLayer = [];
|
||||
mockDataLayer.push({
|
||||
event: "event",
|
||||
category: "category",
|
||||
action: "action",
|
||||
label: "label",
|
||||
value: undefined,
|
||||
path: "/iss/?issPage=",
|
||||
});
|
||||
|
||||
// Act
|
||||
analyticsMixin.methods.pushEventToGA("category", "action", "label", true);
|
||||
|
||||
// Assert
|
||||
expect(mockDataLayer).toEqual(expect.arrayContaining(window.dataLayer));
|
||||
});
|
||||
|
||||
test("pushEventToGA, should call dataLayer push and ValueToLogTypes.LAST_5 only logs last 5 of label", () => {
|
||||
// Arrange
|
||||
window.dataLayer = [];
|
||||
var expectedDataLayer = [];
|
||||
expectedDataLayer.push({
|
||||
event: "event",
|
||||
category: "category",
|
||||
action: "action",
|
||||
label: "33333",
|
||||
value: undefined,
|
||||
path: "/iss/?issPage=",
|
||||
});
|
||||
|
||||
// Act
|
||||
analyticsMixin.methods.pushEventToGA(
|
||||
"category",
|
||||
"action",
|
||||
"1111122222333333",
|
||||
false,
|
||||
ValueToLogTypes.LAST_5
|
||||
);
|
||||
|
||||
// Assert
|
||||
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", () => {
|
||||
// Arrange
|
||||
window.dataLayer = [];
|
||||
var expectedDataLayer = [];
|
||||
expectedDataLayer.push({
|
||||
event: "event",
|
||||
category: "category",
|
||||
action: "action",
|
||||
label: "111",
|
||||
value: undefined,
|
||||
path: "/iss/?issPage=",
|
||||
});
|
||||
|
||||
// Act
|
||||
analyticsMixin.methods.pushEventToGA(
|
||||
"category",
|
||||
"action",
|
||||
"111",
|
||||
false,
|
||||
ValueToLogTypes.LAST_5
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(expectedDataLayer).toEqual(expect.arrayContaining(window.dataLayer));
|
||||
});
|
||||
/*
|
||||
test("Experiments, should push to dataLayer with default Google Custom Dimension Index", () => {
|
||||
// Arrange
|
||||
window.dataLayer = [];
|
||||
|
||||
const mockExperimentData = [
|
||||
{
|
||||
settings: {},
|
||||
variationName: "test",
|
||||
universeName: "testUniverse",
|
||||
},
|
||||
];
|
||||
|
||||
// Mock store
|
||||
jest.mock(
|
||||
"@/store",
|
||||
() => {
|
||||
return {};
|
||||
},
|
||||
{ virtual: true }
|
||||
);
|
||||
|
||||
useMainStore.getters = {
|
||||
applicationUserObj: {
|
||||
experiments: [
|
||||
{
|
||||
settings: {},
|
||||
variationName: "test",
|
||||
universeName: "testUniverse",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// Act
|
||||
analyticsMixin.methods.pushExperimentsToDataLayer(mockExperimentData);
|
||||
|
||||
// Assert
|
||||
expect(window.dataLayer).toEqual([
|
||||
{
|
||||
experimentId_99: undefined,
|
||||
variationId_99: undefined,
|
||||
experimentName_99: "testUniverse",
|
||||
variationName_99: "test",
|
||||
customDimension_99: "undefined_undefined_testUniverse_test",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("Experiments, should push to dataLayer with custom Google Custom Dimension Index", () => {
|
||||
// Arrange
|
||||
window.dataLayer = [];
|
||||
|
||||
const mockExperimentData = [
|
||||
{
|
||||
settings: { "Google Custom Dimension Index": "5" },
|
||||
variationName: "test",
|
||||
universeName: "testUniverse",
|
||||
},
|
||||
];
|
||||
|
||||
// Mock store
|
||||
jest.mock(
|
||||
"@/store",
|
||||
() => {
|
||||
return {};
|
||||
},
|
||||
{ virtual: true }
|
||||
);
|
||||
|
||||
useMainStore.getters = {
|
||||
applicationUserObj: {
|
||||
experiments: [
|
||||
{
|
||||
settings: { "Google Custom Dimension Index": "5" },
|
||||
variationName: "test",
|
||||
universeName: "testUniverse",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// Act
|
||||
analyticsMixin.methods.pushExperimentsToDataLayer(mockExperimentData);
|
||||
|
||||
// Assert
|
||||
expect(window.dataLayer).toEqual([
|
||||
{
|
||||
experimentId_5: undefined,
|
||||
variationId_5: undefined,
|
||||
experimentName_5: "testUniverse",
|
||||
variationName_5: "test",
|
||||
customDimension_5: "undefined_undefined_testUniverse_test",
|
||||
},
|
||||
]);
|
||||
});
|
||||
*/
|
||||
test("Obj is not null after action prepended", () => {
|
||||
//Arrange
|
||||
const obj = { baseMethodName: "testMethodName", data: "testData" };
|
||||
const method = { name: "testMethodName", data: "testData" };
|
||||
const action = "testAction";
|
||||
|
||||
//Act
|
||||
analyticsMixin.methods.prependActionToMethod(obj, method, action);
|
||||
|
||||
//Assert
|
||||
expect(obj != null);
|
||||
});
|
||||
test("Obj method name does not include bound", () => {
|
||||
//Arrange
|
||||
const obj = { baseMethodName: "testMethodName", data: "testData" };
|
||||
const method = { name: "testMethodName", data: "testData" };
|
||||
const action = "testAction";
|
||||
|
||||
//Act
|
||||
analyticsMixin.methods.prependActionToMethod(obj, method, action);
|
||||
|
||||
//Assert
|
||||
expect(method.name.startsWith("bound ")).toBe(false);
|
||||
});
|
||||
test("Prepended action does not include bound", () => {
|
||||
//Arrange
|
||||
const obj = { baseMethodName: "testMethodName", data: "testData" };
|
||||
const method = { name: "testMethodName", data: "testData" };
|
||||
const action = "testAction";
|
||||
|
||||
//Act
|
||||
analyticsMixin.methods.prependActionToMethod(obj, method, action);
|
||||
|
||||
//Assert
|
||||
expect(action.startsWith("bound ")).toBe(false);
|
||||
});
|
||||
|
||||
test("analyticsPageEvents returns constants analyticsPageEvents", () => {
|
||||
//Act
|
||||
const analyticsPE = analyticsMixin.computed.analyticsPageEvents();
|
||||
|
||||
//Assert
|
||||
expect(analyticsPE).toEqual(analyticsPageEvents);
|
||||
});
|
||||
|
||||
test("GaActions returns constants GaActions", () => {
|
||||
//Act
|
||||
const gaActions = analyticsMixin.computed.GaActions();
|
||||
|
||||
//Assert
|
||||
expect(gaActions).toEqual(GaActions);
|
||||
});
|
||||
|
||||
test("GaCategories returns constants GaCategories", () => {
|
||||
//Act
|
||||
const gaCategories = analyticsMixin.computed.GaCategories();
|
||||
|
||||
//Assert
|
||||
expect(gaCategories).toEqual(GaCategories);
|
||||
});
|
||||
|
||||
test("GaLabels returns constants GaLabels", () => {
|
||||
//Act
|
||||
const gaLabels = analyticsMixin.computed.GaLabels();
|
||||
|
||||
//Assert
|
||||
expect(gaLabels).toEqual(GaLabels);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
import { storeActions } from "@/constants/store-actions.js";
|
||||
|
||||
import { mapStores } from "pinia";
|
||||
import { useMainStore } from "@/store";
|
||||
;
|
||||
import { navigationScenarios } from "@/router/router-constants/navigation-scenarios";
|
||||
import { vehicleCategories } from "@/constants/vehicle-categories.js";
|
||||
import { queryStrings } from "@/constants/query-strings";
|
||||
import { dynamicStrings } from "@/constants/dynamic-strings";
|
||||
import { useMainStore } from "@/store";
|
||||
import { mapStores } from "pinia";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
|
|
@ -25,11 +26,9 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
// store will be accessible globally as its id + 'Store'
|
||||
// store will be accessible globally as its id + 'Store'
|
||||
...mapStores(useMainStore),
|
||||
storeActions() {
|
||||
return storeActions;
|
||||
},
|
||||
|
||||
navigationScenarios() {
|
||||
return navigationScenarios;
|
||||
},
|
||||
|
|
@ -46,13 +45,4 @@ export default {
|
|||
return "widget-name-" + this.cmsWidgetName;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function encodeUriData(payload) {
|
||||
if (payload && Object.keys(payload).length > 0) {
|
||||
// Loop through the payload and encode the values
|
||||
Object.keys(payload).forEach((key) => {
|
||||
payload[key] = encodeURIComponent(payload[key]);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
17
src/mixins/experiment-mixin.js
Normal file
17
src/mixins/experiment-mixin.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { useMainStore } from "@/store";
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
hasSettingEqualTo(settingName, settingValue) {
|
||||
return useMainStore.experimentSettings[settingName] == settingValue;
|
||||
},
|
||||
hasSetting(settingName) {
|
||||
return Object.hasOwn( useMainStore.experimentSettings, settingName);
|
||||
},
|
||||
getSettingValue(settingName) {
|
||||
return this.hasSetting(settingName)
|
||||
? useMainStore.experimentSettings[settingName]
|
||||
: null;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -6,13 +6,14 @@ import { useMainStore } from '@/store';
|
|||
import eventBus from "@/helpers/event-bus/event-bus";
|
||||
import { globalEvents, globalEventTypes } from "@/constants/events";
|
||||
|
||||
import analyticsMixin from "@/mixins/analytics-mixin";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'root',
|
||||
async beforeEnter(to, from, next) {
|
||||
try {
|
||||
|
||||
try {
|
||||
to.query.issPage = !to.query.issPage ? issPageValues.VEHICLE_YEAR : to.query.issPage;
|
||||
|
||||
if (router.hasRoute(to.query.issPage)) {
|
||||
|
|
@ -52,10 +53,23 @@ const router = createRouter({
|
|||
routes,
|
||||
});
|
||||
|
||||
router.afterEach((to, from) => {
|
||||
|
||||
const store = useMainStore();
|
||||
// Update lastPageVisited in the store
|
||||
store.updateLastPageVisited(to.name);
|
||||
|
||||
// Push page view to GA
|
||||
analyticsMixin.methods.pushPageViewToGA();
|
||||
|
||||
// Push experiments to Data Layer
|
||||
analyticsMixin.methods.pushExperimentsToDataLayer();
|
||||
});
|
||||
|
||||
// Get route information by page name.
|
||||
// This will reach out to the Cms and there is a 1:1 relationship between page names and route names.
|
||||
async function GetRouteInfoFromPageName(pageName) {
|
||||
|
||||
const response = await useMainStore().getRouteInfo(pageName);
|
||||
|
||||
const jsonFromResponse = JSON.parse(response.data.Result);
|
||||
|
|
@ -69,7 +83,7 @@ async function GetRouteInfoFromPageName(pageName) {
|
|||
component: lazyLoadComponent(jsonFromResponse[key].LayoutName),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
return routeData;
|
||||
};
|
||||
|
||||
|
|
@ -124,6 +138,7 @@ function navigateToUrl(url, optionalQuery = {}) {
|
|||
function getNavigationMap (scenario, currentRoute) {
|
||||
const issPageValue = currentRoute.query.issPage;
|
||||
try {
|
||||
|
||||
const matchedQueryValue = routingTable(useMainStore())
|
||||
.filter(
|
||||
(item) =>
|
||||
|
|
|
|||
|
|
@ -4,5 +4,18 @@ export const issPageValues = {
|
|||
VEHICLE_YEAR: "vehicle-year",
|
||||
VEHICLE_MODEL: "vehicle-model",
|
||||
VEHICLE_STYLE: "vehicle-style",
|
||||
VEHICLE_DAMAGE: "vehicle-damage",
|
||||
ADDRESS_LOOKUP: "address-lookup",
|
||||
VIN_LOOKUP: "vin-lookup",
|
||||
VEHICLE_PARTS: "vehicle-parts",
|
||||
PART_QUESTIONS: "part-questions",
|
||||
MOLDING_QUESTIONS: "molding-questions",
|
||||
CAPABILITY_QUESTIONS: "capability-questions",
|
||||
LICENSE_PLATE_LOOKUP: "license-plate-lookup",
|
||||
REVEAL: "reveal",
|
||||
ESTIMATE: "estimate",
|
||||
ADDRESS_VEHICLES: "address-vehicles",
|
||||
QUOTE: "quote",
|
||||
HERITAGE: "heritage",
|
||||
};
|
||||
|
||||
5
src/router/router-constants/router-params.js
Normal file
5
src/router/router-constants/router-params.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
const routerParams = {
|
||||
DISPLAY_VEHICLE_CHANGE_ALERT: "displayVehicleChangeAlert",
|
||||
};
|
||||
|
||||
export { routerParams };
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { endpoints } from "@/constants/endpoints.js";
|
||||
import { getDateForSavedSessionTimeout } from "@/helpers/session-helper";
|
||||
import globalMethods from "@/global-methods";
|
||||
import { applicationConfig } from "@/constants/application-config";
|
||||
|
||||
|
|
@ -42,11 +43,17 @@ const getDefaultState = () => {
|
|||
accountNumber: 0,
|
||||
},
|
||||
applicationUser: {
|
||||
crmCustomerId: null,
|
||||
lastPageVisited: null,
|
||||
experiments: [],
|
||||
triggeredSiteEntry: false,
|
||||
eventBus: []
|
||||
eventBus: [],
|
||||
pageData: {},
|
||||
savedSessionTimeout: getDateForSavedSessionTimeout(),
|
||||
saveSessionPromise: null,
|
||||
savedSessionId: null,
|
||||
crmCustomerId: null,
|
||||
lastPageVisited: null,
|
||||
triggeredSiteEntry: false,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
@ -68,6 +75,70 @@ export const useMainStore = defineStore({
|
|||
return matchedEvent?.eventValue;
|
||||
},
|
||||
eventBus: (state) => state.applicationUser.eventBus,
|
||||
applicationUserObj: (state) => state.applicationUser,
|
||||
pageData: (state) => (page) => {
|
||||
return state.applicationUser.pageData[page];
|
||||
},
|
||||
experimentOrder: (state) => {
|
||||
return {
|
||||
funnelVehicleYear: state.order.vehicle.year,
|
||||
funnelVehicleMake: state.order.vehicle.make,
|
||||
funnelVehicleModel: state.order.vehicle.model,
|
||||
funnelVehicleStyle: state.order.vehicle.style,
|
||||
funnelIsRepair: state.order.damage.isRepair,
|
||||
funnelNumberOfChips: state.order.damage.numberOfChips,
|
||||
funnelCarId: state.order.vehicle.carId,
|
||||
funnelServiceCity: state.order.serviceLocation.city,
|
||||
funnelServiceState: state.order.serviceLocation.state,
|
||||
funnelServiceZipCode: state.order.serviceLocation.zipCode,
|
||||
funnelParentAccountNumber: state.order.accountNumber,
|
||||
funnelIsCoverageVerified: state.order.payment.insuranceCoverage.isVerified,
|
||||
funnelHasRecalibrationPart: getHasRecalibrationPart(state),
|
||||
funnelSelectedMultiGlass: state.order.damage.glassToReplace?.length > 1,
|
||||
funnelSelectedWindshieldGlass: getNonFalseValuesOfPropertyInArrayOfObjects(
|
||||
state.order.damage.glassToReplace,
|
||||
"glassLocation"
|
||||
).includes(damageLocationsSelected.WINDSHIELD),
|
||||
funnelSelectedBackGlass: getNonFalseValuesOfPropertyInArrayOfObjects(
|
||||
state.order.damage.glassToReplace,
|
||||
"glassLocation"
|
||||
).includes(damageLocationsSelected.REAR),
|
||||
funnelSelectedDriverSideGlass: getNonFalseValuesOfPropertyInArrayOfObjects(
|
||||
state.order.damage.glassToReplace,
|
||||
"glassLocation"
|
||||
).includes(damageLocationsSelected.DRIVER),
|
||||
funnelSelectedPassengerSideGlass: getNonFalseValuesOfPropertyInArrayOfObjects(
|
||||
state.order.damage.glassToReplace,
|
||||
"glassLocation"
|
||||
).includes(damageLocationsSelected.PASSENGER),
|
||||
|
||||
funnelOrderPartNumbers: [
|
||||
...getNonFalseValuesOfPropertyInArrayOfObjects(
|
||||
state.order.lineItems.glassParts,
|
||||
"partNumber"
|
||||
),
|
||||
...getNonFalseValuesOfPropertyInArrayOfObjects(
|
||||
state.order.lineItems.otherParts,
|
||||
"partNumber"
|
||||
),
|
||||
],
|
||||
|
||||
funnelOrderPartTypes: [
|
||||
...getNonFalseValuesOfPropertyInArrayOfObjects(
|
||||
state.order.lineItems.glassParts,
|
||||
"recalibrationType"
|
||||
),
|
||||
...getNonFalseValuesOfPropertyInArrayOfObjects(
|
||||
state.order.lineItems.otherParts,
|
||||
"recalibrationType"
|
||||
),
|
||||
],
|
||||
};
|
||||
},
|
||||
experimentSettings: (state) =>
|
||||
state.applicationUser.experiments
|
||||
.map((x) => x.settings)
|
||||
.reduce((r, c) => Object.assign(r, c), {}) ?? {}
|
||||
},
|
||||
actions:
|
||||
{
|
||||
|
|
@ -87,12 +158,13 @@ export const useMainStore = defineStore({
|
|||
endpoint: endpoints.GetHomepageInfo.url(applicationConfig.APPLICATION_ABBREVIATION),
|
||||
});
|
||||
},
|
||||
|
||||
getPageData(pageName) {
|
||||
return globalMethods.callHttpClient({
|
||||
method: endpoints.GetPageData.method,
|
||||
endpoint: endpoints.GetPageData.url(applicationConfig.APPLICATION_ABBREVIATION, pageName),
|
||||
payload: {},
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// Vehicle API Actions
|
||||
|
|
@ -186,7 +258,108 @@ export const useMainStore = defineStore({
|
|||
if(!localStorage.getItem(storeId)) {
|
||||
this.$state = state;
|
||||
}
|
||||
},
|
||||
|
||||
// Analytics Actions
|
||||
logExperimentExposure({ userId, sessionKey, pageName, experiment }) {
|
||||
return globalMethods.callHttpClient({
|
||||
method: endpoints.LogExperimentExposureIfAssigned.method,
|
||||
endpoint: endpoints.LogExperimentExposureIfAssigned.url,
|
||||
payload: {
|
||||
experimentForLogging: {
|
||||
userId: userId,
|
||||
experimentUniverseId: experiment.universeId,
|
||||
experimentUniverseName: experiment.universeName,
|
||||
experimentTestId: experiment.testId,
|
||||
experimentTestName: experiment.testName,
|
||||
experimentVariationId: experiment.variationId,
|
||||
experimentVariationName: experiment.variationName,
|
||||
enabled: experiment.isActive,
|
||||
isExposed: experiment.isExposed,
|
||||
userPartitionNumber: experiment.userPartitionNumber,
|
||||
assignmentId: experiment.assignmentId,
|
||||
sessionKey: sessionKey,
|
||||
pageName: pageName,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
logPageView({ userId, sessionKey, pageName, sessionId, action, event, shouldUseSessionId, experimentsForUser })
|
||||
{
|
||||
var payload = {
|
||||
userId: userId,
|
||||
sessionKey: sessionKey,
|
||||
sessionId: sessionId,
|
||||
pageName: pageName,
|
||||
applicationName: applicationConfig.APPLICATION_NAME,
|
||||
action: action,
|
||||
event: event,
|
||||
shouldUseSessionId: shouldUseSessionId,
|
||||
experimentsForUser: experimentsForUser,
|
||||
}
|
||||
|
||||
return globalMethods.callHttpClient({
|
||||
method: endpoints.LogPageView.method,
|
||||
endpoint: endpoints.LogPageView.url,
|
||||
payload: payload,
|
||||
logApiCall: false,
|
||||
});
|
||||
},
|
||||
logCustomEvent({ userId, sessionKey, pageName, sessionId, category, action, label, value, shouldUseSessionId, experimentsForUser})
|
||||
{
|
||||
var payload = {
|
||||
userId: userId,
|
||||
sessionKey: sessionKey,
|
||||
sessionId: sessionId,
|
||||
pageName: pageName,
|
||||
applicationName: applicationConfig.APPLICATION_NAME,
|
||||
category: category,
|
||||
action: action,
|
||||
label: label,
|
||||
value: value,
|
||||
shouldUseSessionId: shouldUseSessionId,
|
||||
experimentsForUser: experimentsForUser,
|
||||
};
|
||||
|
||||
return globalMethods.callHttpClient({
|
||||
method: endpoints.LogCustomEvent.method,
|
||||
endpoint: endpoints.LogCustomEvent.url,
|
||||
payload: payload,
|
||||
logApiCall: false,
|
||||
});
|
||||
},
|
||||
initializeSession({ userId, sessionId, userAgent, referrer }) {
|
||||
var payload = {
|
||||
applicationName: applicationConfig.APPLICATION_NAME,
|
||||
userId: userId,
|
||||
deviceId: userId,
|
||||
sessionId: sessionId,
|
||||
userAgent: userAgent,
|
||||
operatorId: "WEB",
|
||||
userName: "SafeliteISS",
|
||||
referrer: referrer,
|
||||
};
|
||||
|
||||
return globalMethods.callHttpClient({
|
||||
method: endpoints.InitializeSession.method,
|
||||
endpoint: endpoints.InitializeSession.url,
|
||||
payload: payload,
|
||||
logApiCall: false,
|
||||
});
|
||||
},
|
||||
|
||||
updateLastPageVisited(lastPageVisited) {
|
||||
this.applicationUser.lastPageVisited = lastPageVisited;
|
||||
},
|
||||
|
||||
GetExperimentsByUser(userId) {
|
||||
return globalMethods.callHttpClient({
|
||||
method: endpoints.GetExperimentsByUser.method,
|
||||
endpoint: `${endpoints.GetExperimentsByUser.url}/${userId}`,
|
||||
payload: {},
|
||||
});
|
||||
},
|
||||
|
||||
},
|
||||
persist: true
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ describe("Store", () => {
|
|||
|
||||
it("Should Store Vehicle Year", () => {
|
||||
let testYear = "2001";
|
||||
store.updateVehicleYear(testYear);
|
||||
store.order.vehicle.year = testYear;
|
||||
expect(store.order.vehicle.year).toEqual(testYear);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue