diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/app/suspense-error/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-15/app/suspense-error/page.tsx new file mode 100644 index 000000000000..007e2e2feac2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/app/suspense-error/page.tsx @@ -0,0 +1,15 @@ +import * as Sentry from '@sentry/nextjs'; +import { use } from 'react'; +export const dynamic = 'force-dynamic'; + +export default async function Page() { + try { + use(fetch('http://example.com/')); + } catch (e) { + Sentry.captureException(e); // This error should not be reported + await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for any async event processors to run + await Sentry.flush(); + } + + return
test
; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/suspense-error.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/suspense-error.test.ts new file mode 100644 index 000000000000..ddbc4a9edee3 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/suspense-error.test.ts @@ -0,0 +1,24 @@ +import { expect, test } from '@playwright/test'; +import { waitForError, waitForTransaction } from '@sentry-internal/event-proxy-server'; + +test('should not capture serverside suspense errors', async ({ page }) => { + const pageServerComponentTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => { + return transactionEvent?.transaction === 'Page Server Component (/suspense-error)'; + }); + + let errorEvent; + waitForError('nextjs-15', async transactionEvent => { + return transactionEvent?.transaction === 'Page Server Component (/suspense-error)'; + }).then(event => { + errorEvent = event; + }); + + await page.goto(`/suspense-error`); + + await page.waitForTimeout(5000); + + const pageServerComponentTransaction = await pageServerComponentTransactionPromise; + expect(pageServerComponentTransaction).toBeDefined(); + + expect(errorEvent).toBeUndefined(); +}); diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index 2b5d7251186a..9ffdcfdc6225 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -190,13 +190,23 @@ export function init(options: NodeOptions): void { const originalException = hint.originalException; - const isReactControlFlowError = + const isPostponeError = typeof originalException === 'object' && originalException !== null && '$$typeof' in originalException && originalException.$$typeof === Symbol.for('react.postpone'); - if (isReactControlFlowError) { + if (isPostponeError) { + // Postpone errors are used for partial-pre-rendering (PPR) + return null; + } + + // We don't want to capture suspense errors as they are simply used by React/Next.js for control flow + const exceptionMessage = event.exception?.values?.[0]?.value; + if ( + exceptionMessage?.includes('Suspense Exception: This is not a real error!') || + exceptionMessage?.includes('Suspense Exception: This is not a real error, and should not leak') + ) { return null; }