Skip to content

Commit

Permalink
refactor stack capture logic
Browse files Browse the repository at this point in the history
  • Loading branch information
huozhi committed Oct 24, 2024
1 parent 7eba07d commit e9ec2a1
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 44 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import isError from '../../../lib/is-error'
import { isNextRouterError } from '../is-next-router-error'
import { stripStackByFrame } from '../react-dev-overlay/internal/helpers/strip-stack-frame'
import { handleClientError } from '../react-dev-overlay/internal/helpers/use-error-handler'

const NEXT_CONSOLE_STACK_FRAME = 'next-console-stack-frame'

const stripBeforeNextConsoleFrame = (stack: string) =>
stripStackByFrame(stack, NEXT_CONSOLE_STACK_FRAME, false)

export const originConsoleError = window.console.error

// Patch console.error to collect information about hydration errors
Expand All @@ -11,42 +17,47 @@ export function patchConsoleError() {
return
}

window.console.error = (...args: any[]) => {
let maybeError: unknown
const namedLoggerInstance = {
[NEXT_CONSOLE_STACK_FRAME](...args: any[]) {
let maybeError: unknown

if (process.env.NODE_ENV !== 'production') {
const replayedError = matchReplayedError(...args)
if (replayedError) {
maybeError = replayedError
if (process.env.NODE_ENV !== 'production') {
const replayedError = matchReplayedError(...args)
if (replayedError) {
maybeError = replayedError
} else {
// See https://github.com/facebook/react/blob/d50323eb845c5fde0d720cae888bf35dedd05506/packages/react-reconciler/src/ReactFiberErrorLogger.js#L78
maybeError = args[1]
}
} else {
// See https://github.com/facebook/react/blob/d50323eb845c5fde0d720cae888bf35dedd05506/packages/react-reconciler/src/ReactFiberErrorLogger.js#L78
maybeError = args[1]
maybeError = args[0]
}
} else {
maybeError = args[0]
}

if (!isNextRouterError(maybeError)) {
if (process.env.NODE_ENV !== 'production') {
// Create an origin stack that pointing to the origin location of the error
const originStack = (new Error().stack || '')
.split('\n')
// Remove error message and the stack of patched `window.console.error` call
.slice(2)
.join('\n')
if (!isNextRouterError(maybeError)) {
if (process.env.NODE_ENV !== 'production') {
// Create an origin stack that pointing to the origin location of the error
const captureStackErrorStackTrace = new Error().stack || ''
const strippedStack = stripBeforeNextConsoleFrame(
captureStackErrorStackTrace
)

handleClientError(
// replayed errors have their own complex format string that should be used,
// but if we pass the error directly, `handleClientError` will ignore it
maybeError,
args,
originStack
)
}
handleClientError(
// replayed errors have their own complex format string that should be used,
// but if we pass the error directly, `handleClientError` will ignore it
maybeError,
args,
strippedStack
)
}

originConsoleError.apply(window.console, args)
}
originConsoleError.apply(window.console, args)
}
},
}

window.console.error = namedLoggerInstance[NEXT_CONSOLE_STACK_FRAME].bind(
window.console
)
}

function matchReplayedError(...args: unknown[]): Error | null {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,11 @@
import React from 'react'
import isError from '../../../../../lib/is-error'
import { stripStackByFrame } from './strip-stack-frame'

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}\\@)`
)

function stripAfterReactBottomFrame(stack: string): string {
const stackLines = stack.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
const strippedStack = isOriginalReactError
? stackLines.slice(0, indexOfSplit).join('\n')
: stack

return strippedStack
}
const stripAfterReactBottomFrame = (stack: string) =>
stripStackByFrame(stack, REACT_ERROR_STACK_BOTTOM_FRAME, true)

export function getReactStitchedError<T = unknown>(err: T): Error | T {
if (typeof (React as any).captureOwnerStack !== 'function') {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { stripStackByFrame } from './strip-stack-frame'

describe('stripStackByFrame', () => {
it('strips stack after frame', () => {
const stripStackByFrameBefore = (stack: string) =>
stripStackByFrame(stack, 'special-stack-frame', true)

const stack = `Error: test
at page (http://localhost:3000/_next/static/chunks/webpack.js:1:1)
at special-stack-frame (http://localhost:3000/_next/static/chunks/webpack.js:1:1)
at foo (http://localhost:3000/_next/static/chunks/webpack.js:1:1)
`

const strippedStack = stripStackByFrameBefore(stack)
expect(strippedStack).toMatchInlineSnapshot(`
"Error: test
at page (http://localhost:3000/_next/static/chunks/webpack.js:1:1)"
`)
})

it('strips stack before frame', () => {
const stripStackByFrameAfter = (stack: string) =>
stripStackByFrame(stack, 'special-stack-frame', false)

const stack = `Error: test
at page (http://localhost:3000/_next/static/chunks/webpack.js:1:1)
at special-stack-frame (http://localhost:3000/_next/static/chunks/webpack.js:1:1)
at foo (http://localhost:3000/_next/static/chunks/webpack.js:1:1)
`

const strippedStack = stripStackByFrameAfter(stack)
expect(strippedStack).toMatchInlineSnapshot(`
" at foo (http://localhost:3000/_next/static/chunks/webpack.js:1:1)
"
`)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export function stripStackByFrame(
stack: string,
framePivot: string,
stripAfter: boolean
): string {
const framePivotRegex = new RegExp(`(at ${framePivot} )|(${framePivot}\\@)`)
const stackLines = stack.split('\n')
const indexOfSplit = stackLines.findIndex((line) =>
framePivotRegex.test(line)
)
const isOriginalReactError = indexOfSplit >= 0 // has the frame pivot
const strippedStack = isOriginalReactError
? stripAfter
? // Keep the frames before pivot
stackLines.slice(0, indexOfSplit).join('\n')
: // Keep the frames after pivot
stackLines.slice(indexOfSplit + 1).join('\n')
: stack

return strippedStack
}

0 comments on commit e9ec2a1

Please sign in to comment.