CASH-2521, CASH-2526 - Handle infinite loop and add logging
This commit is contained in:
parent
6992a5b0b9
commit
7c02499056
8 changed files with 227 additions and 8 deletions
|
|
@ -15,6 +15,7 @@ module.exports = {
|
||||||
"!src/main.js",
|
"!src/main.js",
|
||||||
"!src/constants/*.js",
|
"!src/constants/*.js",
|
||||||
"!src/router/**/*.js",
|
"!src/router/**/*.js",
|
||||||
|
"src/router/methods/error.js", // Re-include after the exclusion above (Jest applies later positive globs as overrides).
|
||||||
"!src/helpers/unit-test-helper.js",
|
"!src/helpers/unit-test-helper.js",
|
||||||
"!src/helpers/logger.js",
|
"!src/helpers/logger.js",
|
||||||
"!src/layouts/vehicle-damage/windshield-options/windshield-options.vue",
|
"!src/layouts/vehicle-damage/windshield-options/windshield-options.vue",
|
||||||
|
|
|
||||||
10
src/App.vue
10
src/App.vue
|
|
@ -64,6 +64,16 @@ window.onerror = (msg, url, line, col, error) => {
|
||||||
var suppressErrorAlert = true;
|
var suppressErrorAlert = true;
|
||||||
return suppressErrorAlert;
|
return suppressErrorAlert;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Log Promise rejections that are never handled (.catch / await try/catch), e.g. fire-and-forget async.
|
||||||
|
window.addEventListener("unhandledrejection", (event) => {
|
||||||
|
const reason = event.reason;
|
||||||
|
const message = reason instanceof Error ? reason.message : String(reason);
|
||||||
|
global.$logger.logError(`unhandledrejection: ${message}`, {
|
||||||
|
stack: reason instanceof Error ? reason.stack : undefined,
|
||||||
|
reason: reason instanceof Error ? undefined : reason,
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
export const sessionStorageKeyConstants = {
|
export const sessionStorageKeyConstants = {
|
||||||
SUBMITTED_STATE: "submittedState",
|
SUBMITTED_STATE: "submittedState",
|
||||||
|
/** Counts SuppressConceptFunnel → heritage redirects to avoid an infinite loop in one tab session. */
|
||||||
|
HERITAGE_SUPPRESS_REDIRECT_COUNT: "heritageSuppressRedirectCount",
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -25,9 +25,9 @@ export async function navigateToHeritageFunnel({ shouldSaveSession, pageNameToLo
|
||||||
}
|
}
|
||||||
|
|
||||||
var heritageParms = {
|
var heritageParms = {
|
||||||
corid: store.getters.order.referralCorrelationId,
|
corid: store.getters.order.referralCorrelationId ?? "",
|
||||||
src: "concept-funnel",
|
src: "concept-funnel",
|
||||||
conceptsqid: store.getters.applicationUser.savedSessionId,
|
conceptsqid: store.getters.applicationUser.savedSessionId ?? "",
|
||||||
isInsurance: store.getters.payment.isInsurance,
|
isInsurance: store.getters.payment.isInsurance,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { isSavedSessionStillActive } from "@/helpers/heritage-integration/sessio
|
||||||
import analyticsMixin from "@/mixins/analytics-mixin";
|
import analyticsMixin from "@/mixins/analytics-mixin";
|
||||||
import { routeData, FUNNEL_START_PAGE } from "@/router/constants/routes";
|
import { routeData, FUNNEL_START_PAGE } from "@/router/constants/routes";
|
||||||
import { runExperiments } from "@/router/methods/helpers/run-experiments";
|
import { runExperiments } from "@/router/methods/helpers/run-experiments";
|
||||||
|
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
|
||||||
|
|
||||||
import { handleSoftError, handleHardError } from "@/router/methods/error";
|
import { handleSoftError, handleHardError } from "@/router/methods/error";
|
||||||
import { checkPagePrerequisites } from "@/router/methods/page-prerequisites";
|
import { checkPagePrerequisites } from "@/router/methods/page-prerequisites";
|
||||||
|
|
@ -16,6 +17,8 @@ import { handleHeritageReturn } from "@/router/methods/helpers/handle-heritage-r
|
||||||
import { isVirtualRoute } from "@/router/methods/helpers/is-virtual-route";
|
import { isVirtualRoute } from "@/router/methods/helpers/is-virtual-route";
|
||||||
import router from "@/router";
|
import router from "@/router";
|
||||||
|
|
||||||
|
const MAX_HERITAGE_SUPPRESS_REDIRECTS_PER_SESSION = 5;
|
||||||
|
|
||||||
export async function beforeEach(to, from) {
|
export async function beforeEach(to, from) {
|
||||||
try {
|
try {
|
||||||
await analyticsMixin.methods.validateSession();
|
await analyticsMixin.methods.validateSession();
|
||||||
|
|
@ -27,10 +30,37 @@ export async function beforeEach(to, from) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getFunnelCookie()?.SuppressConceptFunnel) {
|
if (getFunnelCookie()?.SuppressConceptFunnel) {
|
||||||
// Redirect to heritage.
|
const redirectKey = sessionStorageKeyConstants.HERITAGE_SUPPRESS_REDIRECT_COUNT;
|
||||||
return {
|
const redirectCount = Number(window.sessionStorage.getItem(redirectKey) ?? 0);
|
||||||
name: routeData.HERITAGE.name,
|
|
||||||
};
|
if (redirectCount < MAX_HERITAGE_SUPPRESS_REDIRECTS_PER_SESSION) {
|
||||||
|
window.sessionStorage.setItem(redirectKey, String(redirectCount + 1));
|
||||||
|
navigateToHeritageFunnel({
|
||||||
|
shouldSaveSession: false,
|
||||||
|
pageNameToLog: to.name,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
// If the redirect count is greater than the max allowed, we need to handle the error.
|
||||||
|
|
||||||
|
// Reset the redirect count to 0.
|
||||||
|
window.sessionStorage.setItem(redirectKey, "0");
|
||||||
|
|
||||||
|
debugLog(
|
||||||
|
"SuppressConceptFunnel: skipped heritage redirect after max attempts this session",
|
||||||
|
{ redirectCount, to: to.name }
|
||||||
|
);
|
||||||
|
|
||||||
|
const errorPayload = {
|
||||||
|
cause: "Handling heritage redirect after max attempts this session.",
|
||||||
|
currentPage: from?.name,
|
||||||
|
nextPage: to?.name,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Eject user from Vue app in this scenario and clear localstorage.
|
||||||
|
await handleSoftError(errorPayload, true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure session has not expired
|
// Ensure session has not expired
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ export async function handleSoftError(errorPayload, forceRestart = false) {
|
||||||
deleteFunnelCookie();
|
deleteFunnelCookie();
|
||||||
}
|
}
|
||||||
|
|
||||||
analyticsMixin.methods.pushPageErrorToDataLayer(errorPayload);
|
logErrorToDataLayerAndLogger("handleSoftError", errorPayload, forceRestart);
|
||||||
|
|
||||||
router.push({
|
router.push({
|
||||||
name: routeData.ERROR.name,
|
name: routeData.ERROR.name,
|
||||||
|
|
@ -27,7 +27,7 @@ export async function handleHardError(errorPayload) {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isInStaticErrorExperiment) {
|
if (isInStaticErrorExperiment) {
|
||||||
analyticsMixin?.methods?.pushPageErrorToDataLayer(errorPayload);
|
logErrorToDataLayerAndLogger("handleHardError", errorPayload, null);
|
||||||
window.top.location = "/fmg/static/error";
|
window.top.location = "/fmg/static/error";
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -38,3 +38,10 @@ export async function handleHardError(errorPayload) {
|
||||||
await handleSoftError(errorPayload, true);
|
await handleSoftError(errorPayload, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function logErrorToDataLayerAndLogger(context, errorPayload, forceRestart = null) {
|
||||||
|
const forceRestartSegment = forceRestart === true ? " (force restart)" : "";
|
||||||
|
const message = `${context}${forceRestartSegment}: ${errorPayload?.cause ?? "unknown cause"}`;
|
||||||
|
analyticsMixin?.methods?.pushPageErrorToDataLayer(errorPayload);
|
||||||
|
global.$logger.logError(message, errorPayload);
|
||||||
|
}
|
||||||
|
|
|
||||||
168
src/router/methods/error.spec.js
Normal file
168
src/router/methods/error.spec.js
Normal file
|
|
@ -0,0 +1,168 @@
|
||||||
|
import { handleSoftError, handleHardError } from "./error";
|
||||||
|
import router from "@/router";
|
||||||
|
import store from "@/store";
|
||||||
|
import analyticsMixin from "@/mixins/analytics-mixin";
|
||||||
|
import experimentMixin from "../../mixins/experiment-mixin";
|
||||||
|
import { deleteFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
|
||||||
|
import { storeActions } from "@/constants/store-actions";
|
||||||
|
import { routeData } from "@/router/constants/routes";
|
||||||
|
|
||||||
|
jest.mock("@/router", () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: {
|
||||||
|
push: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("@/store", () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: {
|
||||||
|
dispatch: jest.fn(() => Promise.resolve()),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("@/helpers/heritage-integration/cookie-helper", () => ({
|
||||||
|
deleteFunnelCookie: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("@/mixins/analytics-mixin", () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: {
|
||||||
|
methods: {
|
||||||
|
pushPageErrorToDataLayer: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("../../mixins/experiment-mixin", () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: {
|
||||||
|
methods: {
|
||||||
|
hasSettingEqualTo: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("router/methods/error", () => {
|
||||||
|
const errorPayload = { cause: "test cause", currentPage: "from", nextPage: "to" };
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
store.dispatch.mockResolvedValue(undefined);
|
||||||
|
global.$logger = {
|
||||||
|
logInformation: jest.fn(),
|
||||||
|
logWarning: jest.fn(),
|
||||||
|
logError: jest.fn(),
|
||||||
|
logCritical: jest.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("handleSoftError", () => {
|
||||||
|
it("logs and navigates to the error route without reset when forceRestart is false", async () => {
|
||||||
|
await handleSoftError(errorPayload, false);
|
||||||
|
|
||||||
|
expect(store.dispatch).not.toHaveBeenCalled();
|
||||||
|
expect(deleteFunnelCookie).not.toHaveBeenCalled();
|
||||||
|
expect(analyticsMixin.methods.pushPageErrorToDataLayer).toHaveBeenCalledWith(
|
||||||
|
errorPayload
|
||||||
|
);
|
||||||
|
expect(global.$logger.logError).toHaveBeenCalledWith(
|
||||||
|
"handleSoftError: test cause",
|
||||||
|
errorPayload
|
||||||
|
);
|
||||||
|
expect(router.push).toHaveBeenCalledWith({ name: routeData.ERROR.name });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("dispatches reset, deletes funnel cookie, logs, and navigates when forceRestart is true", async () => {
|
||||||
|
await handleSoftError(errorPayload, true);
|
||||||
|
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(storeActions.RESET_STATE);
|
||||||
|
expect(deleteFunnelCookie).toHaveBeenCalled();
|
||||||
|
expect(analyticsMixin.methods.pushPageErrorToDataLayer).toHaveBeenCalledWith(
|
||||||
|
errorPayload
|
||||||
|
);
|
||||||
|
expect(global.$logger.logError).toHaveBeenCalledWith(
|
||||||
|
"handleSoftError (force restart): test cause",
|
||||||
|
errorPayload
|
||||||
|
);
|
||||||
|
expect(router.push).toHaveBeenCalledWith({ name: routeData.ERROR.name });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses unknown cause in the log message when payload has no cause", async () => {
|
||||||
|
await handleSoftError({}, false);
|
||||||
|
|
||||||
|
expect(global.$logger.logError).toHaveBeenCalledWith(
|
||||||
|
"handleSoftError: unknown cause",
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("handleHardError", () => {
|
||||||
|
let topLocationValue;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
topLocationValue = undefined;
|
||||||
|
Object.defineProperty(window, "top", {
|
||||||
|
configurable: true,
|
||||||
|
value: {},
|
||||||
|
});
|
||||||
|
Object.defineProperty(window.top, "location", {
|
||||||
|
configurable: true,
|
||||||
|
set(value) {
|
||||||
|
topLocationValue = value;
|
||||||
|
},
|
||||||
|
get() {
|
||||||
|
return topLocationValue;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("logs and sets static error location when UseStaticErrorPage experiment is on", async () => {
|
||||||
|
experimentMixin.methods.hasSettingEqualTo.mockReturnValue(true);
|
||||||
|
|
||||||
|
await handleHardError(errorPayload);
|
||||||
|
|
||||||
|
expect(experimentMixin.methods.hasSettingEqualTo).toHaveBeenCalledWith(
|
||||||
|
"UseStaticErrorPage",
|
||||||
|
"true"
|
||||||
|
);
|
||||||
|
expect(analyticsMixin.methods.pushPageErrorToDataLayer).toHaveBeenCalledWith(
|
||||||
|
errorPayload
|
||||||
|
);
|
||||||
|
expect(global.$logger.logError).toHaveBeenCalledWith(
|
||||||
|
"handleHardError: test cause",
|
||||||
|
errorPayload
|
||||||
|
);
|
||||||
|
expect(topLocationValue).toBe("/fmg/static/error");
|
||||||
|
expect(router.push).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("delegates to handleSoftError with force restart when static error page is off", async () => {
|
||||||
|
experimentMixin.methods.hasSettingEqualTo.mockReturnValue(false);
|
||||||
|
|
||||||
|
await handleHardError(errorPayload);
|
||||||
|
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(storeActions.RESET_STATE);
|
||||||
|
expect(deleteFunnelCookie).toHaveBeenCalled();
|
||||||
|
expect(global.$logger.logError).toHaveBeenCalledWith(
|
||||||
|
"handleSoftError (force restart): test cause",
|
||||||
|
errorPayload
|
||||||
|
);
|
||||||
|
expect(router.push).toHaveBeenCalledWith({ name: routeData.ERROR.name });
|
||||||
|
expect(topLocationValue).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("delegates to handleSoftError with force restart when experiment check throws", async () => {
|
||||||
|
experimentMixin.methods.hasSettingEqualTo.mockImplementation(() => {
|
||||||
|
throw new Error("experiment failure");
|
||||||
|
});
|
||||||
|
|
||||||
|
await handleHardError(errorPayload);
|
||||||
|
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(storeActions.RESET_STATE);
|
||||||
|
expect(deleteFunnelCookie).toHaveBeenCalled();
|
||||||
|
expect(router.push).toHaveBeenCalledWith({ name: routeData.ERROR.name });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
|
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
|
||||||
|
|
||||||
|
// Unsure if this is needed. I don't think its used anywhere
|
||||||
export async function heritageBeforeEnter(to, from) {
|
export async function heritageBeforeEnter(to, from) {
|
||||||
await navigateToHeritageFunnel({ shouldSaveSession: false, pageNameToLog: to.name });
|
await navigateToHeritageFunnel({ shouldSaveSession: false, pageNameToLog: to.name });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue