diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/app/layout.tsx b/packages/e2e-tests/test-applications/nextjs-app-dir/app/layout.tsx index 35c4704735e7..b93ad29e53cb 100644 --- a/packages/e2e-tests/test-applications/nextjs-app-dir/app/layout.tsx +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/app/layout.tsx @@ -29,6 +29,12 @@ export default function Layout({ children }: { children: React.ReactNode }) {
  • /server-component/parameter/foo/bar/baz
  • +
  • + /not-found +
  • +
  • + /redirect +
  • {children} diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/app/not-found.tsx b/packages/e2e-tests/test-applications/nextjs-app-dir/app/not-found.tsx index 5e7b156553a6..87ce52a19e73 100644 --- a/packages/e2e-tests/test-applications/nextjs-app-dir/app/not-found.tsx +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/app/not-found.tsx @@ -1,7 +1,10 @@ +import { ClientErrorDebugTools } from '../components/client-error-debug-tools'; + export default function NotFound() { return (

    Not found (/)

    ; +
    ); } diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/app/server-component/not-found/page.tsx b/packages/e2e-tests/test-applications/nextjs-app-dir/app/server-component/not-found/page.tsx new file mode 100644 index 000000000000..c88c2d097d4f --- /dev/null +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/app/server-component/not-found/page.tsx @@ -0,0 +1,7 @@ +import { notFound } from 'next/navigation'; + +export const dynamic = 'force-dynamic'; + +export default function Page() { + notFound(); +} diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/app/server-component/redirect/page.tsx b/packages/e2e-tests/test-applications/nextjs-app-dir/app/server-component/redirect/page.tsx new file mode 100644 index 000000000000..3df1746a97ff --- /dev/null +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/app/server-component/redirect/page.tsx @@ -0,0 +1,7 @@ +import { redirect } from 'next/navigation'; + +export const dynamic = 'force-dynamic'; + +export default function Page() { + redirect('/'); +} diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts b/packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts index 5ef4e6f28b5f..8dac4e58e5ba 100644 --- a/packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts @@ -89,4 +89,26 @@ if (process.env.TEST_ENV === 'production') { ) .toBe(200); }); + + test('Should not set an error status on a server component transaction when it redirects', async ({ page }) => { + const serverComponentTransactionPromise = waitForTransaction('nextjs-13-app-dir', async transactionEvent => { + return transactionEvent?.transaction === 'Page Server Component (/server-component/redirect)'; + }); + + await page.goto('/server-component/redirect'); + + expect((await serverComponentTransactionPromise).contexts?.trace?.status).not.toBe('internal_error'); + }); + + test('Should set a "not_found" status on a server component transaction when notFound() is called', async ({ + page, + }) => { + const serverComponentTransactionPromise = waitForTransaction('nextjs-13-app-dir', async transactionEvent => { + return transactionEvent?.transaction === 'Page Server Component (/server-component/not-found)'; + }); + + await page.goto('/server-component/not-found'); + + expect((await serverComponentTransactionPromise).contexts?.trace?.status).toBe('not_found'); + }); } diff --git a/packages/nextjs/src/common/nextNavigationErrorUtils.ts b/packages/nextjs/src/common/nextNavigationErrorUtils.ts new file mode 100644 index 000000000000..d4a67791525f --- /dev/null +++ b/packages/nextjs/src/common/nextNavigationErrorUtils.ts @@ -0,0 +1,21 @@ +import { isError } from '@sentry/utils'; + +/** + * Determines whether input is a Next.js not-found error. + * https://beta.nextjs.org/docs/api-reference/notfound#notfound + */ +export function isNotFoundNavigationError(subject: unknown): boolean { + return isError(subject) && (subject as Error & { digest?: unknown }).digest === 'NEXT_NOT_FOUND'; +} + +/** + * Determines whether input is a Next.js redirect error. + * https://beta.nextjs.org/docs/api-reference/redirect#redirect + */ +export function isRedirectNavigationError(subject: unknown): boolean { + return ( + isError(subject) && + typeof (subject as Error & { digest?: unknown }).digest === 'string' && + (subject as Error & { digest: string }).digest.startsWith('NEXT_REDIRECT;') // a redirect digest looks like "NEXT_REDIRECT;[redirect path]" + ); +} diff --git a/packages/nextjs/src/server/wrapServerComponentWithSentry.ts b/packages/nextjs/src/server/wrapServerComponentWithSentry.ts index 12ff9ceb5f72..2c25e8811409 100644 --- a/packages/nextjs/src/server/wrapServerComponentWithSentry.ts +++ b/packages/nextjs/src/server/wrapServerComponentWithSentry.ts @@ -2,6 +2,7 @@ import { addTracingExtensions, captureException, getCurrentHub, startTransaction import { baggageHeaderToDynamicSamplingContext, extractTraceparentData } from '@sentry/utils'; import * as domain from 'domain'; +import { isNotFoundNavigationError, isRedirectNavigationError } from '../common/nextNavigationErrorUtils'; import type { ServerComponentContext } from '../common/types'; /** @@ -45,12 +46,24 @@ export function wrapServerComponentWithSentry any> currentScope.setSpan(transaction); } + const handleErrorCase = (e: unknown): void => { + if (isNotFoundNavigationError(e)) { + // We don't want to report "not-found"s + transaction.setStatus('not_found'); + } else if (isRedirectNavigationError(e)) { + // We don't want to report redirects + } else { + transaction.setStatus('internal_error'); + captureException(e); + } + + transaction.finish(); + }; + try { maybePromiseResult = originalFunction.apply(thisArg, args); } catch (e) { - transaction.setStatus('internal_error'); - captureException(e); - transaction.finish(); + handleErrorCase(e); throw e; } @@ -60,10 +73,8 @@ export function wrapServerComponentWithSentry any> () => { transaction.finish(); }, - (e: Error) => { - transaction.setStatus('internal_error'); - captureException(e); - transaction.finish(); + e => { + handleErrorCase(e); }, );