diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 8fd537898b28..73e5a1c6ee3c 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -904,6 +904,7 @@ jobs:
'nextjs-13',
'nextjs-14',
'nextjs-15',
+ 'nextjs-turbo',
'nextjs-t3',
'react-17',
'react-19',
@@ -1126,6 +1127,12 @@ jobs:
- test-application: 'nextjs-15'
build-command: 'test:build-latest'
label: 'nextjs-15 (latest)'
+ - test-application: 'nextjs-turbo'
+ build-command: 'test:build-canary'
+ label: 'nextjs-turbo (canary)'
+ - test-application: 'nextjs-turbo'
+ build-command: 'test:build-latest'
+ label: 'nextjs-turbo (latest)'
steps:
- name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }})
diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml
index 4b8fff855049..b964e6b3d1b0 100644
--- a/.github/workflows/canary.yml
+++ b/.github/workflows/canary.yml
@@ -96,6 +96,12 @@ jobs:
- test-application: 'nextjs-15'
build-command: 'test:build-latest'
label: 'nextjs-15 (latest)'
+ - test-application: 'nextjs-turbo'
+ build-command: 'test:build-canary'
+ label: 'nextjs-turbo (canary)'
+ - test-application: 'nextjs-turbo'
+ build-command: 'test:build-latest'
+ label: 'nextjs-turbo (latest)'
- test-application: 'react-create-hash-router'
build-command: 'test:build-canary'
label: 'react-create-hash-router (canary)'
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/.gitignore b/dev-packages/e2e-tests/test-applications/nextjs-turbo/.gitignore
new file mode 100644
index 000000000000..ebdbfc025b6a
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/.gitignore
@@ -0,0 +1,46 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
+
+# local env files
+.env*.local
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
+
+!*.d.ts
+
+# Sentry
+.sentryclirc
+
+.vscode
+
+test-results
+event-dumps
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/.npmrc b/dev-packages/e2e-tests/test-applications/nextjs-turbo/.npmrc
new file mode 100644
index 000000000000..070f80f05092
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/.npmrc
@@ -0,0 +1,2 @@
+@sentry:registry=http://127.0.0.1:4873
+@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/[param]/rsc-page-error/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/[param]/rsc-page-error/page.tsx
new file mode 100644
index 000000000000..a6ae11918445
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/[param]/rsc-page-error/page.tsx
@@ -0,0 +1,9 @@
+export const dynamic = 'force-dynamic';
+
+export default function Page() {
+ if (Math.random() > -1) {
+ throw new Error('page rsc render error');
+ }
+
+ return null;
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/global-error.tsx b/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/global-error.tsx
new file mode 100644
index 000000000000..912ad3606a61
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/global-error.tsx
@@ -0,0 +1,27 @@
+'use client';
+
+import * as Sentry from '@sentry/nextjs';
+import NextError from 'next/error';
+import { useEffect } from 'react';
+
+export default function GlobalError({
+ error,
+}: {
+ error: Error & { digest?: string };
+}) {
+ useEffect(() => {
+ Sentry.captureException(error);
+ }, [error]);
+
+ return (
+
+
+ {/* `NextError` is the default Next.js error page component. Its type
+ definition requires a `statusCode` prop. However, since the App Router
+ does not expose status codes for errors, we simply pass 0 to render a
+ generic error message. */}
+
+
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/layout.tsx
new file mode 100644
index 000000000000..c8f9cee0b787
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/layout.tsx
@@ -0,0 +1,7 @@
+export default function Layout({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/globals.d.ts b/dev-packages/e2e-tests/test-applications/nextjs-turbo/globals.d.ts
new file mode 100644
index 000000000000..109dbcd55648
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/globals.d.ts
@@ -0,0 +1,4 @@
+interface Window {
+ recordedTransactions?: string[];
+ capturedExceptionId?: string;
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/instrumentation.ts b/dev-packages/e2e-tests/test-applications/nextjs-turbo/instrumentation.ts
new file mode 100644
index 000000000000..964f937c439a
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/instrumentation.ts
@@ -0,0 +1,13 @@
+import * as Sentry from '@sentry/nextjs';
+
+export async function register() {
+ if (process.env.NEXT_RUNTIME === 'nodejs') {
+ await import('./sentry.server.config');
+ }
+
+ if (process.env.NEXT_RUNTIME === 'edge') {
+ await import('./sentry.edge.config');
+ }
+}
+
+export const onRequestError = Sentry.captureRequestError;
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/next-env.d.ts b/dev-packages/e2e-tests/test-applications/nextjs-turbo/next-env.d.ts
new file mode 100644
index 000000000000..40c3d68096c2
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/next-env.d.ts
@@ -0,0 +1,5 @@
+///
+///
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/next.config.js b/dev-packages/e2e-tests/test-applications/nextjs-turbo/next.config.js
new file mode 100644
index 000000000000..e09e64bac6a2
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/next.config.js
@@ -0,0 +1,12 @@
+const { withSentryConfig } = require('@sentry/nextjs');
+
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+ experimental: {
+ turbo: {}, // Enables Turbopack for builds
+ },
+};
+
+module.exports = withSentryConfig(nextConfig, {
+ silent: true,
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json b/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json
new file mode 100644
index 000000000000..900e0b5b2efc
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json
@@ -0,0 +1,49 @@
+{
+ "name": "create-next-app",
+ "version": "0.1.0",
+ "private": true,
+ "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:build": "pnpm install && npx playwright install && pnpm build",
+ "test:build-canary": "pnpm install && pnpm add next@canary && pnpm add react@canary && pnpm add react-dom@canary && npx playwright install && pnpm build",
+ "test:build-latest": "pnpm install && pnpm add next@latest && pnpm add react@rc && pnpm add react-dom@rc && npx playwright install && pnpm build",
+ "test:assert": "pnpm test:prod && pnpm test:dev"
+ },
+ "dependencies": {
+ "@sentry/nextjs": "latest || *",
+ "@types/node": "18.11.17",
+ "@types/react": "18.0.26",
+ "@types/react-dom": "18.0.9",
+ "next": "15.0.0",
+ "react": "rc",
+ "react-dom": "rc",
+ "typescript": "4.9.5"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.44.1",
+ "@sentry-internal/test-utils": "link:../../../test-utils",
+ "@sentry-internal/feedback": "latest || *",
+ "@sentry-internal/replay-canvas": "latest || *",
+ "@sentry-internal/browser-utils": "latest || *",
+ "@sentry/browser": "latest || *",
+ "@sentry/core": "latest || *",
+ "@sentry/nextjs": "latest || *",
+ "@sentry/node": "latest || *",
+ "@sentry/opentelemetry": "latest || *",
+ "@sentry/react": "latest || *",
+ "@sentry-internal/replay": "latest || *",
+ "@sentry/types": "latest || *",
+ "@sentry/utils": "latest || *",
+ "@sentry/vercel-edge": "latest || *",
+ "import-in-the-middle": "1.11.2"
+ },
+ "overrides": {
+ "import-in-the-middle": "1.11.2"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/nextjs-turbo/playwright.config.mjs
new file mode 100644
index 000000000000..a62bec62a5c8
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/playwright.config.mjs
@@ -0,0 +1,19 @@
+import { getPlaywrightConfig } from '@sentry-internal/test-utils';
+const testEnv = process.env.TEST_ENV;
+
+if (!testEnv) {
+ throw new Error('No test env defined');
+}
+
+const config = getPlaywrightConfig(
+ {
+ startCommand: testEnv === 'development' ? 'pnpm next dev -p 3030 --turbo' : 'pnpm next start -p 3030',
+ port: 3030,
+ },
+ {
+ // This comes with the risk of tests leaking into each other but the tests run quite slow so we should parallelize
+ workers: '100%',
+ },
+);
+
+export default config;
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/sentry.client.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-turbo/sentry.client.config.ts
new file mode 100644
index 000000000000..85bd765c9c44
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/sentry.client.config.ts
@@ -0,0 +1,9 @@
+import * as Sentry from '@sentry/nextjs';
+
+Sentry.init({
+ environment: 'qa', // dynamic sampling bias to keep transactions
+ dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN,
+ tunnel: `http://localhost:3031/`, // proxy server
+ tracesSampleRate: 1.0,
+ sendDefaultPii: true,
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/sentry.edge.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-turbo/sentry.edge.config.ts
new file mode 100644
index 000000000000..067d2ead0b8b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/sentry.edge.config.ts
@@ -0,0 +1,13 @@
+import * as Sentry from '@sentry/nextjs';
+
+Sentry.init({
+ environment: 'qa', // dynamic sampling bias to keep transactions
+ dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN,
+ tunnel: `http://localhost:3031/`, // proxy server
+ tracesSampleRate: 1.0,
+ sendDefaultPii: true,
+ transportOptions: {
+ // We are doing a lot of events at once in this test
+ bufferSize: 1000,
+ },
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-turbo/sentry.server.config.ts
new file mode 100644
index 000000000000..067d2ead0b8b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/sentry.server.config.ts
@@ -0,0 +1,13 @@
+import * as Sentry from '@sentry/nextjs';
+
+Sentry.init({
+ environment: 'qa', // dynamic sampling bias to keep transactions
+ dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN,
+ tunnel: `http://localhost:3031/`, // proxy server
+ tracesSampleRate: 1.0,
+ sendDefaultPii: true,
+ transportOptions: {
+ // We are doing a lot of events at once in this test
+ bufferSize: 1000,
+ },
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nextjs-turbo/start-event-proxy.mjs
new file mode 100644
index 000000000000..2773cf8fa977
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/start-event-proxy.mjs
@@ -0,0 +1,14 @@
+import * as fs from 'fs';
+import * as path from 'path';
+import { startEventProxyServer } from '@sentry-internal/test-utils';
+
+const packageJson = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json')));
+
+startEventProxyServer({
+ port: 3031,
+ proxyServerName: 'nextjs-turbo',
+ envelopeDumpPath: path.join(
+ process.cwd(),
+ `event-dumps/nextjs-turbo-${packageJson.dependencies.next}-${process.env.TEST_ENV}.dump`,
+ ),
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/tests/rsc-error.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-turbo/tests/rsc-error.test.ts
new file mode 100644
index 000000000000..604faae7ea59
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/tests/rsc-error.test.ts
@@ -0,0 +1,14 @@
+import { expect, test } from '@playwright/test';
+import { waitForError } from '@sentry-internal/test-utils';
+
+test('Should capture errors from server components', async ({ page }) => {
+ const errorEventPromise = waitForError('nextjs-turbo', errorEvent => {
+ return !!errorEvent?.exception?.values?.some(value => value.value === 'page rsc render error');
+ });
+
+ await page.goto(`/123/rsc-page-error`);
+
+ const errorEvent = await errorEventPromise;
+
+ expect(errorEvent).toBeDefined();
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/tsconfig.json b/dev-packages/e2e-tests/test-applications/nextjs-turbo/tsconfig.json
new file mode 100644
index 000000000000..ef9e351d7a7b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "target": "es2018",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "incremental": true
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "next.config.js", ".next/types/**/*.ts"],
+ "exclude": ["node_modules", "playwright.config.ts"]
+}