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);
},
);