Skip to content

Commit 1fb8361

Browse files
authored
Compare error stack to dedupe error (#71798)
1 parent 14b92e6 commit 1fb8361

File tree

7 files changed

+82
-24
lines changed

7 files changed

+82
-24
lines changed

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

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import isError from '../../../lib/is-error'
22
import { isNextRouterError } from '../is-next-router-error'
3+
import { captureStackTrace } from '../react-dev-overlay/internal/helpers/capture-stack-trace'
34
import { handleClientError } from '../react-dev-overlay/internal/helpers/use-error-handler'
45

56
export const originConsoleError = window.console.error
@@ -10,13 +11,14 @@ export function patchConsoleError() {
1011
if (typeof window === 'undefined') {
1112
return
1213
}
13-
14-
window.console.error = (...args: any[]) => {
14+
window.console.error = function error(...args: any[]) {
1515
let maybeError: unknown
16+
let isReplayedError = false
1617

1718
if (process.env.NODE_ENV !== 'production') {
1819
const replayedError = matchReplayedError(...args)
1920
if (replayedError) {
21+
isReplayedError = true
2022
maybeError = replayedError
2123
} else {
2224
// See https://github.com/facebook/react/blob/d50323eb845c5fde0d720cae888bf35dedd05506/packages/react-reconciler/src/ReactFiberErrorLogger.js#L78
@@ -28,6 +30,11 @@ export function patchConsoleError() {
2830

2931
if (!isNextRouterError(maybeError)) {
3032
if (process.env.NODE_ENV !== 'production') {
33+
// Create an origin stack that pointing to the origin location of the error
34+
if (!isReplayedError && isError(maybeError)) {
35+
captureStackTrace(maybeError)
36+
}
37+
3138
handleClientError(
3239
// replayed errors have their own complex format string that should be used,
3340
// but if we pass the error directly, `handleClientError` will ignore it
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Polyfill for `Error.captureStackTrace` in browsers
2+
export function captureStackTrace(obj: any) {
3+
const container = new Error()
4+
Object.defineProperty(obj, 'stack', {
5+
configurable: true,
6+
get() {
7+
const { stack } = container
8+
Object.defineProperty(this, 'stack', { value: stack })
9+
return stack
10+
},
11+
})
12+
}

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ export function enqueueConsecutiveDedupedError(
77
) {
88
const isFront = isHydrationError(error)
99
const previousError = isFront ? queue[0] : queue[queue.length - 1]
10-
// Only check message to see if it's the same error, as message is representative display in the console.
11-
if (previousError && previousError.message === error.message) {
10+
// Compare the error stack to dedupe the consecutive errors
11+
if (previousError && previousError.stack === error.stack) {
1212
return
1313
}
1414
// TODO: change all to push error into errorQueue,

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

-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ export function getReactStitchedError<T = unknown>(err: T): Error | T {
1010
if (typeof (React as any).captureOwnerStack !== 'function') {
1111
return err
1212
}
13-
1413
const isErrorInstance = isError(err)
1514
const originStack = isErrorInstance ? err.stack || '' : ''
1615
const originMessage = isErrorInstance ? err.message : ''
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
'use client'
2+
3+
export default function Page() {
4+
for (let i = 0; i < 3; i++) {
5+
console.error('trigger an console.error in loop of render')
6+
}
7+
return <p>render</p>
8+
}

test/development/app-dir/capture-console-error/capture-console-error.test.ts

+50-18
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,15 @@ describe('app-dir - capture-console-error', () => {
5555
10 | click to error",
5656
}
5757
`)
58-
} else if (isReactExperimental) {
58+
} else {
5959
expect(result).toMatchInlineSnapshot(`
6060
{
61-
"callStacks": "button
62-
app/browser/event/page.js (5:6)",
61+
"callStacks": ${
62+
isReactExperimental
63+
? `"button
64+
app/browser/event/page.js (5:6)"`
65+
: `""`
66+
},
6367
"count": 1,
6468
"description": "trigger an console <error>",
6569
"source": "app/browser/event/page.js (7:17) @ error
@@ -73,27 +77,55 @@ describe('app-dir - capture-console-error', () => {
7377
10 | click to error",
7478
}
7579
`)
80+
}
81+
})
82+
83+
it('should capture browser console error in render and dedupe if necessary', async () => {
84+
const browser = await next.browser('/browser/render')
85+
86+
await waitForAndOpenRuntimeError(browser)
87+
await assertHasRedbox(browser)
88+
89+
const result = await getRedboxResult(browser)
90+
91+
if (process.env.TURBOPACK) {
92+
expect(result).toMatchInlineSnapshot(`
93+
{
94+
"callStacks": "",
95+
"count": ${isReactExperimental ? 1 : 2},
96+
"description": "trigger an console.error in render",
97+
"source": "app/browser/render/page.js (4:11) @ Page
98+
99+
2 |
100+
3 | export default function Page() {
101+
> 4 | console.error('trigger an console.error in render')
102+
| ^
103+
5 | return <p>render</p>
104+
6 | }
105+
7 |",
106+
}
107+
`)
76108
} else {
77109
expect(result).toMatchInlineSnapshot(`
78110
{
79111
"callStacks": "",
80-
"count": 1,
81-
"description": "trigger an console <error>",
82-
"source": "app/browser/event/page.js (7:17) @ error
112+
"count": ${isReactExperimental ? 1 : 2},
113+
"description": "trigger an console.error in render",
114+
"source": "app/browser/render/page.js (4:11) @ error
83115
84-
5 | <button
85-
6 | onClick={() => {
86-
> 7 | console.error('trigger an console <%s>', 'error')
87-
| ^
88-
8 | }}
89-
9 | >
90-
10 | click to error",
116+
2 |
117+
3 | export default function Page() {
118+
> 4 | console.error('trigger an console.error in render')
119+
| ^
120+
5 | return <p>render</p>
121+
6 | }
122+
7 |",
91123
}
92124
`)
93125
}
94126
})
95127

96-
it('should capture browser console error in render and dedupe if necessary', async () => {
128+
it('should capture browser console error in render and dedupe when multi same errors logged', async () => {
97129
const browser = await next.browser('/browser/render')
98130

99131
await waitForAndOpenRuntimeError(browser)
@@ -105,7 +137,7 @@ describe('app-dir - capture-console-error', () => {
105137
expect(result).toMatchInlineSnapshot(`
106138
{
107139
"callStacks": "",
108-
"count": 1,
140+
"count": ${isReactExperimental ? 1 : 2},
109141
"description": "trigger an console.error in render",
110142
"source": "app/browser/render/page.js (4:11) @ Page
111143
@@ -122,7 +154,7 @@ describe('app-dir - capture-console-error', () => {
122154
expect(result).toMatchInlineSnapshot(`
123155
{
124156
"callStacks": "",
125-
"count": 1,
157+
"count": ${isReactExperimental ? 1 : 2},
126158
"description": "trigger an console.error in render",
127159
"source": "app/browser/render/page.js (4:11) @ error
128160
@@ -150,7 +182,7 @@ describe('app-dir - capture-console-error', () => {
150182
expect(result).toMatchInlineSnapshot(`
151183
{
152184
"callStacks": "",
153-
"count": 1,
185+
"count": ${isReactExperimental ? 1 : 2},
154186
"description": "ssr console error:client",
155187
"source": "app/ssr/page.js (4:11) @ Page
156188
@@ -167,7 +199,7 @@ describe('app-dir - capture-console-error', () => {
167199
expect(result).toMatchInlineSnapshot(`
168200
{
169201
"callStacks": "",
170-
"count": 1,
202+
"count": ${isReactExperimental ? 1 : 2},
171203
"description": "ssr console error:client",
172204
"source": "app/ssr/page.js (4:11) @ error
173205

test/lib/next-test-utils.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1255,7 +1255,7 @@ export async function toggleCollapseComponentStack(
12551255

12561256
export async function getRedboxCallStack(
12571257
browser: BrowserInterface
1258-
): Promise<string> {
1258+
): Promise<string | null> {
12591259
await browser.waitForElementByCss('[data-nextjs-call-stack-frame]', 30000)
12601260

12611261
const callStackFrameElements = await browser.elementsByCss(

0 commit comments

Comments
 (0)