Skip to content

Commit

Permalink
add missing pieces
Browse files Browse the repository at this point in the history
  • Loading branch information
huozhi committed Sep 24, 2024
1 parent 0031745 commit 0cf17e2
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { isNextRouterError } from '../is-next-router-error'
import { handleClientError } from '../react-dev-overlay/internal/helpers/use-error-handler'

const originConsoleError = window.console.error

const isReactOwnerStackEnabled = !!process.env.__NEXT_REACT_OWNER_STACK

// Patch console.error to collect information about hydration errors
export function patchConsoleError() {
function patchConsoleError() {
// Ensure it's only patched once
if (typeof window === 'undefined') {
return
Expand All @@ -11,7 +15,13 @@ export function patchConsoleError() {
const originConsoleError = window.console.error
window.console.error = (...args) => {
// See https://github.com/facebook/react/blob/d50323eb845c5fde0d720cae888bf35dedd05506/packages/react-reconciler/src/ReactFiberErrorLogger.js#L78
const error = process.env.NODE_ENV !== 'production' ? args[1] : args[0]
const error =
process.env.NODE_ENV !== 'production'
? isReactOwnerStackEnabled
? args[1] || args[0]
: args[1]
: args[0]


if (!isNextRouterError(error)) {
if (process.env.NODE_ENV !== 'production') {
Expand All @@ -26,3 +36,5 @@ export function patchConsoleError() {
}
}
}

export { patchConsoleError, originConsoleError }
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
}
106 changes: 106 additions & 0 deletions packages/next/src/client/react-client-callbacks.ts
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)
}
}

0 comments on commit 0cf17e2

Please sign in to comment.