From c90852c46680b3a424d5fcb2e5efbec717989c2d Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Wed, 8 Jan 2025 14:11:23 +0100 Subject: [PATCH 1/3] add failed test --- .../app/browser/error-event/page.js | 20 ++++++++ .../capture-console-error.test.ts | 47 +++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 test/development/app-dir/capture-console-error/app/browser/error-event/page.js diff --git a/test/development/app-dir/capture-console-error/app/browser/error-event/page.js b/test/development/app-dir/capture-console-error/app/browser/error-event/page.js new file mode 100644 index 0000000000000..345a48878a53c --- /dev/null +++ b/test/development/app-dir/capture-console-error/app/browser/error-event/page.js @@ -0,0 +1,20 @@ +'use client' + +export default function Page() { + return ( + + ) +} diff --git a/test/development/app-dir/capture-console-error/capture-console-error.test.ts b/test/development/app-dir/capture-console-error/capture-console-error.test.ts index bd22616d99a71..ba8b2a6a084e8 100644 --- a/test/development/app-dir/capture-console-error/capture-console-error.test.ts +++ b/test/development/app-dir/capture-console-error/capture-console-error.test.ts @@ -315,4 +315,51 @@ describe('app-dir - capture-console-error', () => { `) } }) + + it('should display the error message in error event when event.error is not present', async () => { + const browser = await next.browser('/browser/error-event') + await browser.elementByCss('button').click() + + await openRedbox(browser) + + const result = await getRedboxResult(browser) + + if (process.env.TURBOPACK) { + expect(result).toMatchInlineSnapshot(` + { + "callStacks": "", + "count": 1, + "description": "", + "source": "app/browser/error-event/page.js (14:16) @ onClick + + 12 | + 13 | // Dispatch the event + > 14 | window.dispatchEvent(errorEvent) + | ^ + 15 | }} + 16 | > + 17 | click to trigger error event", + "title": "Console Error", + } + `) + } else { + expect(result).toMatchInlineSnapshot(` + { + "callStacks": "", + "count": 1, + "description": "", + "source": "app/browser/error-event/page.js (14:16) @ onClick + + 12 | + 13 | // Dispatch the event + > 14 | window.dispatchEvent(errorEvent) + | ^ + 15 | }} + 16 | > + 17 | click to trigger error event", + "title": "Console Error", + } + `) + } + }) }) From 97e2b0612078b03dd5570ec5fbab8c4618a45dc2 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Wed, 8 Jan 2025 14:13:50 +0100 Subject: [PATCH 2/3] Fix presentation when window.onerror reports, skip it --- .../internal/helpers/use-error-handler.ts | 6 ++- .../capture-console-error.test.ts | 45 ++----------------- test/lib/next-test-utils.ts | 15 +++++++ 3 files changed, 24 insertions(+), 42 deletions(-) diff --git a/packages/next/src/client/components/react-dev-overlay/internal/helpers/use-error-handler.ts b/packages/next/src/client/components/react-dev-overlay/internal/helpers/use-error-handler.ts index 3fdfd27831179..a7ad315cc6bed 100644 --- a/packages/next/src/client/components/react-dev-overlay/internal/helpers/use-error-handler.ts +++ b/packages/next/src/client/components/react-dev-overlay/internal/helpers/use-error-handler.ts @@ -77,7 +77,11 @@ function onUnhandledError(event: WindowEventMap['error']): void | boolean { event.preventDefault() return false } - handleClientError(event.error, []) + // When there's an error property present, we log the error to error overlay. + // Otherwise we don't do anything as it's not logging in the console either. + if (event.error) { + handleClientError(event.error, []) + } } function onUnhandledRejection(ev: WindowEventMap['unhandledrejection']): void { diff --git a/test/development/app-dir/capture-console-error/capture-console-error.test.ts b/test/development/app-dir/capture-console-error/capture-console-error.test.ts index ba8b2a6a084e8..35c6cc68462e0 100644 --- a/test/development/app-dir/capture-console-error/capture-console-error.test.ts +++ b/test/development/app-dir/capture-console-error/capture-console-error.test.ts @@ -7,6 +7,8 @@ import { getRedboxTotalErrorCount, openRedbox, hasRedboxCallStack, + assertNoRedbox, + assertNoConsoleErrors, } from 'next-test-utils' async function getRedboxResult(browser: any) { @@ -320,46 +322,7 @@ describe('app-dir - capture-console-error', () => { const browser = await next.browser('/browser/error-event') await browser.elementByCss('button').click() - await openRedbox(browser) - - const result = await getRedboxResult(browser) - - if (process.env.TURBOPACK) { - expect(result).toMatchInlineSnapshot(` - { - "callStacks": "", - "count": 1, - "description": "", - "source": "app/browser/error-event/page.js (14:16) @ onClick - - 12 | - 13 | // Dispatch the event - > 14 | window.dispatchEvent(errorEvent) - | ^ - 15 | }} - 16 | > - 17 | click to trigger error event", - "title": "Console Error", - } - `) - } else { - expect(result).toMatchInlineSnapshot(` - { - "callStacks": "", - "count": 1, - "description": "", - "source": "app/browser/error-event/page.js (14:16) @ onClick - - 12 | - 13 | // Dispatch the event - > 14 | window.dispatchEvent(errorEvent) - | ^ - 15 | }} - 16 | > - 17 | click to trigger error event", - "title": "Console Error", - } - `) - } + await assertNoRedbox(browser) + await assertNoConsoleErrors(browser) }) }) diff --git a/test/lib/next-test-utils.ts b/test/lib/next-test-utils.ts index f0800cb6a669d..d4993ae128c66 100644 --- a/test/lib/next-test-utils.ts +++ b/test/lib/next-test-utils.ts @@ -1545,3 +1545,18 @@ export function createNowRouteMatches( return urlSearchParams } + +export async function assertNoConsoleErrors(browser: BrowserInterface) { + const logs = await browser.log() + const warningsAndErrors = logs.filter((log) => { + return ( + log.source === 'warning' || + (log.source === 'error' && + // These are expected when we visit 404 pages. + log.message !== + 'Failed to load resource: the server responded with a status of 404 (Not Found)') + ) + }) + + expect(warningsAndErrors).toEqual([]) +} From e2f412d2c38683143a7f170c39a90a8154eac7f2 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Thu, 9 Jan 2025 19:09:43 +0100 Subject: [PATCH 3/3] remove the duplicated helper --- .../internal/helpers/use-error-handler.ts | 111 ------------------ 1 file changed, 111 deletions(-) delete mode 100644 packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/use-error-handler.ts diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/use-error-handler.ts b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/use-error-handler.ts deleted file mode 100644 index 2f75fa8425a6b..0000000000000 --- a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/helpers/use-error-handler.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { useEffect } from 'react' -import { attachHydrationErrorState } from './attach-hydration-error-state' -import { isNextRouterError } from '../../../../is-next-router-error' -import { storeHydrationErrorStateFromConsoleArgs } from './hydration-error-info' -import { formatConsoleArgs } from '../../../../../lib/console' -import isError from '../../../../../../lib/is-error' -import { createUnhandledError } from './console-error' -import { enqueueConsecutiveDedupedError } from './enqueue-client-error' -import { getReactStitchedError } from './stitched-error' - -const queueMicroTask = - globalThis.queueMicrotask || ((cb: () => void) => Promise.resolve().then(cb)) - -export type ErrorHandler = (error: Error) => void - -const errorQueue: Array = [] -const errorHandlers: Array = [] -const rejectionQueue: Array = [] -const rejectionHandlers: Array = [] - -export function handleClientError( - originError: unknown, - consoleErrorArgs: any[], - capturedFromConsole: boolean = false -) { - let error: Error - if (!originError || !isError(originError)) { - // If it's not an error, format the args into an error - const formattedErrorMessage = formatConsoleArgs(consoleErrorArgs) - error = createUnhandledError(formattedErrorMessage) - } else { - error = capturedFromConsole - ? createUnhandledError(originError) - : originError - } - error = getReactStitchedError(error) - - storeHydrationErrorStateFromConsoleArgs(...consoleErrorArgs) - attachHydrationErrorState(error) - - enqueueConsecutiveDedupedError(errorQueue, error) - for (const handler of errorHandlers) { - // Delayed the error being passed to React Dev Overlay, - // avoid the state being synchronously updated in the component. - queueMicroTask(() => { - handler(error) - }) - } -} - -export function useErrorHandler( - handleOnUnhandledError: ErrorHandler, - handleOnUnhandledRejection: ErrorHandler -) { - useEffect(() => { - // Handle queued errors. - errorQueue.forEach(handleOnUnhandledError) - rejectionQueue.forEach(handleOnUnhandledRejection) - - // Listen to new errors. - errorHandlers.push(handleOnUnhandledError) - rejectionHandlers.push(handleOnUnhandledRejection) - - return () => { - // Remove listeners. - errorHandlers.splice(errorHandlers.indexOf(handleOnUnhandledError), 1) - rejectionHandlers.splice( - rejectionHandlers.indexOf(handleOnUnhandledRejection), - 1 - ) - } - }, [handleOnUnhandledError, handleOnUnhandledRejection]) -} - -function onUnhandledError(event: WindowEventMap['error']): void | boolean { - if (isNextRouterError(event.error)) { - event.preventDefault() - return false - } - handleClientError(event.error, []) -} - -function onUnhandledRejection(ev: WindowEventMap['unhandledrejection']): void { - const reason = ev?.reason - if (isNextRouterError(reason)) { - ev.preventDefault() - return - } - - let error = reason - if (error && !isError(error)) { - error = createUnhandledError(error + '') - } - - rejectionQueue.push(error) - for (const handler of rejectionHandlers) { - handler(error) - } -} - -export function handleGlobalErrors() { - if (typeof window !== 'undefined') { - try { - // Increase the number of stack frames on the client - Error.stackTraceLimit = 50 - } catch {} - - window.addEventListener('error', onUnhandledError) - window.addEventListener('unhandledrejection', onUnhandledRejection) - } -}