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:
parent
79cb2b2256
commit
a1b20470c6
4 changed files with 109 additions and 60 deletions
|
@ -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 }
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue