-
Notifications
You must be signed in to change notification settings - Fork 27.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
159 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
39 changes: 39 additions & 0 deletions
39
packages/next/src/client/components/react-dev-overlay/internal/helpers/stitched-error.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import React from 'react' | ||
|
||
const captureOwnerStack = process.env.__NEXT_REACT_OWNER_STACK | ||
? (React as any).captureOwnerStack | ||
: () => '' | ||
|
||
const REACT_ERROR_STACK_BOTTOM_FRAME = 'react-stack-bottom-frame' | ||
const REACT_ERROR_STACK_BOTTOM_FRAME_REGEX = new RegExp( | ||
`(at ${REACT_ERROR_STACK_BOTTOM_FRAME} )|(${REACT_ERROR_STACK_BOTTOM_FRAME}\\@)` | ||
) | ||
|
||
export function getReactStitchedError<T = unknown>(err: T): Error { | ||
const isErrorInstance = err instanceof Error | ||
const originStack = isErrorInstance ? err.stack || '' : '' | ||
const originMessage = isErrorInstance ? err.message : '' | ||
const stackLines = originStack.split('\n') | ||
const indexOfSplit = stackLines.findIndex((line) => | ||
REACT_ERROR_STACK_BOTTOM_FRAME_REGEX.test(line) | ||
) | ||
const isOriginalReactError = indexOfSplit >= 0 // has the react-stack-bottom-frame | ||
let newStack = isOriginalReactError | ||
? stackLines.slice(0, indexOfSplit).join('\n') | ||
: originStack | ||
|
||
const newError = new Error(originMessage) | ||
// Copy all enumerable properties, e.g. digest | ||
Object.assign(newError, err) | ||
newError.stack = newStack | ||
|
||
// Avoid duplicate overriding stack frames | ||
const ownerStack = captureOwnerStack() | ||
if (ownerStack && newStack.endsWith(ownerStack) === false) { | ||
newStack += ownerStack | ||
// Override stack | ||
newError.stack = newStack | ||
} | ||
|
||
return newError | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
import type { HydrationOptions } from 'react-dom/client' | ||
import { isBailoutToCSRError } from '../shared/lib/lazy-dynamic/bailout-to-csr' | ||
import { getReactStitchedError } from './components/react-dev-overlay/internal/helpers/stitched-error' | ||
import { originConsoleError } from './components/globals/intercept-console-error' | ||
import { handleClientError } from './components/react-dev-overlay/internal/helpers/use-error-handler' | ||
import isError from '../lib/is-error' | ||
import { isNextRouterError } from './components/is-next-router-error' | ||
|
||
const reportGlobalError = | ||
typeof reportError === 'function' | ||
? // In modern browsers, reportError will dispatch an error event, | ||
// emulating an uncaught JavaScript error. | ||
reportError | ||
: (error: any) => { | ||
window.console.error(error) | ||
} | ||
|
||
export const onRecoverableError: HydrationOptions['onRecoverableError'] = ( | ||
err, | ||
errorInfo | ||
) => { | ||
if (isBailoutToCSRError(err)) return | ||
|
||
const stitchedError = getReactStitchedError(err) | ||
// In development mode, pass along the component stack to the error | ||
if (process.env.NODE_ENV === 'development' && errorInfo.componentStack) { | ||
;(stitchedError as any)._componentStack = errorInfo.componentStack | ||
} | ||
// Skip certain custom errors which are not expected to be reported on client | ||
|
||
reportGlobalError(stitchedError) | ||
} | ||
|
||
export const onCaughtError: HydrationOptions['onCaughtError'] = ( | ||
err, | ||
errorInfo | ||
) => { | ||
// Skip certain custom errors which are not expected to be reported on client | ||
if (isBailoutToCSRError(err) || isNextRouterError(err)) return | ||
|
||
const stitchedError = getReactStitchedError(err) | ||
|
||
if (process.env.NODE_ENV === 'development') { | ||
const errorBoundaryComponent = errorInfo?.errorBoundary?.constructor | ||
const errorBoundaryName = | ||
// read react component displayName | ||
(errorBoundaryComponent as any)?.displayName || | ||
errorBoundaryComponent?.name || | ||
'Unknown' | ||
|
||
const componentThatErroredFrame = errorInfo?.componentStack?.split('\n')[1] | ||
|
||
// Match chrome or safari stack trace | ||
const matches = | ||
componentThatErroredFrame?.match(/\s+at (\w+)\s+|(\w+)@/) ?? [] | ||
const componentThatErroredName = matches[1] || matches[2] || 'Unknown' | ||
|
||
// In development mode, pass along the component stack to the error | ||
if (process.env.NODE_ENV === 'development' && errorInfo.componentStack) { | ||
;(stitchedError as any)._componentStack = errorInfo.componentStack | ||
} | ||
|
||
// Create error location with errored component and error boundary, to match the behavior of default React onCaughtError handler. | ||
const errorLocation = `The above error occurred in the <${componentThatErroredName}> component. It was handled by the <${errorBoundaryName}> error boundary.` | ||
|
||
const originErrorStack = isError(err) ? err.stack || '' : '' | ||
// Always log the modified error instance so the console.error interception side can pick it up easily without constructing an error again. | ||
originConsoleError(originErrorStack + '\n\n' + errorLocation) | ||
handleClientError(stitchedError) | ||
} else { | ||
console.error(err) | ||
} | ||
} | ||
|
||
export const onUncaughtError: HydrationOptions['onUncaughtError'] = ( | ||
err, | ||
errorInfo | ||
) => { | ||
// Skip certain custom errors which are not expected to be reported on client | ||
if (isBailoutToCSRError(err) || isNextRouterError(err)) return | ||
|
||
const stitchedError = getReactStitchedError(err) | ||
|
||
if (process.env.NODE_ENV === 'development') { | ||
const componentThatErroredFrame = errorInfo?.componentStack?.split('\n')[1] | ||
|
||
// Match chrome or safari stack trace | ||
const matches = | ||
componentThatErroredFrame?.match(/\s+at (\w+)\s+|(\w+)@/) ?? [] | ||
const componentThatErroredName = matches[1] || matches[2] || 'Unknown' | ||
|
||
// In development mode, pass along the component stack to the error | ||
if (process.env.NODE_ENV === 'development' && errorInfo.componentStack) { | ||
;(stitchedError as any)._componentStack = errorInfo.componentStack | ||
} | ||
|
||
// Create error location with errored component and error boundary, to match the behavior of default React onCaughtError handler. | ||
const errorLocation = `The above error occurred in the <${componentThatErroredName}> component.` | ||
|
||
originConsoleError(stitchedError.stack + '\n\n' + errorLocation) | ||
// Always log the modified error instance so the console.error interception side can pick it up easily without constructing an error again. | ||
reportGlobalError(stitchedError) | ||
} else { | ||
reportGlobalError(err) | ||
} | ||
} |