From f6c7ca50b07ac28cf4e39c7be942ee6fab0a3306 Mon Sep 17 00:00:00 2001 From: Chloe Herd Date: Tue, 9 Jun 2026 16:35:02 -0400 Subject: [PATCH 1/8] Change default variation for Cybersource/Adyen --- src/layouts/payment-method/payment-method.vue | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/layouts/payment-method/payment-method.vue b/src/layouts/payment-method/payment-method.vue index b0347f581..810414b96 100644 --- a/src/layouts/payment-method/payment-method.vue +++ b/src/layouts/payment-method/payment-method.vue @@ -567,14 +567,14 @@ export default { this.dispatchStoreAction(storeActions.RESET_SAVE_SESSION_PROMISE); - const shouldUseAdyen = experimentMixin.methods.hasSettingEqualTo( + const shouldUseCybersource = experimentMixin.methods.hasSettingEqualTo( experimentSettings.USE_ADYEN_PAYMENT, - "true" + "false" ); - const scenarioName = shouldUseAdyen - ? this.navigationScenarios.CLICKED_PAY_NOW_ADYEN - : this.navigationScenarios.CLICKED_PAY_NOW; + const scenarioName = shouldUseCybersource + ? this.navigationScenarios.CLICKED_PAY_NOW + : this.navigationScenarios.CLICKED_PAY_NOW_ADYEN; this.$router.navigateWithoutSaving(scenarioName, this.pageName); }, From f3b9c34656300202fcf2e73778a4fbb58f830f0f Mon Sep 17 00:00:00 2001 From: Minojhini Valaiyapathi Date: Wed, 10 Jun 2026 14:34:27 -0400 Subject: [PATCH 2/8] CASH-2856 - if VinRequired use Parts Api v2 endpoint for Parts-Or-Questions --- src/constants/endpoints.js | 4 ++++ src/store/index.js | 8 ++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/constants/endpoints.js b/src/constants/endpoints.js index efb32ffc6..d6b3e3431 100644 --- a/src/constants/endpoints.js +++ b/src/constants/endpoints.js @@ -56,6 +56,10 @@ const endpoints = { url: "/parts/api/v1/parts/parts-or-questions", method: "POST", }, + GetPartsOrQuestionsV2: { + url: "/parts/api/v2/parts/parts-or-questions", + method: "POST", + }, GetParts: { url: "/parts/api/v1/parts/parts", method: "POST", diff --git a/src/store/index.js b/src/store/index.js index 3f2cf5e0d..5e69005e9 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1970,10 +1970,14 @@ export const actions = { // create a new array to avoid mutating state const glassArrayForPayload = convertGlassPieceNamingForApi(glassArray); + const partsOrQuestionsEndpoint = vehicle.vinRequired + ? endpoints.GetPartsOrQuestionsV2 + : endpoints.GetPartsOrQuestions; + const response = await globalMethods .callHttpClient({ - method: endpoints.GetPartsOrQuestions.method, - endpoint: endpoints.GetPartsOrQuestions.url, + method: partsOrQuestionsEndpoint.method, + endpoint: partsOrQuestionsEndpoint.url, payload: { carId: carId, glassPieces: glassArrayForPayload, From 76b4c35d59f1d217af066851796feee23068fc2b Mon Sep 17 00:00:00 2001 From: Matt Sykes Date: Wed, 10 Jun 2026 15:48:29 -0400 Subject: [PATCH 3/8] Prevent error redirect loops with beforeEach circuit breaker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- public/static/error/index.html | 26 ++ src/constants/session-storage.js | 4 + .../methods/before-each-error-recovery.js | 51 ++++ .../before-each-error-recovery.spec.js | 80 ++++++ src/router/methods/before-each.js | 41 ++- src/router/methods/before-each.spec.js | 257 ++++++++++++++++++ src/router/methods/error.js | 35 ++- src/router/methods/error.spec.js | 125 ++++++++- src/router/methods/route-logic/error.js | 2 + src/store/index.js | 15 +- 10 files changed, 625 insertions(+), 11 deletions(-) create mode 100644 src/router/methods/before-each-error-recovery.js create mode 100644 src/router/methods/before-each-error-recovery.spec.js create mode 100644 src/router/methods/before-each.spec.js diff --git a/public/static/error/index.html b/public/static/error/index.html index 53bb9fc08..da15bd7a0 100644 --- a/public/static/error/index.html +++ b/public/static/error/index.html @@ -258,10 +258,36 @@ body { return match?.value; } + // Refactor opportunity: sessionStorage keys and cookie names below are duplicated from the + // Vue app because this static page is not webpack-bundled. Canonical sources: + // - src/constants/session-storage.js (sessionStorageKeyConstants) + // - src/constants/cookie-names.js (cookieNames; env-suffixed funnel cookies) + // - src/router/methods/error.js (clearBailoutSessionStorage — keep in sync on bailout keys) + // Options: build-time inject into this file, shared plain JS under public/static/, or generate + // from constants during CI. Until then, update all three places when keys change. function clearApplicationData() { window.localStorage.removeItem('vuex'); + // Keys match sessionStorageKeyConstants in src/constants/session-storage.js and + // clearBailoutSessionStorage() in src/router/methods/error.js — keep in sync. + const sessionStorageKeysToClear = [ + 'submittedState', + 'heritageSuppressRedirectCount', + 'beforeEachErrorRecoveryCount', + 'externalParameterState', + ]; + // Clear sessionStorage keys that can re-poison the funnel on re-entry. + sessionStorageKeysToClear.forEach((key) => { + try { + window.sessionStorage.removeItem(key); + } catch { + // sessionStorage unavailable + } + }); + const environmentData = getCurrentEnvironmentData(); + // Funnel cookie prefixes match cookie-names.js; suffix is environmentData.name here + // (equivalent to applicationConfig.CURRENT_ENVIRONMENT in the Vue app). const cookieNames = [ `FunnelUserId-${environmentData?.name}`, `FunnelSessionKey-${environmentData?.name}`, diff --git a/src/constants/session-storage.js b/src/constants/session-storage.js index 75cd1b578..9481c53d8 100644 --- a/src/constants/session-storage.js +++ b/src/constants/session-storage.js @@ -2,4 +2,8 @@ export const sessionStorageKeyConstants = { SUBMITTED_STATE: "submittedState", /** Counts SuppressConceptFunnel → heritage redirects to avoid an infinite loop in one tab session. */ HERITAGE_SUPPRESS_REDIRECT_COUNT: "heritageSuppressRedirectCount", + /** Counts beforeEach uncaught-exception recovery attempts to avoid an infinite ERROR redirect loop. */ + BEFORE_EACH_ERROR_RECOVERY_COUNT: "beforeEachErrorRecoveryCount", + /** Persists deep-link / querystring funnel entry state across navigations. */ + EXTERNAL_PARAMETER_STATE: "externalParameterState", }; diff --git a/src/router/methods/before-each-error-recovery.js b/src/router/methods/before-each-error-recovery.js new file mode 100644 index 000000000..b925c510f --- /dev/null +++ b/src/router/methods/before-each-error-recovery.js @@ -0,0 +1,51 @@ +import { sessionStorageKeyConstants } from "@/constants/session-storage"; + +const recoveryKey = sessionStorageKeyConstants.BEFORE_EACH_ERROR_RECOVERY_COUNT; + +/** Used when sessionStorage is missing, throws, or does not persist (e.g. some bots). */ +let inMemoryRecoveryCount = 0; + +export function getBeforeEachErrorRecoveryCount() { + try { + const stored = window.sessionStorage?.getItem(recoveryKey); + if (stored !== null && stored !== undefined) { + return Number(stored) || 0; + } + } catch { + // sessionStorage unavailable; use in-memory fallback + } + + return inMemoryRecoveryCount; +} + +export function incrementBeforeEachErrorRecoveryCount() { + const nextCount = getBeforeEachErrorRecoveryCount() + 1; + inMemoryRecoveryCount = nextCount; + + try { + window.sessionStorage?.setItem(recoveryKey, String(nextCount)); + } catch { + // in-memory count already updated + } + + return nextCount; +} + +export function clearBeforeEachErrorRecoveryCount() { + inMemoryRecoveryCount = 0; + + try { + window.sessionStorage?.removeItem(recoveryKey); + } catch { + // in-memory count already cleared + } +} + +export function hasBeforeEachErrorRecoveryBeenAttempted() { + return getBeforeEachErrorRecoveryCount() >= 1; +} + +/** Test-only: resets in-memory and sessionStorage recovery state between unit tests. */ +export function resetBeforeEachErrorRecoveryStateForTests() { + clearBeforeEachErrorRecoveryCount(); +} diff --git a/src/router/methods/before-each-error-recovery.spec.js b/src/router/methods/before-each-error-recovery.spec.js new file mode 100644 index 000000000..e916ff356 --- /dev/null +++ b/src/router/methods/before-each-error-recovery.spec.js @@ -0,0 +1,80 @@ +import { + clearBeforeEachErrorRecoveryCount, + getBeforeEachErrorRecoveryCount, + hasBeforeEachErrorRecoveryBeenAttempted, + incrementBeforeEachErrorRecoveryCount, + resetBeforeEachErrorRecoveryStateForTests, +} from "./before-each-error-recovery"; +import { sessionStorageKeyConstants } from "@/constants/session-storage"; + +describe("router/methods/before-each-error-recovery", () => { + const recoveryKey = sessionStorageKeyConstants.BEFORE_EACH_ERROR_RECOVERY_COUNT; + let originalSessionStorage; + + beforeEach(() => { + resetBeforeEachErrorRecoveryStateForTests(); + originalSessionStorage = window.sessionStorage; + }); + + afterEach(() => { + Object.defineProperty(window, "sessionStorage", { + configurable: true, + value: originalSessionStorage, + }); + resetBeforeEachErrorRecoveryStateForTests(); + }); + + it("reads and writes recovery count via sessionStorage when available", () => { + incrementBeforeEachErrorRecoveryCount(); + + expect(window.sessionStorage.getItem(recoveryKey)).toBe("1"); + expect(getBeforeEachErrorRecoveryCount()).toBe(1); + expect(hasBeforeEachErrorRecoveryBeenAttempted()).toBe(true); + }); + + it("clears sessionStorage and in-memory count", () => { + incrementBeforeEachErrorRecoveryCount(); + clearBeforeEachErrorRecoveryCount(); + + expect(window.sessionStorage.getItem(recoveryKey)).toBeNull(); + expect(getBeforeEachErrorRecoveryCount()).toBe(0); + expect(hasBeforeEachErrorRecoveryBeenAttempted()).toBe(false); + }); + + it("uses in-memory count when sessionStorage getItem throws", () => { + Object.defineProperty(window, "sessionStorage", { + configurable: true, + value: { + getItem: () => { + throw new Error("sessionStorage blocked"); + }, + setItem: jest.fn(), + removeItem: jest.fn(), + }, + }); + + incrementBeforeEachErrorRecoveryCount(); + incrementBeforeEachErrorRecoveryCount(); + + expect(hasBeforeEachErrorRecoveryBeenAttempted()).toBe(true); + expect(getBeforeEachErrorRecoveryCount()).toBe(2); + }); + + it("uses in-memory count when sessionStorage setItem throws", () => { + Object.defineProperty(window, "sessionStorage", { + configurable: true, + value: { + getItem: () => null, + setItem: () => { + throw new Error("sessionStorage blocked"); + }, + removeItem: jest.fn(), + }, + }); + + incrementBeforeEachErrorRecoveryCount(); + + expect(hasBeforeEachErrorRecoveryBeenAttempted()).toBe(true); + expect(getBeforeEachErrorRecoveryCount()).toBe(1); + }); +}); diff --git a/src/router/methods/before-each.js b/src/router/methods/before-each.js index 568cf312c..84e653551 100644 --- a/src/router/methods/before-each.js +++ b/src/router/methods/before-each.js @@ -9,7 +9,16 @@ 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 { + 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"; @@ -19,8 +28,21 @@ 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 @@ -63,7 +85,7 @@ export async function beforeEach(to, from) { } } - // Ensure session has not expired + // 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", @@ -141,18 +163,31 @@ export async function beforeEach(to, from) { { 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; + return false; } } diff --git a/src/router/methods/before-each.spec.js b/src/router/methods/before-each.spec.js new file mode 100644 index 000000000..caac0ef6f --- /dev/null +++ b/src/router/methods/before-each.spec.js @@ -0,0 +1,257 @@ +import analyticsMixin from "@/mixins/analytics-mixin"; +import { runExperiments } from "@/router/methods/helpers/run-experiments"; +import { handleHardError, redirectToStaticErrorPage } from "@/router/methods/error"; +import { sessionStorageKeyConstants } from "@/constants/session-storage"; +import { routeData } from "@/router/constants/routes"; +import { debugLog } from "@/helpers/debug-log-helper"; + +jest.mock("@/mixins/analytics-mixin", () => ({ + __esModule: true, + default: { + methods: { + validateSession: jest.fn(() => Promise.resolve()), + }, + }, +})); + +jest.mock("@/router/methods/helpers/run-experiments", () => ({ + runExperiments: jest.fn(() => Promise.resolve()), +})); + +jest.mock("@/helpers/heritage-integration/cookie-helper", () => ({ + getFunnelCookie: jest.fn(() => null), + updateOrCreateFunnelCookie: jest.fn(), +})); + +jest.mock("@/helpers/heritage-integration/session-helper", () => ({ + isSavedSessionStillActive: jest.fn(() => true), + getDateForSavedSessionTimeout: jest.fn(() => "2099-01-01T00:00:00.000Z"), +})); + +jest.mock("@/helpers/heritage-integration/navigation-helper", () => ({ + navigateToHeritageFunnel: jest.fn(), +})); + +jest.mock("@/router/methods/helpers/handle-heritage-return", () => ({ + handleHeritageReturn: jest.fn(() => Promise.resolve()), +})); + +jest.mock("@/router/methods/page-prerequisites", () => ({ + checkPagePrerequisites: jest.fn(() => Promise.resolve(true)), +})); + +jest.mock("@/helpers/debug-log-helper", () => ({ + debugLog: jest.fn(), + checkLogParam: jest.fn(), +})); + +jest.mock("@/router/methods/error", () => ({ + handleSoftError: jest.fn(() => Promise.resolve()), + handleHardError: jest.fn(() => Promise.resolve()), + redirectToStaticErrorPage: jest.fn(() => Promise.resolve()), +})); + +jest.mock("@/router", () => ({ + __esModule: true, + default: { + push: jest.fn(), + }, +})); + +import { beforeEach as routerBeforeEach } from "./before-each"; +import { + hasBeforeEachErrorRecoveryBeenAttempted, + incrementBeforeEachErrorRecoveryCount, + resetBeforeEachErrorRecoveryStateForTests, +} from "./before-each-error-recovery"; + +describe("router/methods/before-each", () => { + const recoveryKey = sessionStorageKeyConstants.BEFORE_EACH_ERROR_RECOVERY_COUNT; + + beforeEach(() => { + jest.clearAllMocks(); + resetBeforeEachErrorRecoveryStateForTests(); + window.sessionStorage.removeItem(recoveryKey); + window.sessionStorage.removeItem(sessionStorageKeyConstants.SUBMITTED_STATE); + analyticsMixin.methods.validateSession.mockResolvedValue(undefined); + debugLog.mockImplementation(() => undefined); + global.$logger = { + logError: jest.fn(), + }; + }); + + it("skips validateSession and runExperiments when navigating to ERROR", async () => { + await routerBeforeEach({ name: routeData.ERROR.name }, { name: routeData.VEHICLE.name }); + + expect(analyticsMixin.methods.validateSession).not.toHaveBeenCalled(); + expect(runExperiments).not.toHaveBeenCalled(); + }); + + it("skips validateSession and runExperiments when navigating to RESTART", async () => { + await routerBeforeEach({ name: routeData.RESTART.name }, { name: routeData.VEHICLE.name }); + + expect(analyticsMixin.methods.validateSession).not.toHaveBeenCalled(); + expect(runExperiments).not.toHaveBeenCalled(); + }); + + it("clears recovery counter after successful funnel navigation", async () => { + incrementBeforeEachErrorRecoveryCount(); + expect(hasBeforeEachErrorRecoveryBeenAttempted()).toBe(true); + + await routerBeforeEach( + { name: routeData.VEHICLE_DAMAGE.name }, + { name: routeData.VEHICLE.name } + ); + + expect(hasBeforeEachErrorRecoveryBeenAttempted()).toBe(false); + expect(window.sessionStorage.getItem(recoveryKey)).toBeNull(); + }); + + it("preserves recovery counter when passing through the ERROR recovery route", async () => { + incrementBeforeEachErrorRecoveryCount(); + expect(hasBeforeEachErrorRecoveryBeenAttempted()).toBe(true); + + await routerBeforeEach({ name: routeData.ERROR.name }, { name: routeData.VEHICLE.name }); + + // The counter must survive the ERROR bounce so consecutive failures can trip the + // circuit breaker; it is only cleared after a fully successful guard pass. + expect(hasBeforeEachErrorRecoveryBeenAttempted()).toBe(true); + expect(window.sessionStorage.getItem(recoveryKey)).toBe("1"); + }); + + it("runs validateSession on a normal funnel page", async () => { + await routerBeforeEach( + { name: routeData.VEHICLE_DAMAGE.name }, + { name: routeData.VEHICLE.name } + ); + + expect(analyticsMixin.methods.validateSession).toHaveBeenCalled(); + expect(runExperiments).toHaveBeenCalledWith(routeData.VEHICLE_DAMAGE.name); + }); + + it("calls handleHardError once on uncaught exception", async () => { + analyticsMixin.methods.validateSession.mockRejectedValue(new Error("session failure")); + + const result = await routerBeforeEach( + { name: routeData.VEHICLE_DAMAGE.name }, + { name: routeData.VEHICLE.name } + ); + + expect(handleHardError).toHaveBeenCalledTimes(1); + expect(handleHardError).toHaveBeenCalledWith( + expect.objectContaining({ + cause: "Uncaught exception in `beforeEach`.", + errorMessage: "session failure", + errorStack: expect.any(String), + }) + ); + expect(global.$logger.logError).toHaveBeenCalledWith( + "beforeEach uncaught exception", + expect.objectContaining({ errorMessage: "session failure" }) + ); + expect(redirectToStaticErrorPage).not.toHaveBeenCalled(); + expect(window.sessionStorage.getItem(recoveryKey)).toBe("1"); + expect(result).toBe(false); + }); + + it("redirects to static error page when exception occurs on ERROR route", async () => { + debugLog.mockImplementation(() => { + throw new Error("recovery route failure"); + }); + + const result = await routerBeforeEach( + { name: routeData.ERROR.name }, + { name: routeData.VEHICLE.name } + ); + + expect(global.$logger.logError).toHaveBeenCalledWith( + "beforeEach uncaught exception", + expect.objectContaining({ + errorMessage: "recovery route failure", + nextPage: routeData.ERROR.name, + }) + ); + expect(redirectToStaticErrorPage).toHaveBeenCalledWith( + expect.objectContaining({ + cause: "Uncaught exception in `beforeEach`.", + errorMessage: "recovery route failure", + nextPage: routeData.ERROR.name, + }) + ); + expect(handleHardError).not.toHaveBeenCalled(); + expect(window.sessionStorage.getItem(recoveryKey)).toBeNull(); + expect(result).toBe(false); + }); + + it("redirects to static error page on second uncaught exception in session", async () => { + analyticsMixin.methods.validateSession.mockRejectedValue(new Error("session failure")); + window.sessionStorage.setItem(recoveryKey, "1"); + + const result = await routerBeforeEach( + { name: routeData.VEHICLE_DAMAGE.name }, + { name: routeData.VEHICLE.name } + ); + + expect(global.$logger.logError).toHaveBeenCalledWith( + "beforeEach uncaught exception", + expect.objectContaining({ cause: "Uncaught exception in `beforeEach`." }) + ); + expect(redirectToStaticErrorPage).toHaveBeenCalled(); + expect(handleHardError).not.toHaveBeenCalled(); + expect(window.sessionStorage.getItem(recoveryKey)).toBeNull(); + expect(result).toBe(false); + }); + + it("redirects to static error page on second failure when sessionStorage throws", async () => { + analyticsMixin.methods.validateSession.mockRejectedValue(new Error("session failure")); + + const storageBlockedError = new Error("sessionStorage blocked"); + const underlyingStorage = window.sessionStorage; + Object.defineProperty(window, "sessionStorage", { + configurable: true, + value: { + getItem(key) { + if (key === recoveryKey) { + throw storageBlockedError; + } + return underlyingStorage.getItem(key); + }, + setItem(key, value) { + if (key === recoveryKey) { + throw storageBlockedError; + } + underlyingStorage.setItem(key, value); + }, + removeItem(key) { + if (key === recoveryKey) { + throw storageBlockedError; + } + underlyingStorage.removeItem(key); + }, + }, + }); + + await routerBeforeEach( + { name: routeData.VEHICLE_DAMAGE.name }, + { name: routeData.VEHICLE.name } + ); + expect(handleHardError).toHaveBeenCalledTimes(1); + expect(redirectToStaticErrorPage).not.toHaveBeenCalled(); + + jest.clearAllMocks(); + + const result = await routerBeforeEach( + { name: routeData.VEHICLE_DAMAGE.name }, + { name: routeData.VEHICLE.name } + ); + + expect(redirectToStaticErrorPage).toHaveBeenCalled(); + expect(handleHardError).not.toHaveBeenCalled(); + expect(result).toBe(false); + + Object.defineProperty(window, "sessionStorage", { + configurable: true, + value: underlyingStorage, + }); + }); +}); diff --git a/src/router/methods/error.js b/src/router/methods/error.js index 15cabb9a0..031f2367b 100644 --- a/src/router/methods/error.js +++ b/src/router/methods/error.js @@ -3,9 +3,41 @@ import { routeData } from "@/router/constants/routes"; import router from "@/router"; import store from "@/store"; import { storeActions } from "@/constants/store-actions"; +import { sessionStorageKeyConstants } from "@/constants/session-storage"; import { deleteFunnelCookie } from "@/helpers/heritage-integration/cookie-helper"; +import { clearBeforeEachErrorRecoveryCount } from "@/router/methods/before-each-error-recovery"; import experimentMixin from "../../mixins/experiment-mixin"; +const STATIC_ERROR_PAGE_PATH = "/fmg/static/error"; + +/** Clears sessionStorage keys that can re-poison the funnel after a bailout. */ +export function clearBailoutSessionStorage() { + clearBeforeEachErrorRecoveryCount(); + + try { + window.sessionStorage?.removeItem(sessionStorageKeyConstants.SUBMITTED_STATE); + window.sessionStorage?.removeItem( + sessionStorageKeyConstants.HERITAGE_SUPPRESS_REDIRECT_COUNT + ); + window.sessionStorage?.removeItem(sessionStorageKeyConstants.EXTERNAL_PARAMETER_STATE); + } catch { + // sessionStorage unavailable + } +} + +export async function redirectToStaticErrorPage(errorPayload) { + await logErrorToDataLayerAndLogger("redirectToStaticErrorPage", errorPayload, null); + clearBailoutSessionStorage(); + + // Prefer top-level navigation so the static page loads outside any iframe wrapper. + // Assigning window.top.location throws SecurityError when cross-origin; fall back to this frame. + try { + window.top.location = STATIC_ERROR_PAGE_PATH; + } catch { + window.location = STATIC_ERROR_PAGE_PATH; + } +} + export async function handleSoftError(errorPayload, forceRestart = false) { if (forceRestart) { await store.dispatch(storeActions.RESET_STATE); @@ -27,8 +59,7 @@ export async function handleHardError(errorPayload) { ); if (isInStaticErrorExperiment) { - logErrorToDataLayerAndLogger("handleHardError", errorPayload, null); - window.top.location = "/fmg/static/error"; + await redirectToStaticErrorPage(errorPayload); return; } else { await handleSoftError(errorPayload, true); diff --git a/src/router/methods/error.spec.js b/src/router/methods/error.spec.js index c48d63fda..262347fc6 100644 --- a/src/router/methods/error.spec.js +++ b/src/router/methods/error.spec.js @@ -1,10 +1,11 @@ -import { handleSoftError, handleHardError } from "./error"; +import { handleSoftError, handleHardError, redirectToStaticErrorPage } 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 { sessionStorageKeyConstants } from "@/constants/session-storage"; import { routeData } from "@/router/constants/routes"; jest.mock("@/router", () => ({ @@ -98,6 +99,126 @@ describe("router/methods/error", () => { }); }); + describe("redirectToStaticErrorPage", () => { + 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", async () => { + await redirectToStaticErrorPage(errorPayload); + + expect(analyticsMixin.methods.pushPageErrorToDataLayer).toHaveBeenCalledWith( + errorPayload + ); + expect(global.$logger.logError).toHaveBeenCalledWith( + "redirectToStaticErrorPage: test cause", + errorPayload + ); + expect(topLocationValue).toBe("/fmg/static/error"); + }); + + it("clears bailout sessionStorage keys before navigating away", async () => { + window.sessionStorage.setItem(sessionStorageKeyConstants.SUBMITTED_STATE, "not-json"); + window.sessionStorage.setItem( + sessionStorageKeyConstants.HERITAGE_SUPPRESS_REDIRECT_COUNT, + "3" + ); + window.sessionStorage.setItem( + sessionStorageKeyConstants.BEFORE_EACH_ERROR_RECOVERY_COUNT, + "1" + ); + window.sessionStorage.setItem( + sessionStorageKeyConstants.EXTERNAL_PARAMETER_STATE, + "{}" + ); + + await redirectToStaticErrorPage(errorPayload); + + expect( + window.sessionStorage.getItem(sessionStorageKeyConstants.SUBMITTED_STATE) + ).toBeNull(); + expect( + window.sessionStorage.getItem( + sessionStorageKeyConstants.HERITAGE_SUPPRESS_REDIRECT_COUNT + ) + ).toBeNull(); + expect( + window.sessionStorage.getItem( + sessionStorageKeyConstants.BEFORE_EACH_ERROR_RECOVERY_COUNT + ) + ).toBeNull(); + expect( + window.sessionStorage.getItem(sessionStorageKeyConstants.EXTERNAL_PARAMETER_STATE) + ).toBeNull(); + }); + + it("falls back to window.location when window.top.location setter throws", async () => { + let windowLocationValue; + Object.defineProperty(window, "location", { + configurable: true, + set(value) { + windowLocationValue = value; + }, + get() { + return windowLocationValue; + }, + }); + Object.defineProperty(window.top, "location", { + configurable: true, + set() { + throw new Error("cross-origin frame"); + }, + get() { + return undefined; + }, + }); + + await redirectToStaticErrorPage(errorPayload); + + expect(windowLocationValue).toBe("/fmg/static/error"); + expect(topLocationValue).toBeUndefined(); + }); + + it("awaits logging before navigating away", async () => { + const callOrder = []; + analyticsMixin.methods.pushPageErrorToDataLayer.mockImplementation(() => { + callOrder.push("datalayer"); + }); + global.$logger.logError.mockImplementation(() => { + callOrder.push("logger"); + }); + Object.defineProperty(window.top, "location", { + configurable: true, + set(value) { + callOrder.push("navigate"); + topLocationValue = value; + }, + get() { + return topLocationValue; + }, + }); + + await redirectToStaticErrorPage(errorPayload); + + expect(callOrder).toEqual(["datalayer", "logger", "navigate"]); + }); + }); + describe("handleHardError", () => { let topLocationValue; @@ -131,7 +252,7 @@ describe("router/methods/error", () => { errorPayload ); expect(global.$logger.logError).toHaveBeenCalledWith( - "handleHardError: test cause", + "redirectToStaticErrorPage: test cause", errorPayload ); expect(topLocationValue).toBe("/fmg/static/error"); diff --git a/src/router/methods/route-logic/error.js b/src/router/methods/route-logic/error.js index 162e5b914..93cfc3cff 100644 --- a/src/router/methods/route-logic/error.js +++ b/src/router/methods/route-logic/error.js @@ -25,6 +25,8 @@ export async function errorBeforeEnter(to, from) { // If we've already encountered one error, clear session to avoid more. // Otherwise mark that we encountered an error here. + // Note: an earlier breaker also runs in before-each.js via before-each-error-recovery.js + // (consecutive uncaught guard failures before this route handler runs). if (store.getters.applicationUser.hasAlreadyTriggeredError) { const errorPayload = { cause: "Successive errors triggered.", diff --git a/src/store/index.js b/src/store/index.js index 3f2cf5e0d..542f68e02 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -3733,7 +3733,7 @@ export const actions = { context.commit(storeMutations.UPDATE_EXPERIMENTS, experiments); //clear externalParameter session storage - window.sessionStorage.removeItem("externalParameterState"); + window.sessionStorage.removeItem(sessionStorageKeyConstants.EXTERNAL_PARAMETER_STATE); //restore affiliate cookies context.commit(storeMutations.UPDATE_AFFILIATE_COOKIES, affiliateCookies); @@ -4447,14 +4447,21 @@ function createExternalParameterDefaultState() { saveExternalParameterState(externalParameterDefaultState); } function getExternalParameterDefaultState() { - const externalParameterState = window.sessionStorage.getItem("externalParameterState"); + const externalParameterState = window.sessionStorage.getItem( + sessionStorageKeyConstants.EXTERNAL_PARAMETER_STATE + ); if (externalParameterState === null) { createExternalParameterDefaultState(); } - return JSON.parse(window.sessionStorage.getItem("externalParameterState")); + return JSON.parse( + window.sessionStorage.getItem(sessionStorageKeyConstants.EXTERNAL_PARAMETER_STATE) + ); } function saveExternalParameterState(externalParameterState) { - window.sessionStorage.setItem("externalParameterState", JSON.stringify(externalParameterState)); + window.sessionStorage.setItem( + sessionStorageKeyConstants.EXTERNAL_PARAMETER_STATE, + JSON.stringify(externalParameterState) + ); } const timeSlotCallFlags = { From 9e086175820dbd5c8abb6bb0bfd61f276ddcad4c Mon Sep 17 00:00:00 2001 From: credelinghuys Date: Thu, 11 Jun 2026 09:09:10 -0400 Subject: [PATCH 4/8] CASH-2855: Bailout when required VIN is not found --- src/layouts/address-lookup/address-lookup.vue | 32 ++++++++- .../license-plate-lookup.vue | 19 +++++- src/layouts/vin-lookup/vin-lookup.spec.js | 16 +++++ src/layouts/vin-lookup/vin-lookup.vue | 65 +++++++++++++++++-- src/mixins/vin-pages-mixin.js | 5 +- src/router/constants/routing-table.js | 4 ++ 6 files changed, 131 insertions(+), 10 deletions(-) diff --git a/src/layouts/address-lookup/address-lookup.vue b/src/layouts/address-lookup/address-lookup.vue index 081be8def..f79ee5667 100644 --- a/src/layouts/address-lookup/address-lookup.vue +++ b/src/layouts/address-lookup/address-lookup.vue @@ -355,7 +355,15 @@ export default { } } else { // No VINS found. - this.displayVinNotFoundAlert = true; + const isVinLookupRequired = this.$store.getters.vehicle.vinRequired; + if (isVinLookupRequired) { + this.continueWithFailedLookup( + this.customerQuestions, + resultMap.serviceZipValidationResponse + ); + } else { + this.displayVinNotFoundAlert = true; + } return this.$refs.navbar.removeLoader(); } @@ -462,6 +470,28 @@ export default { this.displayVinLookupByHomeAddressNotAllowedAlert = false; this.displayNoServiceAlert = false; }, + async continueWithFailedLookup(customerQuestions, zipCodeValidationResponse) { + // Save the entered customer questions and service zip code (if applicable) so that it can be used in the next page if needed + await this.dispatchStoreAction( + this.storeActions.SAVE_CUSTOMER_DETAILS, + { + firstName: customerQuestions.firstName, + lastName: customerQuestions.lastName, + }, + false + ); + await this.dispatchStoreAction( + storeActions.SAVE_SERVICE_ZIP_CODE_INFO, + { + state: zipCodeValidationResponse.state, + zipCode: this.serviceZipCode || this.customerQuestions.addressQuestions.zipCode, + zipCodeCtu: zipCodeValidationResponse.zipCodeCtu, + }, + false + ); + + this.navigateForwardWithSingleCarMatch(); + }, }, mounted() { this.attachCustomEvents(); diff --git a/src/layouts/license-plate-lookup/license-plate-lookup.vue b/src/layouts/license-plate-lookup/license-plate-lookup.vue index a1477a664..a3873e437 100644 --- a/src/layouts/license-plate-lookup/license-plate-lookup.vue +++ b/src/layouts/license-plate-lookup/license-plate-lookup.vue @@ -254,7 +254,12 @@ export default { resultMap.registrationZipValidationResponse.state ).catch(() => { // No VIN found. - this.displayVinNotFoundAlert = true; + const isVinLookupRequired = this.$store.getters.vehicle.vinRequired; + if (isVinLookupRequired) { + this.continueWithFailedLookup(resultMap.serviceZipValidationResponse); + } else { + this.displayVinNotFoundAlert = true; + } return this.$refs.navbar.removeLoader(); }); // If no VIN was found, stop processing after displaying alert @@ -393,6 +398,18 @@ export default { this.displayInvalidZipAlert = false; this.displayNoServiceAlert = false; }, + async continueWithFailedLookup(zipCodeData) { + await this.dispatchStoreAction( + storeActions.SAVE_SERVICE_ZIP_CODE_INFO, + { + state: zipCodeData.state, + zipCode: this.serviceZipCode || this.registrationZipCode, + zipCodeCtu: zipCodeData.zipCodeCtu, + }, + false + ); + this.navigateForwardWithSingleCarMatch(); + }, }, mounted() { this.attachCustomEvents(); diff --git a/src/layouts/vin-lookup/vin-lookup.spec.js b/src/layouts/vin-lookup/vin-lookup.spec.js index 7a5ba37eb..5fe09edb2 100644 --- a/src/layouts/vin-lookup/vin-lookup.spec.js +++ b/src/layouts/vin-lookup/vin-lookup.spec.js @@ -11,9 +11,20 @@ jest.mock("@/store", () => ({ commit: jest.fn(), dispatch: jest.fn(), getters: { + applicationUser: { + bailoutCode: null, + }, + externalParameterState: { + isExternalParameter: false, + }, + emailOrSms: "email", vehicle: { year: 2019, carId: "C00000", + vin: null, + vinRequired: false, + isBigTruck: false, + canSafeliteService: true, }, order: { serviceLocation: { @@ -23,6 +34,11 @@ jest.mock("@/store", () => ({ emailAddress: "builddigitaltest@safelite.com", }, referralNumber: "666666", + vehicle: { + make: null, + model: null, + year: null, + }, }, payment: { insuranceCoverage: { diff --git a/src/layouts/vin-lookup/vin-lookup.vue b/src/layouts/vin-lookup/vin-lookup.vue index ae80f670f..67d7a2d3f 100644 --- a/src/layouts/vin-lookup/vin-lookup.vue +++ b/src/layouts/vin-lookup/vin-lookup.vue @@ -93,7 +93,8 @@ vinPopulatedOnPageLoad && !isInsuranceVerified && !displayInvalidZipAlert && - !displayNonServiceableZipAlert + !displayNonServiceableZipAlert && + !requiredVinNotFound " alertClass="alert-success" /> @@ -144,6 +145,7 @@ import baseMixin from "@/mixins/base-mixin.js"; import store from "@/store"; import vinPagesMixin from "@/mixins/vin-pages-mixin"; import { routeData } from "@/router/constants/routes"; +import { bailoutCodes } from "@/constants/bailout-codes"; // DEFINE VALIDATION RULES defineRule("zip-required", required(errorMessages.SERVICE_ZIP_REQUIRED)); @@ -202,6 +204,7 @@ export default { displayMatchedDifferentVehicleAlert: false, displayVinScanFailedAlert: false, displayNoServiceAlert: false, + requiredVinNotFound: this.getRequiredVinNotFound(), }; }, methods: { @@ -289,7 +292,13 @@ export default { if (!resultMap.vehicleLookupResponse || !resultMap.zipCodeData.isServiceable) { // If the vehicle result is undefined, the vin entered was invalid. if (!resultMap.vehicleLookupResponse) { - this.displayVinNotFoundAlert = true; + const isVinLookupRequired = this.$store.getters.vehicle.vinRequired; + if (isVinLookupRequired) { + this.requiredVinNotFound = true; + this.continueWithFailedVin(this.vin, resultMap.zipCodeData); + } else { + this.displayVinNotFoundAlert = true; + } } // Check if Service Zip entered is serviceable, if not display an alert @@ -303,18 +312,18 @@ export default { // Check if the CarId is different from the lookup vs what is in state currently. this.isCarIdDifferent = - resultMap.vehicleLookupResponse.carId !== this.$store.getters.vehicle.carId; + resultMap.vehicleLookupResponse?.carId !== this.$store.getters.vehicle.carId; if ( this.isCarIdDifferent && - resultMap.vehicleLookupResponse.carId !== this.previouslyEnteredCarId + resultMap.vehicleLookupResponse?.carId !== this.previouslyEnteredCarId ) { - this.previouslyEnteredCarId = resultMap.vehicleLookupResponse.carId; + this.previouslyEnteredCarId = resultMap.vehicleLookupResponse?.carId; this.customAlertData.vehicleInfo = resultMap.vehicleLookupResponse; this.displayMatchedDifferentVehicleAlert = true; this.isSelectedGlassAvailableForVehicle = await isGlassAvailableForCarId( - resultMap.vehicleLookupResponse.carId, + resultMap.vehicleLookupResponse?.carId, "vin-lookup" ); @@ -486,10 +495,52 @@ export default { this.displayVinNotFoundAlert = false; this.displayVinScanFailedAlert = false; this.displayNoServiceAlert = false; + this.requiredVinNotFound = false; + }, + async continueWithFailedVin(vin, zipCodeData) { + var selectedVehicle = { + vin: vin, + vehicle: { + carId: this.carId, + make: this.$store.getters.order.vehicle.make || null, + model: this.$store.getters.order.vehicle.model || null, + year: this.$store.getters.order.vehicle.year || null, + }, + }; + await this.dispatchStoreAction( + storeActions.SAVE_VIN, + { + vehicleInfo: Object.assign(selectedVehicle.vehicle, { + vin: vin, + }), + isSelectedGlassAvailableForVehicle: this.isSelectedGlassAvailableForVehicle, + }, + false + ); + await this.dispatchStoreAction( + storeActions.SAVE_SERVICE_ZIP_CODE_INFO, + { + state: zipCodeData.state, + zipCode: this.serviceZipCode, + zipCodeCtu: zipCodeData.zipCodeCtu, + }, + false + ); + this.navigateForwardWithSingleCarMatch(); + }, + getRequiredVinNotFound() { + // Prevents success alert from showing + var bailoutCode = this.$store.getters.applicationUser.bailoutCode; + var vinRequired = this.$store.getters.vehicle.vinRequired; + if (vinRequired && this.vin && bailoutCode == bailoutCodes.PART_NOT_FOUND) { + return true; + } + return false; }, }, - mounted() { + async mounted() { this.attachCustomEvents(); + this.requiredVinNotFound = this.getRequiredVinNotFound(); }, computed: { AlertMatchedDifferentVehicleHeader() { diff --git a/src/mixins/vin-pages-mixin.js b/src/mixins/vin-pages-mixin.js index c593d88b4..78cb134cf 100644 --- a/src/mixins/vin-pages-mixin.js +++ b/src/mixins/vin-pages-mixin.js @@ -38,7 +38,10 @@ export default { }); if (result.PartNotFound) { - bailoutMixin.methods.navigateToBailoutPage(this, bailoutCodes.PART_NOT_FOUND); + return bailoutMixin.methods.navigateToBailoutPage( + this, + bailoutCodes.PART_NOT_FOUND + ); } const partsOrQuestions = result.data.partsOrQuestions; diff --git a/src/router/constants/routing-table.js b/src/router/constants/routing-table.js index ea2020a21..3a5d12860 100644 --- a/src/router/constants/routing-table.js +++ b/src/router/constants/routing-table.js @@ -181,6 +181,10 @@ const routingTable = function () { scenario: navigationScenarios.CLICKED_VIN_RETRY, destinationPageData: routeData.ESTIMATE, }, + { + scenario: navigationScenarios.BAILOUT, + destinationPageData: routeData.BAILOUT, + }, ], }, { From df5aba062f2b67d5a776568c9449dcb1ace8e07a Mon Sep 17 00:00:00 2001 From: credelinghuys Date: Thu, 11 Jun 2026 09:48:01 -0400 Subject: [PATCH 5/8] CASH-2855: Save all current vehicle data with vin --- src/layouts/vin-lookup/vin-lookup.vue | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/layouts/vin-lookup/vin-lookup.vue b/src/layouts/vin-lookup/vin-lookup.vue index 67d7a2d3f..5d89c40c7 100644 --- a/src/layouts/vin-lookup/vin-lookup.vue +++ b/src/layouts/vin-lookup/vin-lookup.vue @@ -500,12 +500,7 @@ export default { async continueWithFailedVin(vin, zipCodeData) { var selectedVehicle = { vin: vin, - vehicle: { - carId: this.carId, - make: this.$store.getters.order.vehicle.make || null, - model: this.$store.getters.order.vehicle.model || null, - year: this.$store.getters.order.vehicle.year || null, - }, + vehicle: this.$store.getters.vehicle, }; await this.dispatchStoreAction( storeActions.SAVE_VIN, From d4f9c210415358bb5e0c6b28ccf15110c1bf27f3 Mon Sep 17 00:00:00 2001 From: Carl Nation Date: Thu, 11 Jun 2026 13:25:32 -0400 Subject: [PATCH 6/8] CASH-2844 new fields CASH-2844 new fields for logging --- src/mixins/analytics-mixin.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/mixins/analytics-mixin.js b/src/mixins/analytics-mixin.js index c07240498..1a9df51f7 100644 --- a/src/mixins/analytics-mixin.js +++ b/src/mixins/analytics-mixin.js @@ -264,6 +264,9 @@ export default { sessionData.coverageSubStatus = order?.payment?.insuranceCoverage?.coverageSubStatus; sessionData.isNoComp = order?.policy?.isNoComp; sessionData.isItac = order?.policy?.isItac; + sessionData.isClaimAndCoverage = order.payment.isClaimAndCoverage; + sessionData.vinRequired = order.vehicle.vinRequired ?? false; + sessionData.installOemGlass = order.damage?.installOemGlass ?? false; sessionData.subTotalPrice = getSubTotal(order?.lineItems); sessionData.totalPrice = getAmountDue(order?.lineItems, true); sessionData.userAgent = navigator.userAgent; From 653045bcd52a87f306f1fa6f224054f76b99a8ae Mon Sep 17 00:00:00 2001 From: Minojhini Valaiyapathi Date: Thu, 11 Jun 2026 13:44:59 -0400 Subject: [PATCH 7/8] CASH-2856 - Pointed to v3 of Parts Service for parts-or-question endpoint for SL vehicles --- src/constants/endpoints.js | 4 ++-- src/store/index.js | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/constants/endpoints.js b/src/constants/endpoints.js index d6b3e3431..76077db76 100644 --- a/src/constants/endpoints.js +++ b/src/constants/endpoints.js @@ -56,8 +56,8 @@ const endpoints = { url: "/parts/api/v1/parts/parts-or-questions", method: "POST", }, - GetPartsOrQuestionsV2: { - url: "/parts/api/v2/parts/parts-or-questions", + GetPartsOrQuestionsV3: { + url: "/parts/api/v3/parts/parts-or-questions", method: "POST", }, GetParts: { diff --git a/src/store/index.js b/src/store/index.js index 5e69005e9..503c2baef 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1971,7 +1971,7 @@ export const actions = { const glassArrayForPayload = convertGlassPieceNamingForApi(glassArray); const partsOrQuestionsEndpoint = vehicle.vinRequired - ? endpoints.GetPartsOrQuestionsV2 + ? endpoints.GetPartsOrQuestionsV3 : endpoints.GetPartsOrQuestions; const response = await globalMethods @@ -2020,6 +2020,7 @@ export const actions = { const vehicle = context.getters.vehicle; const damage = context.getters.damage; const order = context.state.order; + const payment = context.getters.payment; const carId = vehicle.carId; const glassArray = damage.glassToReplace; @@ -2028,6 +2029,7 @@ export const actions = { const vin = vehicle.vin; const serviceType = order.serviceLocation?.appointmentType; const referralSeqNumber = order.referralSequenceNumber; + const parentAccountNumber = payment.parentAccountNumber; // create a new array to avoid mutating state const glassArrayForPayload = convertGlassPieceNamingForApi(glassArray); @@ -2044,6 +2046,7 @@ export const actions = { vin: vin, serviceType: serviceType, referralSeqNumber: referralSeqNumber, + parentAccountNumber: parentAccountNumber, }, logApiCall: true, pageNameToLog: pageNameToLog, From dfd805b3926a3b81017b61945d6f267f35c3725a Mon Sep 17 00:00:00 2001 From: Carl Nation Date: Thu, 11 Jun 2026 14:01:08 -0400 Subject: [PATCH 8/8] track page name track page name so it can logged to cloudwatch --- src/constants/header-keys.js | 1 + src/global-methods.js | 1 + 2 files changed, 2 insertions(+) diff --git a/src/constants/header-keys.js b/src/constants/header-keys.js index ed88393b5..4341d416b 100644 --- a/src/constants/header-keys.js +++ b/src/constants/header-keys.js @@ -6,4 +6,5 @@ export const headerKeys = { TRANSACTION_ID: "X-Transaction-Id", EON: "X-Enterprise-Order-Number", LOG_ENABLED: "log-enabled", + PAGE_NAME_TO_LOG: "X-Page-Name-To-Log", }; diff --git a/src/global-methods.js b/src/global-methods.js index f408230f8..584743b2d 100644 --- a/src/global-methods.js +++ b/src/global-methods.js @@ -69,6 +69,7 @@ export default { [headerKeys.APPLICATION_NAME]: applicationConfig.APPLICATION_NAME, [headerKeys.SESSION_SEQUENCE_NUMBER]: getSessionKeyValue(), [headerKeys.REFERRAL_SEQUENCE_NUMBER]: order?.referralSequenceNumber, + [headerKeys.PAGE_NAME_TO_LOG]: pageNameToLog, [headerKeys.TRANSACTION_ID]: crypto.randomUUID(), [headerKeys.EON]: order?.eon, [headerKeys.LOG_ENABLED]: store.getters.applicationUser?.loggingOption ?? false,