diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 99d4f14e611e..c0b66a2d8a12 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -987,7 +987,7 @@ jobs:
with:
node-version-file: 'dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}/package.json'
- name: Set up Bun
- if: matrix.test-application == 'node-exports-test-app'
+ if: contains(fromJSON('["node-exports-test-app","nextjs-16-bun"]'), matrix.test-application)
uses: oven-sh/setup-bun@v2
- name: Set up AWS SAM
if: matrix.test-application == 'aws-serverless'
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/.gitignore b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/.gitignore
new file mode 100644
index 000000000000..dd146b53d966
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/.gitignore
@@ -0,0 +1,44 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.*
+.yarn/*
+!.yarn/patches
+!.yarn/plugins
+!.yarn/releases
+!.yarn/versions
+
+# 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*
+
+# env files (can opt-in for committing if needed)
+.env*
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
+
+# Sentry Config File
+.env.sentry-build-plugin
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/.npmrc b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/.npmrc
new file mode 100644
index 000000000000..a3160f4de175
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/.npmrc
@@ -0,0 +1,4 @@
+@sentry:registry=http://127.0.0.1:4873
+@sentry-internal:registry=http://127.0.0.1:4873
+public-hoist-pattern[]=*import-in-the-middle*
+public-hoist-pattern[]=*require-in-the-middle*
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/(nested-layout)/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/(nested-layout)/layout.tsx
new file mode 100644
index 000000000000..ace0c2f086b7
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/(nested-layout)/layout.tsx
@@ -0,0 +1,12 @@
+import { PropsWithChildren } from 'react';
+
+export const dynamic = 'force-dynamic';
+
+export default function Layout({ children }: PropsWithChildren<{}>) {
+ return (
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/(nested-layout)/nested-layout/[dynamic]/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/(nested-layout)/nested-layout/[dynamic]/layout.tsx
new file mode 100644
index 000000000000..dbdc60adadc2
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/(nested-layout)/nested-layout/[dynamic]/layout.tsx
@@ -0,0 +1,12 @@
+import { PropsWithChildren } from 'react';
+
+export const dynamic = 'force-dynamic';
+
+export default function Layout({ children }: PropsWithChildren<{}>) {
+ return (
+
+
DynamicLayout
+ {children}
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/(nested-layout)/nested-layout/[dynamic]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/(nested-layout)/nested-layout/[dynamic]/page.tsx
new file mode 100644
index 000000000000..3eaddda2a1df
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/(nested-layout)/nested-layout/[dynamic]/page.tsx
@@ -0,0 +1,15 @@
+export const dynamic = 'force-dynamic';
+
+export default async function Page() {
+ return (
+
+ );
+}
+
+export async function generateMetadata() {
+ return {
+ title: 'I am dynamic page generated metadata',
+ };
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/(nested-layout)/nested-layout/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/(nested-layout)/nested-layout/layout.tsx
new file mode 100644
index 000000000000..ace0c2f086b7
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/(nested-layout)/nested-layout/layout.tsx
@@ -0,0 +1,12 @@
+import { PropsWithChildren } from 'react';
+
+export const dynamic = 'force-dynamic';
+
+export default function Layout({ children }: PropsWithChildren<{}>) {
+ return (
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/(nested-layout)/nested-layout/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/(nested-layout)/nested-layout/page.tsx
new file mode 100644
index 000000000000..8077c14d23ca
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/(nested-layout)/nested-layout/page.tsx
@@ -0,0 +1,11 @@
+export const dynamic = 'force-dynamic';
+
+export default function Page() {
+ return Hello World!
;
+}
+
+export async function generateMetadata() {
+ return {
+ title: 'I am generated metadata',
+ };
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/api/endpoint-behind-middleware/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/api/endpoint-behind-middleware/route.ts
new file mode 100644
index 000000000000..2733cc918f44
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/api/endpoint-behind-middleware/route.ts
@@ -0,0 +1,3 @@
+export function GET() {
+ return Response.json({ name: 'John Doe' });
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/global-error.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/global-error.tsx
new file mode 100644
index 000000000000..20c175015b03
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/global-error.tsx
@@ -0,0 +1,23 @@
+'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-16-bun/app/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/layout.tsx
new file mode 100644
index 000000000000..c8f9cee0b787
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/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-16-bun/app/nested-rsc-error/[param]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/nested-rsc-error/[param]/page.tsx
new file mode 100644
index 000000000000..675b248026be
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/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-16-bun/app/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/page.tsx
new file mode 100644
index 000000000000..3204f4df69e1
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/page.tsx
@@ -0,0 +1,3 @@
+export default function Page() {
+ return Next 16 Bun test app
;
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/pageload-tracing/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/pageload-tracing/layout.tsx
new file mode 100644
index 000000000000..1f0cbe478f88
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/pageload-tracing/layout.tsx
@@ -0,0 +1,8 @@
+import { PropsWithChildren } from 'react';
+
+export const dynamic = 'force-dynamic';
+
+export default async function Layout({ children }: PropsWithChildren) {
+ await new Promise(resolve => setTimeout(resolve, 500));
+ return <>{children}>;
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/pageload-tracing/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/pageload-tracing/page.tsx
new file mode 100644
index 000000000000..689735d61ddf
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/pageload-tracing/page.tsx
@@ -0,0 +1,14 @@
+export const dynamic = 'force-dynamic';
+
+export default async function Page() {
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ return I am page 2
;
+}
+
+export async function generateMetadata() {
+ (await fetch('https://example.com/', { cache: 'no-store' })).text();
+
+ return {
+ title: 'my title',
+ };
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/parameterized/[one]/beep/[two]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/parameterized/[one]/beep/[two]/page.tsx
new file mode 100644
index 000000000000..f34461c2bb07
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/parameterized/[one]/beep/[two]/page.tsx
@@ -0,0 +1,3 @@
+export default function ParameterizedPage() {
+ return Dynamic page two
;
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/parameterized/[one]/beep/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/parameterized/[one]/beep/page.tsx
new file mode 100644
index 000000000000..a7d9164c8c03
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/parameterized/[one]/beep/page.tsx
@@ -0,0 +1,3 @@
+export default function BeepPage() {
+ return Beep
;
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/parameterized/[one]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/parameterized/[one]/page.tsx
new file mode 100644
index 000000000000..9fa617a22381
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/parameterized/[one]/page.tsx
@@ -0,0 +1,3 @@
+export default function ParameterizedPage() {
+ return Dynamic page one
;
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/parameterized/static/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/parameterized/static/page.tsx
new file mode 100644
index 000000000000..16ef0482d53b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/parameterized/static/page.tsx
@@ -0,0 +1,3 @@
+export default function StaticPage() {
+ return Static page
;
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/propagation/test-outgoing-fetch-external-disallowed/check/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/propagation/test-outgoing-fetch-external-disallowed/check/route.ts
new file mode 100644
index 000000000000..e5f497a1bed6
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/propagation/test-outgoing-fetch-external-disallowed/check/route.ts
@@ -0,0 +1,5 @@
+import { checkHandler } from '../../utils';
+
+export const dynamic = 'force-dynamic';
+
+export const GET = checkHandler;
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/propagation/test-outgoing-fetch-external-disallowed/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/propagation/test-outgoing-fetch-external-disallowed/route.ts
new file mode 100644
index 000000000000..be38866a9e94
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/propagation/test-outgoing-fetch-external-disallowed/route.ts
@@ -0,0 +1,10 @@
+import { NextResponse } from 'next/server';
+
+export const dynamic = 'force-dynamic';
+
+export async function GET() {
+ const data = await fetch(`http://localhost:3030/propagation/test-outgoing-fetch-external-disallowed/check`, {
+ cache: 'no-store',
+ }).then(res => res.json());
+ return NextResponse.json(data);
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/propagation/test-outgoing-fetch/check/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/propagation/test-outgoing-fetch/check/route.ts
new file mode 100644
index 000000000000..e5f497a1bed6
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/propagation/test-outgoing-fetch/check/route.ts
@@ -0,0 +1,5 @@
+import { checkHandler } from '../../utils';
+
+export const dynamic = 'force-dynamic';
+
+export const GET = checkHandler;
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/propagation/test-outgoing-fetch/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/propagation/test-outgoing-fetch/route.ts
new file mode 100644
index 000000000000..4ee7fac97f83
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/propagation/test-outgoing-fetch/route.ts
@@ -0,0 +1,10 @@
+import { NextResponse } from 'next/server';
+
+export const dynamic = 'force-dynamic';
+
+export async function GET() {
+ const data = await fetch(`http://localhost:3030/propagation/test-outgoing-fetch/check`, { cache: 'no-store' }).then(
+ res => res.json(),
+ );
+ return NextResponse.json(data);
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/propagation/utils.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/propagation/utils.ts
new file mode 100644
index 000000000000..8e6b1cfff218
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/propagation/utils.ts
@@ -0,0 +1,13 @@
+import { headers } from 'next/headers';
+import { NextResponse } from 'next/server';
+
+export async function checkHandler() {
+ const headerList = await headers();
+
+ const headerObj: Record = {};
+ headerList.forEach((value, key) => {
+ headerObj[key] = value;
+ });
+
+ return NextResponse.json({ headers: headerObj });
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/route-handler/[xoxo]/edge/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/route-handler/[xoxo]/edge/route.ts
new file mode 100644
index 000000000000..7cd1fc7e332c
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/route-handler/[xoxo]/edge/route.ts
@@ -0,0 +1,8 @@
+import { NextResponse } from 'next/server';
+
+export const runtime = 'edge';
+export const dynamic = 'force-dynamic';
+
+export async function GET() {
+ return NextResponse.json({ message: 'Hello Edge Route Handler' });
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/route-handler/[xoxo]/node/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/route-handler/[xoxo]/node/route.ts
new file mode 100644
index 000000000000..5bc418f077aa
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/route-handler/[xoxo]/node/route.ts
@@ -0,0 +1,7 @@
+import { NextResponse } from 'next/server';
+
+export const dynamic = 'force-dynamic';
+
+export async function GET() {
+ return NextResponse.json({ message: 'Hello Node Route Handler' });
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/streaming-rsc-error/[param]/client-page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/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-16-bun/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-16-bun/app/streaming-rsc-error/[param]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/app/streaming-rsc-error/[param]/page.tsx
new file mode 100644
index 000000000000..9531f9a42139
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/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-16-bun/instrumentation-client.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/instrumentation-client.ts
new file mode 100644
index 000000000000..4870c64e7959
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/instrumentation-client.ts
@@ -0,0 +1,11 @@
+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,
+});
+
+export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/instrumentation.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/instrumentation.ts
new file mode 100644
index 000000000000..964f937c439a
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/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-16-bun/next.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/next.config.ts
new file mode 100644
index 000000000000..6699b3dd2c33
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/next.config.ts
@@ -0,0 +1,8 @@
+import { withSentryConfig } from '@sentry/nextjs';
+import type { NextConfig } from 'next';
+
+const nextConfig: NextConfig = {};
+
+export default withSentryConfig(nextConfig, {
+ silent: true,
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/package.json
new file mode 100644
index 000000000000..2540ca74678c
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "nextjs-16-bun",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "bun --bun next dev",
+ "build": "bun --bun 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 .tmp_dev_server_logs",
+ "start": "bun --bun next start",
+ "test:prod": "TEST_ENV=production playwright test",
+ "test:build": "pnpm install && pnpm build",
+ "test:assert": "pnpm test:prod"
+ },
+ "dependencies": {
+ "@sentry/nextjs": "latest || *",
+ "@sentry/core": "latest || *",
+ "import-in-the-middle": "^2",
+ "next": "16.1.5",
+ "react": "19.1.0",
+ "react-dom": "19.1.0",
+ "require-in-the-middle": "^8"
+ },
+ "devDependencies": {
+ "@playwright/test": "~1.56.0",
+ "@sentry-internal/test-utils": "link:../../../test-utils",
+ "@types/node": "^20",
+ "@types/react": "^19",
+ "@types/react-dom": "^19",
+ "typescript": "^5"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/playwright.config.mjs
new file mode 100644
index 000000000000..28662eab2d7b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/playwright.config.mjs
@@ -0,0 +1,25 @@
+import { getPlaywrightConfig } from '@sentry-internal/test-utils';
+const testEnv = process.env.TEST_ENV;
+
+if (!testEnv) {
+ throw new Error('No test env defined');
+}
+
+const getStartCommand = () => {
+ if (testEnv === 'development') {
+ return 'bun --bun next dev -p 3030 2>&1 | tee .tmp_dev_server_logs';
+ }
+
+ if (testEnv === 'production') {
+ return 'bun --bun next start -p 3030';
+ }
+
+ throw new Error(`Unknown test env: ${testEnv}`);
+};
+
+const config = getPlaywrightConfig({
+ startCommand: getStartCommand(),
+ port: 3030,
+});
+
+export default config;
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/proxy.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/proxy.ts
new file mode 100644
index 000000000000..60722f329fa0
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/proxy.ts
@@ -0,0 +1,24 @@
+import { getDefaultIsolationScope } from '@sentry/core';
+import * as Sentry from '@sentry/nextjs';
+import { NextResponse } from 'next/server';
+import type { NextRequest } from 'next/server';
+
+export async function proxy(request: NextRequest) {
+ Sentry.setTag('my-isolated-tag', true);
+ Sentry.setTag('my-global-scope-isolated-tag', getDefaultIsolationScope().getScopeData().tags['my-isolated-tag']); // We set this tag to be able to assert that the previously set tag has not leaked into the global isolation scope
+
+ if (request.headers.has('x-should-throw')) {
+ throw new Error('Middleware Error');
+ }
+
+ if (request.headers.has('x-should-make-request')) {
+ await fetch('http://localhost:3030/');
+ }
+
+ return NextResponse.next();
+}
+
+// See "Matching Paths" below to learn more
+export const config = {
+ matcher: ['/api/endpoint-behind-middleware', '/api/endpoint-behind-faulty-middleware'],
+};
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/sentry.edge.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/sentry.edge.config.ts
new file mode 100644
index 000000000000..85bd765c9c44
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/sentry.edge.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-16-bun/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/sentry.server.config.ts
new file mode 100644
index 000000000000..bf9c3a7f3ee9
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/sentry.server.config.ts
@@ -0,0 +1,10 @@
+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,
+ tracePropagationTargets: ['http://localhost:3030/propagation/test-outgoing-fetch/check'],
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/start-event-proxy.mjs
new file mode 100644
index 000000000000..d56d5fb74e59
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/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-16-bun',
+ envelopeDumpPath: path.join(
+ process.cwd(),
+ `event-dumps/nextjs-16-bun-v${packageJson.dependencies.next}-${process.env.TEST_ENV}.dump`,
+ ),
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/tests/middleware.test.ts
new file mode 100644
index 000000000000..9e569acae30f
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/tests/middleware.test.ts
@@ -0,0 +1,22 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+test('Should create a transaction for middleware', async ({ request }) => {
+ const middlewareTransactionPromise = waitForTransaction('nextjs-16-bun', async transactionEvent => {
+ return transactionEvent?.transaction === 'middleware GET';
+ });
+
+ const response = await request.get('/api/endpoint-behind-middleware');
+ expect(await response.json()).toStrictEqual({ name: 'John Doe' });
+
+ const middlewareTransaction = await middlewareTransactionPromise;
+
+ expect(middlewareTransaction.contexts?.trace?.status).toBe('ok');
+ expect(middlewareTransaction.contexts?.trace?.op).toBe('http.server.middleware');
+ expect(middlewareTransaction.contexts?.runtime?.name).toBe('node');
+ expect(middlewareTransaction.transaction_info?.source).toBe('route');
+
+ // Assert that isolation scope works properly
+ expect(middlewareTransaction.tags?.['my-isolated-tag']).toBe(true);
+ expect(middlewareTransaction.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/tests/nested-rsc-error.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/tests/nested-rsc-error.test.ts
new file mode 100644
index 000000000000..10cf1e353c32
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/tests/nested-rsc-error.test.ts
@@ -0,0 +1,38 @@
+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-16-bun', errorEvent => {
+ return !!errorEvent?.exception?.values?.some(value => value.value === 'I am technically uncatchable');
+ });
+
+ const serverTransactionPromise = waitForTransaction('nextjs-16-bun', 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',
+ });
+
+ expect(errorEvent.exception?.values?.[0]?.mechanism).toEqual({
+ handled: false,
+ type: 'auto.function.nextjs.on_request_error',
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/tests/pageload-tracing.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/tests/pageload-tracing.test.ts
new file mode 100644
index 000000000000..c1c103b5a11e
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/tests/pageload-tracing.test.ts
@@ -0,0 +1,28 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+test('App router transactions should be attached to the pageload request span', async ({ page }) => {
+ const serverTransactionPromise = waitForTransaction('nextjs-16-bun', async transactionEvent => {
+ return transactionEvent?.transaction === 'GET /pageload-tracing';
+ });
+
+ const pageloadTransactionPromise = waitForTransaction('nextjs-16-bun', async transactionEvent => {
+ return transactionEvent?.transaction === '/pageload-tracing';
+ });
+
+ await page.goto(`/pageload-tracing`);
+
+ const [serverTransaction, pageloadTransaction] = await Promise.all([
+ serverTransactionPromise,
+ pageloadTransactionPromise,
+ ]);
+
+ const pageloadTraceId = pageloadTransaction.contexts?.trace?.trace_id;
+
+ expect(pageloadTraceId).toBeTruthy();
+ expect(serverTransaction.contexts?.trace?.trace_id).toBe(pageloadTraceId);
+});
+
+// Bun runtime does not populate HTTP request headers as span attributes
+// because the OTel HTTP instrumentation does not extract headers when running on Bun.
+// This is a known behavioral difference from Node.js.
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/tests/parameterized-routes.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/tests/parameterized-routes.test.ts
new file mode 100644
index 000000000000..dc16f1590aa3
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/tests/parameterized-routes.test.ts
@@ -0,0 +1,189 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+test('should create a parameterized transaction when the `app` directory is used', async ({ page }) => {
+ const transactionPromise = waitForTransaction('nextjs-16-bun', async transactionEvent => {
+ return (
+ transactionEvent.transaction === '/parameterized/:one' && transactionEvent.contexts?.trace?.op === 'pageload'
+ );
+ });
+
+ await page.goto(`/parameterized/cappuccino`);
+
+ const transaction = await transactionPromise;
+
+ expect(transaction).toMatchObject({
+ breadcrumbs: expect.arrayContaining([
+ {
+ category: 'navigation',
+ data: { from: '/parameterized/cappuccino', to: '/parameterized/cappuccino' },
+ timestamp: expect.any(Number),
+ },
+ ]),
+ contexts: {
+ react: { version: expect.any(String) },
+ trace: {
+ data: {
+ 'sentry.op': 'pageload',
+ 'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation',
+ 'sentry.source': 'route',
+ },
+ op: 'pageload',
+ origin: 'auto.pageload.nextjs.app_router_instrumentation',
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ },
+ },
+ environment: 'qa',
+ request: {
+ headers: expect.any(Object),
+ url: expect.stringMatching(/\/parameterized\/cappuccino$/),
+ },
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ transaction: '/parameterized/:one',
+ transaction_info: { source: 'route' },
+ type: 'transaction',
+ });
+});
+
+test('should create a static transaction when the `app` directory is used and the route is not parameterized', async ({
+ page,
+}) => {
+ const transactionPromise = waitForTransaction('nextjs-16-bun', async transactionEvent => {
+ return (
+ transactionEvent.transaction === '/parameterized/static' && transactionEvent.contexts?.trace?.op === 'pageload'
+ );
+ });
+
+ await page.goto(`/parameterized/static`);
+
+ const transaction = await transactionPromise;
+
+ expect(transaction).toMatchObject({
+ breadcrumbs: expect.arrayContaining([
+ {
+ category: 'navigation',
+ data: { from: '/parameterized/static', to: '/parameterized/static' },
+ timestamp: expect.any(Number),
+ },
+ ]),
+ contexts: {
+ react: { version: expect.any(String) },
+ trace: {
+ data: {
+ 'sentry.op': 'pageload',
+ 'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation',
+ 'sentry.source': 'url',
+ },
+ op: 'pageload',
+ origin: 'auto.pageload.nextjs.app_router_instrumentation',
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ },
+ },
+ environment: 'qa',
+ request: {
+ headers: expect.any(Object),
+ url: expect.stringMatching(/\/parameterized\/static$/),
+ },
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ transaction: '/parameterized/static',
+ transaction_info: { source: 'url' },
+ type: 'transaction',
+ });
+});
+
+test('should create a partially parameterized transaction when the `app` directory is used', async ({ page }) => {
+ const transactionPromise = waitForTransaction('nextjs-16-bun', async transactionEvent => {
+ return (
+ transactionEvent.transaction === '/parameterized/:one/beep' && transactionEvent.contexts?.trace?.op === 'pageload'
+ );
+ });
+
+ await page.goto(`/parameterized/cappuccino/beep`);
+
+ const transaction = await transactionPromise;
+
+ expect(transaction).toMatchObject({
+ breadcrumbs: expect.arrayContaining([
+ {
+ category: 'navigation',
+ data: { from: '/parameterized/cappuccino/beep', to: '/parameterized/cappuccino/beep' },
+ timestamp: expect.any(Number),
+ },
+ ]),
+ contexts: {
+ react: { version: expect.any(String) },
+ trace: {
+ data: {
+ 'sentry.op': 'pageload',
+ 'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation',
+ 'sentry.source': 'route',
+ },
+ op: 'pageload',
+ origin: 'auto.pageload.nextjs.app_router_instrumentation',
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ },
+ },
+ environment: 'qa',
+ request: {
+ headers: expect.any(Object),
+ url: expect.stringMatching(/\/parameterized\/cappuccino\/beep$/),
+ },
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ transaction: '/parameterized/:one/beep',
+ transaction_info: { source: 'route' },
+ type: 'transaction',
+ });
+});
+
+test('should create a nested parameterized transaction when the `app` directory is used.', async ({ page }) => {
+ const transactionPromise = waitForTransaction('nextjs-16-bun', async transactionEvent => {
+ return (
+ transactionEvent.transaction === '/parameterized/:one/beep/:two' &&
+ transactionEvent.contexts?.trace?.op === 'pageload'
+ );
+ });
+
+ await page.goto(`/parameterized/cappuccino/beep/espresso`);
+
+ const transaction = await transactionPromise;
+
+ expect(transaction).toMatchObject({
+ breadcrumbs: expect.arrayContaining([
+ {
+ category: 'navigation',
+ data: { from: '/parameterized/cappuccino/beep/espresso', to: '/parameterized/cappuccino/beep/espresso' },
+ timestamp: expect.any(Number),
+ },
+ ]),
+ contexts: {
+ react: { version: expect.any(String) },
+ trace: {
+ data: {
+ 'sentry.op': 'pageload',
+ 'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation',
+ 'sentry.source': 'route',
+ },
+ op: 'pageload',
+ origin: 'auto.pageload.nextjs.app_router_instrumentation',
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ },
+ },
+ environment: 'qa',
+ request: {
+ headers: expect.any(Object),
+ url: expect.stringMatching(/\/parameterized\/cappuccino\/beep\/espresso$/),
+ },
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ transaction: '/parameterized/:one/beep/:two',
+ transaction_info: { source: 'route' },
+ type: 'transaction',
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/tests/propagation.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/tests/propagation.test.ts
new file mode 100644
index 000000000000..850a07bc25a2
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/tests/propagation.test.ts
@@ -0,0 +1,63 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+// Bun runtime does not propagate trace headers for outgoing fetch requests.
+// The OTel node_fetch instrumentation does not intercept Bun's native fetch,
+// so sentry-trace and baggage headers are not attached to outgoing requests.
+// This test documents the current limitation - un-skip when Bun fetch instrumentation is supported.
+test.skip('Propagates trace for outgoing fetch requests', async ({ baseURL, request }) => {
+ const inboundTransactionPromise = waitForTransaction('nextjs-16-bun', transactionEvent => {
+ return transactionEvent.transaction === 'GET /propagation/test-outgoing-fetch/check';
+ });
+
+ const outboundTransactionPromise = waitForTransaction('nextjs-16-bun', transactionEvent => {
+ return transactionEvent.transaction === 'GET /propagation/test-outgoing-fetch';
+ });
+
+ const { headers } = await (await request.get(`${baseURL}/propagation/test-outgoing-fetch`)).json();
+
+ const inboundTransaction = await inboundTransactionPromise;
+ const outboundTransaction = await outboundTransactionPromise;
+
+ expect(inboundTransaction.contexts?.trace?.trace_id).toStrictEqual(expect.any(String));
+ expect(inboundTransaction.contexts?.trace?.trace_id).toBe(outboundTransaction.contexts?.trace?.trace_id);
+
+ const httpClientSpan = outboundTransaction.spans?.find(
+ span => span.op === 'http.client' && span.data?.['sentry.origin'] === 'auto.http.otel.node_fetch',
+ );
+
+ expect(httpClientSpan).toBeDefined();
+ expect(httpClientSpan?.span_id).toStrictEqual(expect.any(String));
+ expect(inboundTransaction.contexts?.trace?.parent_span_id).toBe(httpClientSpan?.span_id);
+
+ expect(headers).toMatchObject({
+ baggage: expect.any(String),
+ 'sentry-trace': `${outboundTransaction.contexts?.trace?.trace_id}-${httpClientSpan?.span_id}-1`,
+ });
+});
+
+test('Does not propagate outgoing fetch requests not covered by tracePropagationTargets', async ({
+ baseURL,
+ request,
+}) => {
+ const inboundTransactionPromise = waitForTransaction('nextjs-16-bun', transactionEvent => {
+ return transactionEvent.transaction === 'GET /propagation/test-outgoing-fetch-external-disallowed/check';
+ });
+
+ const outboundTransactionPromise = waitForTransaction('nextjs-16-bun', transactionEvent => {
+ return transactionEvent.transaction === 'GET /propagation/test-outgoing-fetch-external-disallowed';
+ });
+
+ const { headers } = await (
+ await request.get(`${baseURL}/propagation/test-outgoing-fetch-external-disallowed`)
+ ).json();
+
+ expect(headers.baggage).toBeUndefined();
+ expect(headers['sentry-trace']).toBeUndefined();
+
+ const inboundTransaction = await inboundTransactionPromise;
+ const outboundTransaction = await outboundTransactionPromise;
+
+ expect(typeof outboundTransaction.contexts?.trace?.trace_id).toBe('string');
+ expect(inboundTransaction.contexts?.trace?.trace_id).not.toBe(outboundTransaction.contexts?.trace?.trace_id);
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/tests/route-handler.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/tests/route-handler.test.ts
new file mode 100644
index 000000000000..ba967c3f9bf2
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/tests/route-handler.test.ts
@@ -0,0 +1,16 @@
+import test, { expect } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+test('Should create a transaction for node route handlers', async ({ request }) => {
+ const routehandlerTransactionPromise = waitForTransaction('nextjs-16-bun', async transactionEvent => {
+ return transactionEvent?.transaction === 'GET /route-handler/[xoxo]/node';
+ });
+
+ const response = await request.get('/route-handler/123/node', { headers: { 'x-charly': 'gomez' } });
+ expect(await response.json()).toStrictEqual({ message: 'Hello Node Route Handler' });
+
+ const routehandlerTransaction = await routehandlerTransactionPromise;
+
+ expect(routehandlerTransaction.contexts?.trace?.status).toBe('ok');
+ expect(routehandlerTransaction.contexts?.trace?.op).toBe('http.server');
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/tests/server-components.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/tests/server-components.test.ts
new file mode 100644
index 000000000000..75459564436e
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/tests/server-components.test.ts
@@ -0,0 +1,98 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+test('Sends a transaction for a request to app router with URL', async ({ page }) => {
+ const serverComponentTransactionPromise = waitForTransaction('nextjs-16-bun', transactionEvent => {
+ return (
+ transactionEvent?.transaction === 'GET /parameterized/[one]/beep/[two]' &&
+ transactionEvent.contexts?.trace?.data?.['http.target']?.startsWith('/parameterized/1337/beep/42')
+ );
+ });
+
+ await page.goto('/parameterized/1337/beep/42');
+
+ const transactionEvent = await serverComponentTransactionPromise;
+
+ expect(transactionEvent.contexts?.trace).toEqual({
+ data: expect.objectContaining({
+ 'sentry.op': 'http.server',
+ 'sentry.origin': 'auto',
+ 'sentry.sample_rate': 1,
+ 'sentry.source': 'route',
+ 'http.method': 'GET',
+ 'http.response.status_code': 200,
+ 'http.route': '/parameterized/[one]/beep/[two]',
+ 'http.status_code': 200,
+ 'http.target': '/parameterized/1337/beep/42',
+ 'otel.kind': 'SERVER',
+ 'next.route': '/parameterized/[one]/beep/[two]',
+ }),
+ op: 'http.server',
+ origin: 'auto',
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ status: 'ok',
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ });
+
+ expect(transactionEvent.request).toMatchObject({
+ url: expect.stringContaining('/parameterized/1337/beep/42'),
+ });
+
+ // The transaction should not contain any spans with the same name as the transaction
+ // e.g. "GET /parameterized/[one]/beep/[two]"
+ expect(
+ transactionEvent.spans?.filter(span => {
+ return span.description === transactionEvent.transaction;
+ }),
+ ).toHaveLength(0);
+});
+
+test('Will create a transaction with spans for every server component and metadata generation functions when visiting a page', async ({
+ page,
+}) => {
+ const serverTransactionEventPromise = waitForTransaction('nextjs-16-bun', async transactionEvent => {
+ return transactionEvent?.transaction === 'GET /nested-layout';
+ });
+
+ await page.goto('/nested-layout');
+
+ const spanDescriptions = (await serverTransactionEventPromise).spans?.map(span => {
+ return span.description;
+ });
+
+ expect(spanDescriptions).toContainEqual('render route (app) /nested-layout');
+ expect(spanDescriptions).toContainEqual('build component tree');
+ expect(spanDescriptions).toContainEqual('resolve root layout server component');
+ expect(spanDescriptions).toContainEqual('resolve layout server component "(nested-layout)"');
+ expect(spanDescriptions).toContainEqual('resolve layout server component "nested-layout"');
+ expect(spanDescriptions).toContainEqual('resolve page server component "/nested-layout"');
+ expect(spanDescriptions).toContainEqual('generateMetadata /(nested-layout)/nested-layout/page');
+ expect(spanDescriptions).toContainEqual('start response');
+ expect(spanDescriptions).toContainEqual('NextNodeServer.clientComponentLoading');
+});
+
+test('Will create a transaction with spans for every server component and metadata generation functions when visiting a dynamic page', async ({
+ page,
+}) => {
+ const serverTransactionEventPromise = waitForTransaction('nextjs-16-bun', async transactionEvent => {
+ return transactionEvent?.transaction === 'GET /nested-layout/[dynamic]';
+ });
+
+ await page.goto('/nested-layout/123');
+
+ const spanDescriptions = (await serverTransactionEventPromise).spans?.map(span => {
+ return span.description;
+ });
+
+ expect(spanDescriptions).toContainEqual('resolve page components');
+ expect(spanDescriptions).toContainEqual('render route (app) /nested-layout/[dynamic]');
+ expect(spanDescriptions).toContainEqual('build component tree');
+ expect(spanDescriptions).toContainEqual('resolve root layout server component');
+ expect(spanDescriptions).toContainEqual('resolve layout server component "(nested-layout)"');
+ expect(spanDescriptions).toContainEqual('resolve layout server component "nested-layout"');
+ expect(spanDescriptions).toContainEqual('resolve layout server component "[dynamic]"');
+ expect(spanDescriptions).toContainEqual('resolve page server component "/nested-layout/[dynamic]"');
+ expect(spanDescriptions).toContainEqual('generateMetadata /(nested-layout)/nested-layout/[dynamic]/page');
+ expect(spanDescriptions).toContainEqual('start response');
+ expect(spanDescriptions).toContainEqual('NextNodeServer.clientComponentLoading');
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/tests/streaming-rsc-error.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/tests/streaming-rsc-error.test.ts
new file mode 100644
index 000000000000..68d8b1bb8bcf
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/tests/streaming-rsc-error.test.ts
@@ -0,0 +1,38 @@
+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-16-bun', errorEvent => {
+ return !!errorEvent?.exception?.values?.some(value => value.value === 'I am a data streaming error');
+ });
+
+ const serverTransactionPromise = waitForTransaction('nextjs-16-bun', 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',
+ });
+
+ expect(errorEvent.exception?.values?.[0]?.mechanism).toEqual({
+ handled: false,
+ type: 'auto.function.nextjs.on_request_error',
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-bun/tsconfig.json b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/tsconfig.json
new file mode 100644
index 000000000000..cc9ed39b5aa2
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-bun/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "compilerOptions": {
+ "target": "ES2017",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "react-jsx",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./*"]
+ }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts", "**/*.mts"],
+ "exclude": ["node_modules"]
+}