-
Notifications
You must be signed in to change notification settings - Fork 179
✨ NextJS- addNextjsError component #4343
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
BeltranBulbarellaDD
merged 6 commits into
main
from
beltran.bulbarella/nextjs_app_router_error
Mar 17, 2026
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
12f7e0d
Add Next.js error handling, update plugin to use addError
BeltranBulbarellaDD 69da5f0
Add Pages Router error handling tests
BeltranBulbarellaDD 7d4f339
Linting and formatting
BeltranBulbarellaDD d13a9d8
Linting
BeltranBulbarellaDD 16e7481
Fixes for comments.
BeltranBulbarellaDD 7fc18f4
Add JSDoc for addNextjsError
BeltranBulbarellaDD File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
113 changes: 113 additions & 0 deletions
113
packages/rum-nextjs/src/domain/error/addNextjsError.spec.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| import type { RumInitConfiguration, RumPublicApi } from '@datadog/browser-rum-core' | ||
| import { registerCleanupTask } from '@datadog/browser-core/test' | ||
| import { nextjsPlugin, resetNextjsPlugin } from '../nextjsPlugin' | ||
| import { addNextjsError } from './addNextjsError' | ||
|
|
||
| const INIT_CONFIGURATION = {} as RumInitConfiguration | ||
|
|
||
| function initializeNextjsPlugin() { | ||
| const addErrorSpy = jasmine.createSpy() | ||
| const publicApi = { startView: jasmine.createSpy() } as unknown as RumPublicApi | ||
| const plugin = nextjsPlugin() | ||
| plugin.onInit({ publicApi, initConfiguration: { ...INIT_CONFIGURATION } }) | ||
| plugin.onRumStart({ addError: addErrorSpy }) | ||
| registerCleanupTask(() => { | ||
| resetNextjsPlugin() | ||
| }) | ||
| return { addErrorSpy } | ||
| } | ||
|
|
||
| describe('addNextjsError', () => { | ||
| it('does nothing when the plugin is not initialized', () => { | ||
| registerCleanupTask(() => { | ||
| resetNextjsPlugin() | ||
| }) | ||
| expect(() => addNextjsError(new Error('test'))).not.toThrow() | ||
| }) | ||
|
|
||
| it('delegates the error to addError', () => { | ||
| const { addErrorSpy } = initializeNextjsPlugin() | ||
| const originalError = new Error('test error') | ||
|
|
||
| addNextjsError(originalError, { componentStack: 'at ComponentSpy toto.js' }) | ||
|
|
||
| expect(addErrorSpy).toHaveBeenCalledOnceWith({ | ||
| error: originalError, | ||
| handlingStack: jasmine.any(String), | ||
| componentStack: 'at ComponentSpy toto.js', | ||
| startClocks: jasmine.any(Object), | ||
| context: { framework: 'nextjs' }, | ||
| }) | ||
| }) | ||
|
|
||
| it('merges dd_context from the original error with nextjs error context', () => { | ||
| const { addErrorSpy } = initializeNextjsPlugin() | ||
| const originalError = new Error('error message') | ||
| ;(originalError as any).dd_context = { component: 'Menu', param: 123 } | ||
|
|
||
| addNextjsError(originalError, {}) | ||
|
|
||
| expect(addErrorSpy).toHaveBeenCalledWith( | ||
| jasmine.objectContaining({ | ||
| error: originalError, | ||
| context: { | ||
| framework: 'nextjs', | ||
| component: 'Menu', | ||
| param: 123, | ||
| }, | ||
| }) | ||
| ) | ||
| }) | ||
|
|
||
| it('adds nextjs.digest context when error.digest is present', () => { | ||
| const { addErrorSpy } = initializeNextjsPlugin() | ||
| const error = Object.assign(new Error('server error'), { digest: 'abc123' }) | ||
|
|
||
| addNextjsError(error, {}) | ||
|
|
||
| expect(addErrorSpy.calls.mostRecent().args[0]).toEqual( | ||
| jasmine.objectContaining({ | ||
| context: jasmine.objectContaining({ framework: 'nextjs', nextjs: { digest: 'abc123' } }), | ||
| }) | ||
| ) | ||
| }) | ||
|
|
||
| it('omits nextjs key when digest is undefined', () => { | ||
| const { addErrorSpy } = initializeNextjsPlugin() | ||
| const error = new Error('client error') | ||
|
|
||
| addNextjsError(error) | ||
|
|
||
| expect(addErrorSpy.calls.mostRecent().args[0]).toEqual( | ||
| jasmine.objectContaining({ | ||
| context: { framework: 'nextjs' }, | ||
| }) | ||
| ) | ||
| }) | ||
|
|
||
| it('omits componentStack when errorInfo is missing', () => { | ||
| const { addErrorSpy } = initializeNextjsPlugin() | ||
| const error = new Error('client error') | ||
|
|
||
| addNextjsError(error) | ||
|
|
||
| expect(addErrorSpy.calls.mostRecent().args[0]).toEqual( | ||
| jasmine.objectContaining({ | ||
| componentStack: undefined, | ||
| }) | ||
| ) | ||
| }) | ||
|
|
||
| it('does not let error.dd_context overwrite framework', () => { | ||
| const { addErrorSpy } = initializeNextjsPlugin() | ||
| const error = Object.assign(new Error('test error'), { dd_context: { framework: 'from-dd-context' } }) | ||
|
|
||
| addNextjsError(error, {}) | ||
|
|
||
| expect(addErrorSpy.calls.mostRecent().args[0]).toEqual( | ||
| jasmine.objectContaining({ | ||
| context: jasmine.objectContaining({ framework: 'nextjs' }), | ||
| }) | ||
| ) | ||
| }) | ||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| import { callMonitored, clocksNow, createHandlingStack } from '@datadog/browser-core' | ||
| import type { Context } from '@datadog/browser-core' | ||
| import type { ErrorInfo } from 'react' | ||
| import { onRumStart } from '../nextjsPlugin' | ||
|
|
||
| /** | ||
| * Add a Next.js error to the RUM session. | ||
| * | ||
| * @category Error | ||
| * @example | ||
| * ```ts | ||
| * // app/error.tsx (or app/global-error.tsx) | ||
| * 'use client' | ||
| * import { useEffect } from 'react' | ||
| * import { addNextjsError } from '@datadog/browser-rum-nextjs' | ||
| * | ||
| * export default function Error({ error }: { error: Error & { digest?: string } }) { | ||
| * useEffect(() => { | ||
| * addNextjsError(error) | ||
| * }, [error]) | ||
| * return <div>Something went wrong</div> | ||
| * } | ||
| * ``` | ||
| */ | ||
| export function addNextjsError(error: Error & { digest?: string }, errorInfo?: ErrorInfo) { | ||
| const handlingStack = createHandlingStack('nextjs error') | ||
| const startClocks = clocksNow() | ||
| onRumStart((addError) => { | ||
| callMonitored(() => { | ||
| addError({ | ||
| error, | ||
| handlingStack, | ||
|
mormubis marked this conversation as resolved.
|
||
| componentStack: errorInfo?.componentStack ?? undefined, | ||
| startClocks, | ||
| context: { | ||
| ...(error as Error & { dd_context?: Context }).dd_context, | ||
| ...(error.digest && { nextjs: { digest: error.digest } }), | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❓ question: Should |
||
| framework: 'nextjs', | ||
| }, | ||
| }) | ||
| }) | ||
| }) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| .next | ||
| node_modules | ||
| .yarn/* | ||
| .yarn/* | ||
| next-env.d.ts |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| // Segment-level error boundary for the error-test route (https://nextjs.org/docs/app/api-reference/file-conventions/error). | ||
| // Calls addNextjsError to report both client errors and server errors (with digest) to RUM. | ||
| 'use client' | ||
|
|
||
| import { addNextjsError } from '@datadog/browser-rum-nextjs' | ||
| import Link from 'next/link' | ||
| import { useEffect } from 'react' | ||
|
|
||
| export default function ErrorBoundary({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) { | ||
| useEffect(() => { | ||
| addNextjsError(error) | ||
| }, [error]) | ||
|
|
||
| return ( | ||
| <div data-testid="error-boundary"> | ||
| <h2>Something went wrong!</h2> | ||
| {error.digest && <p data-testid="error-digest">Digest: {error.digest}</p>} | ||
| <button onClick={reset}>Try again</button> | ||
| <br /> | ||
| <Link href="/">Go to Home</Link> | ||
| </div> | ||
| ) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| // Error test page. Triggers a client-side error | ||
| // and verifies it is captured by error.tsx via addNextjsError. | ||
| 'use client' | ||
|
|
||
| import { useState } from 'react' | ||
| import Link from 'next/link' | ||
|
|
||
| export default function ErrorTestPage() { | ||
| const [shouldThrow, setShouldThrow] = useState(false) | ||
|
|
||
| if (shouldThrow) { | ||
| throw new Error('Client error from error-test') | ||
| } | ||
|
|
||
| return ( | ||
| <div> | ||
| <Link href="/">← Back to Home</Link> | ||
| <h1>Error Test</h1> | ||
| <button data-testid="trigger-error" onClick={() => setShouldThrow(true)}> | ||
| Trigger Error | ||
| </button> | ||
| </div> | ||
| ) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| // Server component that throws when ?throw=true is present. Used to verify that server errors | ||
| // are caught by the parent error.tsx with a digest attached to the error context. | ||
| // The ?throw=true guard prevents Next.js from failing the build during static prerendering. | ||
| export default async function ServerErrorPage({ searchParams }: { searchParams: Promise<{ throw?: string }> }) { | ||
| const { throw: shouldThrow } = await searchParams | ||
|
|
||
| if (shouldThrow === 'true') { | ||
| throw new Error('Server error from error-test') | ||
| } | ||
|
|
||
| return ( | ||
| <div> | ||
| <h1>Server Error Test</h1> | ||
| </div> | ||
| ) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| // Server component that throws at root level when ?throw=true, triggering global-error.tsx. | ||
| // Needed because global-error.tsx only activates for errors that escape the root layout. | ||
| export default async function GlobalErrorTestPage({ searchParams }: { searchParams: Promise<{ throw?: string }> }) { | ||
| const { throw: shouldThrow } = await searchParams | ||
| if (shouldThrow === 'true') throw new Error('Global error test') | ||
| return <div>Global error test page</div> | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| // Global error boundary — catches errors that escape the root layout (https://nextjs.org/docs/app/getting-started/error-handling#global-errors). | ||
| // Calls addNextjsError so tests can verify RUM captures root-level errors. | ||
| 'use client' | ||
|
|
||
| import { addNextjsError } from '@datadog/browser-rum-nextjs' | ||
| import { useEffect } from 'react' | ||
|
|
||
| export default function GlobalError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) { | ||
| useEffect(() => { | ||
| addNextjsError(error) | ||
| }, [error]) | ||
|
|
||
| return ( | ||
| <html> | ||
| <body> | ||
| <div data-testid="global-error-boundary"> | ||
| <h2>Global error!</h2> | ||
| {error.digest && <p data-testid="error-digest">Digest: {error.digest}</p>} | ||
| <button onClick={reset}>Try again</button> | ||
| <br /> | ||
| <a href="/">Go to Home</a> | ||
| </div> | ||
| </body> | ||
| </html> | ||
| ) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.