Skip to content

Commit e9ec2a1

Browse files
committed
refactor stack capture logic
1 parent 7eba07d commit e9ec2a1

File tree

4 files changed

+101
-44
lines changed

4 files changed

+101
-44
lines changed

packages/next/src/client/components/globals/intercept-console-error.ts

Lines changed: 40 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import isError from '../../../lib/is-error'
22
import { isNextRouterError } from '../is-next-router-error'
3+
import { stripStackByFrame } from '../react-dev-overlay/internal/helpers/strip-stack-frame'
34
import { handleClientError } from '../react-dev-overlay/internal/helpers/use-error-handler'
45

6+
const NEXT_CONSOLE_STACK_FRAME = 'next-console-stack-frame'
7+
8+
const stripBeforeNextConsoleFrame = (stack: string) =>
9+
stripStackByFrame(stack, NEXT_CONSOLE_STACK_FRAME, false)
10+
511
export const originConsoleError = window.console.error
612

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

14-
window.console.error = (...args: any[]) => {
15-
let maybeError: unknown
20+
const namedLoggerInstance = {
21+
[NEXT_CONSOLE_STACK_FRAME](...args: any[]) {
22+
let maybeError: unknown
1623

17-
if (process.env.NODE_ENV !== 'production') {
18-
const replayedError = matchReplayedError(...args)
19-
if (replayedError) {
20-
maybeError = replayedError
24+
if (process.env.NODE_ENV !== 'production') {
25+
const replayedError = matchReplayedError(...args)
26+
if (replayedError) {
27+
maybeError = replayedError
28+
} else {
29+
// See https://github.com/facebook/react/blob/d50323eb845c5fde0d720cae888bf35dedd05506/packages/react-reconciler/src/ReactFiberErrorLogger.js#L78
30+
maybeError = args[1]
31+
}
2132
} else {
22-
// See https://github.com/facebook/react/blob/d50323eb845c5fde0d720cae888bf35dedd05506/packages/react-reconciler/src/ReactFiberErrorLogger.js#L78
23-
maybeError = args[1]
33+
maybeError = args[0]
2434
}
25-
} else {
26-
maybeError = args[0]
27-
}
2835

29-
if (!isNextRouterError(maybeError)) {
30-
if (process.env.NODE_ENV !== 'production') {
31-
// Create an origin stack that pointing to the origin location of the error
32-
const originStack = (new Error().stack || '')
33-
.split('\n')
34-
// Remove error message and the stack of patched `window.console.error` call
35-
.slice(2)
36-
.join('\n')
36+
if (!isNextRouterError(maybeError)) {
37+
if (process.env.NODE_ENV !== 'production') {
38+
// Create an origin stack that pointing to the origin location of the error
39+
const captureStackErrorStackTrace = new Error().stack || ''
40+
const strippedStack = stripBeforeNextConsoleFrame(
41+
captureStackErrorStackTrace
42+
)
3743

38-
handleClientError(
39-
// replayed errors have their own complex format string that should be used,
40-
// but if we pass the error directly, `handleClientError` will ignore it
41-
maybeError,
42-
args,
43-
originStack
44-
)
45-
}
44+
handleClientError(
45+
// replayed errors have their own complex format string that should be used,
46+
// but if we pass the error directly, `handleClientError` will ignore it
47+
maybeError,
48+
args,
49+
strippedStack
50+
)
51+
}
4652

47-
originConsoleError.apply(window.console, args)
48-
}
53+
originConsoleError.apply(window.console, args)
54+
}
55+
},
4956
}
57+
58+
window.console.error = namedLoggerInstance[NEXT_CONSOLE_STACK_FRAME].bind(
59+
window.console
60+
)
5061
}
5162

5263
function matchReplayedError(...args: unknown[]): Error | null {

packages/next/src/client/components/react-dev-overlay/internal/helpers/stitched-error.ts

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,11 @@
11
import React from 'react'
22
import isError from '../../../../../lib/is-error'
3+
import { stripStackByFrame } from './strip-stack-frame'
34

45
const REACT_ERROR_STACK_BOTTOM_FRAME = 'react-stack-bottom-frame'
5-
const REACT_ERROR_STACK_BOTTOM_FRAME_REGEX = new RegExp(
6-
`(at ${REACT_ERROR_STACK_BOTTOM_FRAME} )|(${REACT_ERROR_STACK_BOTTOM_FRAME}\\@)`
7-
)
86

9-
function stripAfterReactBottomFrame(stack: string): string {
10-
const stackLines = stack.split('\n')
11-
const indexOfSplit = stackLines.findIndex((line) =>
12-
REACT_ERROR_STACK_BOTTOM_FRAME_REGEX.test(line)
13-
)
14-
const isOriginalReactError = indexOfSplit >= 0 // has the react-stack-bottom-frame
15-
const strippedStack = isOriginalReactError
16-
? stackLines.slice(0, indexOfSplit).join('\n')
17-
: stack
18-
19-
return strippedStack
20-
}
7+
const stripAfterReactBottomFrame = (stack: string) =>
8+
stripStackByFrame(stack, REACT_ERROR_STACK_BOTTOM_FRAME, true)
219

2210
export function getReactStitchedError<T = unknown>(err: T): Error | T {
2311
if (typeof (React as any).captureOwnerStack !== 'function') {
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { stripStackByFrame } from './strip-stack-frame'
2+
3+
describe('stripStackByFrame', () => {
4+
it('strips stack after frame', () => {
5+
const stripStackByFrameBefore = (stack: string) =>
6+
stripStackByFrame(stack, 'special-stack-frame', true)
7+
8+
const stack = `Error: test
9+
at page (http://localhost:3000/_next/static/chunks/webpack.js:1:1)
10+
at special-stack-frame (http://localhost:3000/_next/static/chunks/webpack.js:1:1)
11+
at foo (http://localhost:3000/_next/static/chunks/webpack.js:1:1)
12+
`
13+
14+
const strippedStack = stripStackByFrameBefore(stack)
15+
expect(strippedStack).toMatchInlineSnapshot(`
16+
"Error: test
17+
at page (http://localhost:3000/_next/static/chunks/webpack.js:1:1)"
18+
`)
19+
})
20+
21+
it('strips stack before frame', () => {
22+
const stripStackByFrameAfter = (stack: string) =>
23+
stripStackByFrame(stack, 'special-stack-frame', false)
24+
25+
const stack = `Error: test
26+
at page (http://localhost:3000/_next/static/chunks/webpack.js:1:1)
27+
at special-stack-frame (http://localhost:3000/_next/static/chunks/webpack.js:1:1)
28+
at foo (http://localhost:3000/_next/static/chunks/webpack.js:1:1)
29+
`
30+
31+
const strippedStack = stripStackByFrameAfter(stack)
32+
expect(strippedStack).toMatchInlineSnapshot(`
33+
" at foo (http://localhost:3000/_next/static/chunks/webpack.js:1:1)
34+
"
35+
`)
36+
})
37+
})
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
export function stripStackByFrame(
2+
stack: string,
3+
framePivot: string,
4+
stripAfter: boolean
5+
): string {
6+
const framePivotRegex = new RegExp(`(at ${framePivot} )|(${framePivot}\\@)`)
7+
const stackLines = stack.split('\n')
8+
const indexOfSplit = stackLines.findIndex((line) =>
9+
framePivotRegex.test(line)
10+
)
11+
const isOriginalReactError = indexOfSplit >= 0 // has the frame pivot
12+
const strippedStack = isOriginalReactError
13+
? stripAfter
14+
? // Keep the frames before pivot
15+
stackLines.slice(0, indexOfSplit).join('\n')
16+
: // Keep the frames after pivot
17+
stackLines.slice(indexOfSplit + 1).join('\n')
18+
: stack
19+
20+
return strippedStack
21+
}

0 commit comments

Comments
 (0)