feat(error-overlay): hide <unknown>/stringify methods in <anonymous> file from stack (#62325)

### What?

Clean up the error overlay:

<details>
<summary><b>Before:</b></summary>
<img
src="https://github.com/vercel/next.js/assets/18369201/22c3ab2c-8445-4c25-8554-a5ab51100af4"/>
</details>

<details>
<summary><b>After:</b></summary>
<img
src="https://github.com/vercel/next.js/assets/18369201/403c30fc-8b27-4529-838c-47d9cbe52381"/></details>


I also simplified the current code as it was likely using `useMemo` a
bit eagerly.

### Why?

This is an unactionable line by the user, no value in showing it in the
overlay.

### How?

Filter out the frame before rendering it in the overlay.

This answers [this
question](https://github.com/vercel/next.js/pull/62206#issuecomment-1956636486)
too, since the module grouping is local. Now that `<anonymous>` is
filtered out, the two Next.js groups are now merged into one, further
cleaning up the stack.

Closes NEXT-2505
This commit is contained in:
Balázs Orbán 2024-02-21 16:58:22 +01:00 committed by GitHub
parent 79cb2b2256
commit a1b20470c6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 109 additions and 60 deletions

View file

@ -2,75 +2,64 @@ import * as React from 'react'
import { CodeFrame } from '../../components/CodeFrame'
import type { ReadyRuntimeError } from '../../helpers/getErrorByType'
import { noop as css } from '../../helpers/noop-template'
import type { OriginalStackFrame } from '../../helpers/stack-frame'
import { groupStackFramesByFramework } from '../../helpers/group-stack-frames-by-framework'
import { GroupedStackFrames } from './GroupedStackFrames'
import { ComponentStackFrameRow } from './ComponentStackFrameRow'
export type RuntimeErrorProps = { error: ReadyRuntimeError }
const RuntimeError: React.FC<RuntimeErrorProps> = function RuntimeError({
error,
}) {
const firstFirstPartyFrameIndex = React.useMemo<number>(() => {
return error.frames.findIndex(
(entry) =>
entry.expanded &&
Boolean(entry.originalCodeFrame) &&
Boolean(entry.originalStackFrame)
)
}, [error.frames])
const firstFrame = React.useMemo<OriginalStackFrame | null>(() => {
return error.frames[firstFirstPartyFrameIndex] ?? null
}, [error.frames, firstFirstPartyFrameIndex])
export function RuntimeError({ error }: RuntimeErrorProps) {
const { firstFrame, allLeadingFrames, allCallStackFrames } =
React.useMemo(() => {
const filteredFrames = error.frames.filter(
(f) =>
!(
f.sourceStackFrame.file === '<anonymous>' &&
['stringify', '<unknown>'].includes(f.sourceStackFrame.methodName)
)
)
const allLeadingFrames = React.useMemo<OriginalStackFrame[]>(
() =>
firstFirstPartyFrameIndex < 0
? []
: error.frames.slice(0, firstFirstPartyFrameIndex),
[error.frames, firstFirstPartyFrameIndex]
)
const firstFirstPartyFrameIndex = filteredFrames.findIndex(
(entry) =>
entry.expanded &&
Boolean(entry.originalCodeFrame) &&
Boolean(entry.originalStackFrame)
)
return {
firstFrame: filteredFrames[firstFirstPartyFrameIndex] ?? null,
allLeadingFrames:
firstFirstPartyFrameIndex < 0
? []
: filteredFrames.slice(0, firstFirstPartyFrameIndex),
allCallStackFrames: filteredFrames.slice(firstFirstPartyFrameIndex + 1),
}
}, [error.frames])
const [all, setAll] = React.useState(firstFrame == null)
const toggleAll = React.useCallback(() => {
setAll((v) => !v)
}, [])
const leadingFrames = React.useMemo(
() => allLeadingFrames.filter((f) => f.expanded || all),
[all, allLeadingFrames]
)
const allCallStackFrames = React.useMemo<OriginalStackFrame[]>(
() => error.frames.slice(firstFirstPartyFrameIndex + 1),
[error.frames, firstFirstPartyFrameIndex]
)
const visibleCallStackFrames = React.useMemo<OriginalStackFrame[]>(
() => allCallStackFrames.filter((f) => f.expanded || all),
[all, allCallStackFrames]
)
const canShowMore = React.useMemo<boolean>(() => {
return (
allCallStackFrames.length !== visibleCallStackFrames.length ||
(all && firstFrame != null)
const {
canShowMore,
leadingFramesGroupedByFramework,
stackFramesGroupedByFramework,
} = React.useMemo(() => {
const leadingFrames = allLeadingFrames.filter((f) => f.expanded || all)
const visibleCallStackFrames = allCallStackFrames.filter(
(f) => f.expanded || all
)
}, [
all,
allCallStackFrames.length,
firstFrame,
visibleCallStackFrames.length,
])
const stackFramesGroupedByFramework = React.useMemo(
() => groupStackFramesByFramework(allCallStackFrames),
[allCallStackFrames]
)
return {
canShowMore:
allCallStackFrames.length !== visibleCallStackFrames.length ||
(all && firstFrame != null),
const leadingFramesGroupedByFramework = React.useMemo(
() => groupStackFramesByFramework(leadingFrames),
[leadingFrames]
)
stackFramesGroupedByFramework:
groupStackFramesByFramework(allCallStackFrames),
leadingFramesGroupedByFramework:
groupStackFramesByFramework(leadingFrames),
}
}, [all, allCallStackFrames, allLeadingFrames, firstFrame])
return (
<React.Fragment>
@ -115,7 +104,7 @@ const RuntimeError: React.FC<RuntimeErrorProps> = function RuntimeError({
tabIndex={10}
data-nextjs-data-runtime-error-collapsed-action
type="button"
onClick={toggleAll}
onClick={() => setAll(!all)}
>
{all ? 'Hide' : 'Show'} collapsed frames
</button>
@ -212,5 +201,3 @@ export const styles = css`
margin-bottom: var(--size-gap-double);
}
`
export { RuntimeError }

View file

@ -17,7 +17,7 @@ const reactVendoredRe =
const reactNodeModulesRe = /node_modules[\\/](react|react-dom|scheduler)[\\/]/
const nextRe =
/([\\/]next[\\/](dist|src)[\\/]|[\\/].next[\\/]static[\\/]chunks[\\/]webpack\.js$)/
/(node_modules[\\/]next[\\/]|[\\/].next[\\/]static[\\/]chunks[\\/]webpack\.js$)/
/** Given a potential file path, it parses which package the file belongs to. */
export function findSourcePackage(

View file

@ -839,6 +839,37 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox app %s', () => {
await cleanup()
})
test('stringify <anonymous> and <unknown> <anonymous> are hidden in stack trace', async () => {
const { session, browser, cleanup } = await sandbox(
next,
new Map([
[
'app/page.js',
outdent`
export default function Page() {
const e = new Error("Boom!");
e.stack += \`
at stringify (<anonymous>)
at <unknown> (<anonymous>)
at foo (bar:1:1)\`;
throw e;
}
`,
],
])
)
expect(await session.hasRedbox()).toBe(true)
await expandCallStack(browser)
const callStackFrames = await browser.elementsByCss(
'[data-nextjs-call-stack-frame]'
)
const texts = await Promise.all(callStackFrames.map((f) => f.innerText()))
expect(texts).not.toContain('stringify\n<anonymous>')
expect(texts).not.toContain('<unknown>\n<anonymous>')
expect(texts).toContain('foo\nbar (1:1)')
await cleanup()
})
test('Server component errors should open up in fullscreen', async () => {
const { session, browser, cleanup } = await sandbox(
next,

View file

@ -783,4 +783,35 @@ describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => {
await cleanup()
})
test('stringify <anonymous> and <unknown> <anonymous> are hidden in stack trace for pages error', async () => {
const { session, browser, cleanup } = await sandbox(
next,
new Map([
[
'pages/index.js',
outdent`
export default function Page() {
const e = new Error("Client error!");
e.stack += \`
at stringify (<anonymous>)
at <unknown> (<anonymous>)
at foo (bar:1:1)\`;
throw e;
}
`,
],
])
)
expect(await session.hasRedbox()).toBe(true)
await expandCallStack(browser)
const callStackFrames = await browser.elementsByCss(
'[data-nextjs-call-stack-frame]'
)
const texts = await Promise.all(callStackFrames.map((f) => f.innerText()))
expect(texts).not.toContain('stringify\n<anonymous>')
expect(texts).not.toContain('<unknown>\n<anonymous>')
expect(texts).toContain('foo\nbar (1:1)')
await cleanup()
})
})