diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/app/nested-rsc-error/[param]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-15/app/nested-rsc-error/[param]/page.tsx
new file mode 100644
index 000000000000..675b248026be
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15/app/nested-rsc-error/[param]/page.tsx
@@ -0,0 +1,17 @@
+import { Suspense } from 'react';
+
+export const dynamic = 'force-dynamic';
+
+export default async function Page() {
+ return (
+ Loading...
}>
+ {/* @ts-ignore */}
+ ;
+
+ );
+}
+
+async function Crash() {
+ throw new Error('I am technically uncatchable');
+ return unreachable
;
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/app/streaming-rsc-error/[param]/client-page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-15/app/streaming-rsc-error/[param]/client-page.tsx
new file mode 100644
index 000000000000..7b66c3fbdeef
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15/app/streaming-rsc-error/[param]/client-page.tsx
@@ -0,0 +1,8 @@
+'use client';
+
+import { use } from 'react';
+
+export function RenderPromise({ stringPromise }: { stringPromise: Promise }) {
+ const s = use(stringPromise);
+ return <>{s}>;
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/app/streaming-rsc-error/[param]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-15/app/streaming-rsc-error/[param]/page.tsx
new file mode 100644
index 000000000000..9531f9a42139
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15/app/streaming-rsc-error/[param]/page.tsx
@@ -0,0 +1,18 @@
+import { Suspense } from 'react';
+import { RenderPromise } from './client-page';
+
+export const dynamic = 'force-dynamic';
+
+export default async function Page() {
+ const crashingPromise = new Promise((_, reject) => {
+ setTimeout(() => {
+ reject(new Error('I am a data streaming error'));
+ }, 100);
+ });
+
+ return (
+ Loading...}>
+ ;
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/instrumentation.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/instrumentation.ts
index 7b89a972e157..ca4a213e58ba 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-15/instrumentation.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15/instrumentation.ts
@@ -1,3 +1,5 @@
+import * as Sentry from '@sentry/nextjs';
+
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
await import('./sentry.server.config');
@@ -7,3 +9,5 @@ export async function register() {
await import('./sentry.edge.config');
}
}
+
+export const onRequestError = Sentry.experimental_captureRequestError;
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/package.json b/dev-packages/e2e-tests/test-applications/nextjs-15/package.json
index ebd18c6fb10e..4c3f56b0aa0c 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-15/package.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15/package.json
@@ -5,8 +5,8 @@
"scripts": {
"build": "next build > .tmp_build_stdout 2> .tmp_build_stderr || (cat .tmp_build_stdout && cat .tmp_build_stderr && exit 1)",
"clean": "npx rimraf node_modules pnpm-lock.yaml",
- "test:prod": "TEST_ENV=production playwright test",
- "test:dev": "TEST_ENV=development playwright test",
+ "test:prod": "TEST_ENV=production __NEXT_EXPERIMENTAL_INSTRUMENTATION=1 playwright test",
+ "test:dev": "TEST_ENV=development __NEXT_EXPERIMENTAL_INSTRUMENTATION=1 playwright test",
"test:build": "pnpm install && npx playwright install && pnpm build",
"test:build-canary": "pnpm install && pnpm add next@rc && pnpm add react@beta && pnpm add react-dom@beta && npx playwright install && pnpm build",
"test:build-latest": "pnpm install && pnpm add next@rc && pnpm add react@beta && pnpm add react-dom@beta && npx playwright install && pnpm build",
@@ -17,7 +17,7 @@
"@types/node": "18.11.17",
"@types/react": "18.0.26",
"@types/react-dom": "18.0.9",
- "next": "14.3.0-canary.73",
+ "next": "15.0.0-canary.63",
"react": "beta",
"react-dom": "beta",
"typescript": "4.9.5"
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/nested-rsc-error.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/nested-rsc-error.test.ts
new file mode 100644
index 000000000000..223da5b245e9
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/nested-rsc-error.test.ts
@@ -0,0 +1,33 @@
+import { expect, test } from '@playwright/test';
+import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
+
+test('Should capture errors from nested server components when `Sentry.captureRequestError` is added to the `onRequestError` hook', async ({
+ page,
+}) => {
+ const errorEventPromise = waitForError('nextjs-15', errorEvent => {
+ return !!errorEvent?.exception?.values?.some(value => value.value === 'I am technically uncatchable');
+ });
+
+ const serverTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => {
+ return transactionEvent?.transaction === 'GET /nested-rsc-error/[param]';
+ });
+
+ await page.goto(`/nested-rsc-error/123`);
+ const errorEvent = await errorEventPromise;
+ const serverTransactionEvent = await serverTransactionPromise;
+
+ // error event is part of the transaction
+ expect(errorEvent.contexts?.trace?.trace_id).toBe(serverTransactionEvent.contexts?.trace?.trace_id);
+
+ expect(errorEvent.request).toMatchObject({
+ headers: expect.any(Object),
+ method: 'GET',
+ });
+
+ expect(errorEvent.contexts?.nextjs).toEqual({
+ route_type: 'render',
+ router_kind: 'App Router',
+ router_path: '/nested-rsc-error/[param]',
+ request_path: '/nested-rsc-error/123',
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/streaming-rsc-error.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/streaming-rsc-error.test.ts
new file mode 100644
index 000000000000..b50e9688861e
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/streaming-rsc-error.test.ts
@@ -0,0 +1,33 @@
+import { expect, test } from '@playwright/test';
+import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
+
+test('Should capture errors for crashing streaming promises in server components when `Sentry.captureRequestError` is added to the `onRequestError` hook', async ({
+ page,
+}) => {
+ const errorEventPromise = waitForError('nextjs-15', errorEvent => {
+ return !!errorEvent?.exception?.values?.some(value => value.value === 'I am a data streaming error');
+ });
+
+ const serverTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => {
+ return transactionEvent?.transaction === 'GET /streaming-rsc-error/[param]';
+ });
+
+ await page.goto(`/streaming-rsc-error/123`);
+ const errorEvent = await errorEventPromise;
+ const serverTransactionEvent = await serverTransactionPromise;
+
+ // error event is part of the transaction
+ expect(errorEvent.contexts?.trace?.trace_id).toBe(serverTransactionEvent.contexts?.trace?.trace_id);
+
+ expect(errorEvent.request).toMatchObject({
+ headers: expect.any(Object),
+ method: 'GET',
+ });
+
+ expect(errorEvent.contexts?.nextjs).toEqual({
+ route_type: 'render',
+ router_kind: 'App Router',
+ router_path: '/streaming-rsc-error/[param]',
+ request_path: '/streaming-rsc-error/123',
+ });
+});
diff --git a/packages/nextjs/src/common/captureRequestError.ts b/packages/nextjs/src/common/captureRequestError.ts
new file mode 100644
index 000000000000..7968907ad9bf
--- /dev/null
+++ b/packages/nextjs/src/common/captureRequestError.ts
@@ -0,0 +1,50 @@
+import { captureException, withScope } from '@sentry/core';
+
+type RequestInfo = {
+ url: string;
+ method: string;
+ headers: Record;
+};
+
+type ErrorContext = {
+ routerKind: string; // 'Pages Router' | 'App Router'
+ routePath: string;
+ routeType: string; // 'render' | 'route' | 'middleware'
+};
+
+/**
+ * Reports errors for the Next.js `onRequestError` instrumentation hook.
+ *
+ * Notice: This function is experimental and not intended for production use. Breaking changes may be done to this funtion in any release.
+ *
+ * @experimental
+ */
+export function experimental_captureRequestError(
+ error: unknown,
+ request: RequestInfo,
+ errorContext: ErrorContext,
+): void {
+ withScope(scope => {
+ scope.setSDKProcessingMetadata({
+ request: {
+ headers: request.headers,
+ method: request.method,
+ },
+ });
+
+ scope.setContext('nextjs', {
+ request_path: request.url,
+ router_kind: errorContext.routerKind,
+ router_path: errorContext.routePath,
+ route_type: errorContext.routeType,
+ });
+
+ scope.setTransactionName(errorContext.routePath);
+
+ captureException(error, {
+ mechanism: {
+ handled: false,
+ },
+ });
+ });
+}
diff --git a/packages/nextjs/src/common/index.ts b/packages/nextjs/src/common/index.ts
index e308537f1358..23ddfa383772 100644
--- a/packages/nextjs/src/common/index.ts
+++ b/packages/nextjs/src/common/index.ts
@@ -1,25 +1,14 @@
export { wrapGetStaticPropsWithSentry } from './wrapGetStaticPropsWithSentry';
-
export { wrapGetInitialPropsWithSentry } from './wrapGetInitialPropsWithSentry';
-
export { wrapAppGetInitialPropsWithSentry } from './wrapAppGetInitialPropsWithSentry';
-
export { wrapDocumentGetInitialPropsWithSentry } from './wrapDocumentGetInitialPropsWithSentry';
-
export { wrapErrorGetInitialPropsWithSentry } from './wrapErrorGetInitialPropsWithSentry';
-
export { wrapGetServerSidePropsWithSentry } from './wrapGetServerSidePropsWithSentry';
-
export { wrapServerComponentWithSentry } from './wrapServerComponentWithSentry';
-
export { wrapRouteHandlerWithSentry } from './wrapRouteHandlerWithSentry';
-
export { wrapApiHandlerWithSentryVercelCrons } from './wrapApiHandlerWithSentryVercelCrons';
-
export { wrapMiddlewareWithSentry } from './wrapMiddlewareWithSentry';
-
export { wrapPageComponentWithSentry } from './wrapPageComponentWithSentry';
-
export { wrapGenerationFunctionWithSentry } from './wrapGenerationFunctionWithSentry';
-
export { withServerActionInstrumentation } from './withServerActionInstrumentation';
+export { experimental_captureRequestError } from './captureRequestError';
diff --git a/packages/nextjs/src/index.types.ts b/packages/nextjs/src/index.types.ts
index afff0bd98a19..b093968bdebe 100644
--- a/packages/nextjs/src/index.types.ts
+++ b/packages/nextjs/src/index.types.ts
@@ -140,3 +140,5 @@ export declare function wrapApiHandlerWithSentryVercelCrons(WrappingTarget: C): C;
+
+export { experimental_captureRequestError } from './common/captureRequestError';