This adds a circuit breaker for uncaught failures in beforeEach. The first failure still goes through the normal ERROR/RESTART recovery path; a second consecutive failure redirects to the static error page instead of looping. - New before-each-error-recovery module tracks recovery attempts in sessionStorage (with an in-memory fallback). - ERROR/RESTART routes skip normal guard logic so the counter can accumulate across bounce-backs. - The counter clears only after a fully successful navigation. - redirectToStaticErrorPage centralizes hard-error bailout: logs, clears poisoned sessionStorage keys (submittedState, heritage redirect count, recovery count, externalParameterState), then navigates to /fmg/static/error. - The static error page RESTART flow clears the same sessionStorage keys so bad state does not re-enter the funnel. - Session storage key strings are centralized in session-storage.js. - Unit tests cover recovery counting, bailout clearing, and navigation fallbacks. Expected impact: Stops repeated ERROR → RESTART → ERROR cycles (including corrupt submittedState JSON.parse failures and recurring automation errors in us-east-1/us-east-2) by bailing out to the static error page after one failed recovery attempt.
197 lines
7.2 KiB
JavaScript
197 lines
7.2 KiB
JavaScript
import { sessionStorageKeyConstants } from "@/constants/session-storage";
|
|
import {
|
|
getFunnelCookie,
|
|
updateOrCreateFunnelCookie,
|
|
} from "@/helpers/heritage-integration/cookie-helper";
|
|
import { isSavedSessionStillActive } from "@/helpers/heritage-integration/session-helper";
|
|
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,
|
|
redirectToStaticErrorPage,
|
|
} from "@/router/methods/error";
|
|
import {
|
|
clearBeforeEachErrorRecoveryCount,
|
|
hasBeforeEachErrorRecoveryBeenAttempted,
|
|
incrementBeforeEachErrorRecoveryCount,
|
|
} from "@/router/methods/before-each-error-recovery";
|
|
import { checkPagePrerequisites } from "@/router/methods/page-prerequisites";
|
|
import { checkLogParam } from "@/helpers/debug-log-helper";
|
|
import { debugLog } from "@/helpers/debug-log-helper";
|
|
import { handleHeritageReturn } from "@/router/methods/helpers/handle-heritage-return";
|
|
import { isVirtualRoute } from "@/router/methods/helpers/is-virtual-route";
|
|
import router from "@/router";
|
|
|
|
const MAX_HERITAGE_SUPPRESS_REDIRECTS_PER_SESSION = 5;
|
|
|
|
function isRecoveryRoute(routeName) {
|
|
return routeName === routeData.ERROR.name || routeName === routeData.RESTART.name;
|
|
}
|
|
|
|
export async function beforeEach(to, from) {
|
|
try {
|
|
if (isRecoveryRoute(to.name)) {
|
|
debugLog(`--- before-each.js recovery route ${to.name} ---`);
|
|
// Do NOT clear the recovery counter here. The counter must accumulate across
|
|
// consecutive failures that bounce through the ERROR/RESTART route so the
|
|
// circuit breaker can trip and fall back to the static error page. It is only
|
|
// cleared after a fully successful guard pass (end of try block).
|
|
return;
|
|
}
|
|
|
|
await analyticsMixin.methods.validateSession();
|
|
|
|
// prettier-ignore
|
|
{
|
|
debugLog(`--- before-each.js ${from?.name} start ---`);
|
|
debugLog(" funnel cookie init: ", getFunnelCookie());
|
|
}
|
|
|
|
if (getFunnelCookie()?.SuppressConceptFunnel) {
|
|
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 - not primary check for session expiration and uses FunnelSessionInfo-{environment} cookie.
|
|
if (getFunnelCookie() !== null && !isSavedSessionStillActive()) {
|
|
const errorPayload = {
|
|
cause: "expired session",
|
|
currentPage: from?.name,
|
|
nextPage: to?.name,
|
|
};
|
|
|
|
await handleSoftError(errorPayload, true);
|
|
return false;
|
|
}
|
|
|
|
// Block navigation if an order has been submitted.
|
|
const submittedState = window.sessionStorage.getItem(
|
|
sessionStorageKeyConstants.SUBMITTED_STATE
|
|
);
|
|
if (submittedState !== null) {
|
|
const isBailout = getIsBailout(submittedState);
|
|
const exceptionPages = isBailout
|
|
? [FUNNEL_START_PAGE.name, routeData.BAILOUT_SUCCESS.name]
|
|
: [FUNNEL_START_PAGE.name, routeData.CONFIRMATION.name];
|
|
|
|
if (!exceptionPages.some((name) => to.name === name) && !isVirtualRoute(to.name)) {
|
|
if (isBailout) {
|
|
router.push({
|
|
name: routeData.BAILOUT_SUCCESS.name,
|
|
});
|
|
return false;
|
|
} else {
|
|
router.push({
|
|
name: routeData.CONFIRMATION.name,
|
|
});
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Special logic when returning from insurance flow.
|
|
await handleHeritageReturn(to, from);
|
|
|
|
// prettier-ignore
|
|
{
|
|
debugLog(`--- before-each.js ${from?.name} mid ---`);
|
|
debugLog(" funnel cookie after load: ", getFunnelCookie());
|
|
}
|
|
|
|
// Process funnel cookie.
|
|
updateOrCreateFunnelCookie();
|
|
|
|
// prettier-ignore
|
|
{
|
|
debugLog(`--- before-each.js ${from?.name} end ---`);
|
|
debugLog(" funnel cookie after update: ", getFunnelCookie());
|
|
}
|
|
|
|
// Update if logging is enabled.
|
|
checkLogParam();
|
|
|
|
// Ensure page-prerequisites are fulfilled
|
|
const arePagePrerequisitesValid = await checkPagePrerequisites(to.name);
|
|
|
|
if (!arePagePrerequisitesValid) {
|
|
const errorPayload = {
|
|
cause: "invalid page prerequisites for page",
|
|
currentPage: from?.name,
|
|
nextPage: to?.name,
|
|
};
|
|
|
|
await handleSoftError(errorPayload);
|
|
return;
|
|
}
|
|
|
|
await runExperiments(to.name);
|
|
|
|
// prettier-ignore
|
|
{
|
|
debugLog("--- before-each.js end ---");
|
|
}
|
|
|
|
clearBeforeEachErrorRecoveryCount();
|
|
} catch (error) {
|
|
const errorPayload = {
|
|
cause: "Uncaught exception in `beforeEach`.",
|
|
currentPage: from?.name,
|
|
nextPage: to?.name,
|
|
errorMessage: error?.message,
|
|
errorStack: error?.stack,
|
|
};
|
|
|
|
console.log(error);
|
|
global.$logger?.logError("beforeEach uncaught exception", errorPayload);
|
|
|
|
if (to.name === routeData.ERROR.name || hasBeforeEachErrorRecoveryBeenAttempted()) {
|
|
clearBeforeEachErrorRecoveryCount();
|
|
await redirectToStaticErrorPage(errorPayload);
|
|
return false;
|
|
}
|
|
|
|
incrementBeforeEachErrorRecoveryCount();
|
|
|
|
// Eject user from Vue app in this scenario and clear localstorage.
|
|
await handleHardError(errorPayload);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function getIsBailout(submittedState) {
|
|
const submittedStateObj = JSON.parse(submittedState);
|
|
return !!submittedStateObj?.applicationUser?.bailoutCode;
|
|
}
|