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/constants/*.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/logger.js",
|
||||
"!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;
|
||||
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>
|
||||
|
||||
<style lang="scss">
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
export const sessionStorageKeyConstants = {
|
||||
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 = {
|
||||
corid: store.getters.order.referralCorrelationId,
|
||||
corid: store.getters.order.referralCorrelationId ?? "",
|
||||
src: "concept-funnel",
|
||||
conceptsqid: store.getters.applicationUser.savedSessionId,
|
||||
conceptsqid: store.getters.applicationUser.savedSessionId ?? "",
|
||||
isInsurance: store.getters.payment.isInsurance,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { isSavedSessionStillActive } from "@/helpers/heritage-integration/sessio
|
|||
import analyticsMixin from "@/mixins/analytics-mixin";
|
||||
import { routeData, FUNNEL_START_PAGE } from "@/router/constants/routes";
|
||||
import { runExperiments } from "@/router/methods/helpers/run-experiments";
|
||||
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
|
||||
|
||||
import { handleSoftError, handleHardError } from "@/router/methods/error";
|
||||
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 router from "@/router";
|
||||
|
||||
const MAX_HERITAGE_SUPPRESS_REDIRECTS_PER_SESSION = 5;
|
||||
|
||||
export async function beforeEach(to, from) {
|
||||
try {
|
||||
await analyticsMixin.methods.validateSession();
|
||||
|
|
@ -27,10 +30,37 @@ export async function beforeEach(to, from) {
|
|||
}
|
||||
|
||||
if (getFunnelCookie()?.SuppressConceptFunnel) {
|
||||
// Redirect to heritage.
|
||||
return {
|
||||
name: routeData.HERITAGE.name,
|
||||
const redirectKey = sessionStorageKeyConstants.HERITAGE_SUPPRESS_REDIRECT_COUNT;
|
||||
const redirectCount = Number(window.sessionStorage.getItem(redirectKey) ?? 0);
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export async function handleSoftError(errorPayload, forceRestart = false) {
|
|||
deleteFunnelCookie();
|
||||
}
|
||||
|
||||
analyticsMixin.methods.pushPageErrorToDataLayer(errorPayload);
|
||||
logErrorToDataLayerAndLogger("handleSoftError", errorPayload, forceRestart);
|
||||
|
||||
router.push({
|
||||
name: routeData.ERROR.name,
|
||||
|
|
@ -27,7 +27,7 @@ export async function handleHardError(errorPayload) {
|
|||
);
|
||||
|
||||
if (isInStaticErrorExperiment) {
|
||||
analyticsMixin?.methods?.pushPageErrorToDataLayer(errorPayload);
|
||||
logErrorToDataLayerAndLogger("handleHardError", errorPayload, null);
|
||||
window.top.location = "/fmg/static/error";
|
||||
return;
|
||||
} else {
|
||||
|
|
@ -38,3 +38,10 @@ export async function handleHardError(errorPayload) {
|
|||
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";
|
||||
|
||||
// Unsure if this is needed. I don't think its used anywhere
|
||||
export async function heritageBeforeEnter(to, from) {
|
||||
await navigateToHeritageFunnel({ shouldSaveSession: false, pageNameToLog: to.name });
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue