From 6f6da14cc85c07f85df4241a277a5b80d2c63331 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?= <71827178+bosiraphael@users.noreply.github.com> Date: Tue, 4 Aug 2026 10:24:59 +0200 Subject: [PATCH] Stop showing raw chunk load errors (#23571) On iOS Safari, a failed chunk load showed a snackbar containing the raw browser string `Importing a module script failed.` `PromiseRejectionEffect` snackbars the raw `error.message` of any unhandled rejection, so a floating dynamic import leaked browser internals to the user. It now skips the snackbar for stale chunk errors, which are still captured by Sentry. This only changes what the user sees, not the underlying fetch failure. --- .../components/PromiseRejectionEffect.tsx | 7 +- .../__tests__/PromiseRejectionEffect.test.tsx | 71 +++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 packages/twenty-front/src/modules/error-handler/components/__tests__/PromiseRejectionEffect.test.tsx diff --git a/packages/twenty-front/src/modules/error-handler/components/PromiseRejectionEffect.tsx b/packages/twenty-front/src/modules/error-handler/components/PromiseRejectionEffect.tsx index 4f4e7c6a7e..b29f7051ca 100644 --- a/packages/twenty-front/src/modules/error-handler/components/PromiseRejectionEffect.tsx +++ b/packages/twenty-front/src/modules/error-handler/components/PromiseRejectionEffect.tsx @@ -1,5 +1,6 @@ import { useCallback, useEffect } from 'react'; +import { checkIfItsAViteStaleChunkLazyLoadingError } from '@/error-handler/utils/checkIfItsAViteStaleChunkLazyLoadingError'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { CombinedGraphQLErrors, @@ -44,7 +45,11 @@ export const PromiseRejectionEffect = () => { error?.networkError?.name === 'AbortError' || error?.name === 'AbortError'; - if (!isAbortError) { + const isViteStaleChunkLazyLoadingError = + error instanceof Error && + checkIfItsAViteStaleChunkLazyLoadingError(error); + + if (!isAbortError && !isViteStaleChunkLazyLoadingError) { enqueueErrorSnackBar( error instanceof Error ? { message: error.message } : {}, ); diff --git a/packages/twenty-front/src/modules/error-handler/components/__tests__/PromiseRejectionEffect.test.tsx b/packages/twenty-front/src/modules/error-handler/components/__tests__/PromiseRejectionEffect.test.tsx new file mode 100644 index 0000000000..a52ed4b321 --- /dev/null +++ b/packages/twenty-front/src/modules/error-handler/components/__tests__/PromiseRejectionEffect.test.tsx @@ -0,0 +1,71 @@ +import { render, waitFor } from '@testing-library/react'; + +import { PromiseRejectionEffect } from '@/error-handler/components/PromiseRejectionEffect'; + +jest.mock('@sentry/react', () => ({ + captureException: jest.fn(), +})); + +const enqueueErrorSnackBar = jest.fn(); + +jest.mock('@/ui/feedback/snack-bar-manager/hooks/useSnackBar', () => ({ + useSnackBar: () => ({ enqueueErrorSnackBar }), +})); + +const { captureException } = jest.requireMock('@sentry/react'); + +const dispatchUnhandledRejection = (reason: unknown) => { + const event = new Event('unhandledrejection'); + Object.defineProperty(event, 'reason', { value: reason }); + + window.dispatchEvent(event); +}; + +describe('PromiseRejectionEffect', () => { + beforeEach(() => { + jest.clearAllMocks(); + render(); + }); + + it('should not snackbar a stale chunk error', async () => { + dispatchUnhandledRejection(new Error('Importing a module script failed.')); + + await waitFor(() => { + expect(captureException).toHaveBeenCalledTimes(1); + }); + + expect(enqueueErrorSnackBar).not.toHaveBeenCalled(); + }); + + it('should still snackbar an unrelated error', async () => { + dispatchUnhandledRejection(new Error('Some unrelated error')); + + expect(enqueueErrorSnackBar).toHaveBeenCalledWith({ + message: 'Some unrelated error', + }); + + await waitFor(() => { + expect(captureException).toHaveBeenCalledTimes(1); + }); + }); + + it('should not snackbar an abort error', async () => { + dispatchUnhandledRejection({ name: 'AbortError' }); + + await waitFor(() => { + expect(captureException).toHaveBeenCalledTimes(1); + }); + + expect(enqueueErrorSnackBar).not.toHaveBeenCalled(); + }); + + it('should snackbar a generic message when the reason is not an Error', async () => { + dispatchUnhandledRejection('something went wrong'); + + expect(enqueueErrorSnackBar).toHaveBeenCalledWith({}); + + await waitFor(() => { + expect(captureException).toHaveBeenCalledTimes(1); + }); + }); +});