diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/.gitignore b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/.gitignore
new file mode 100644
index 000000000000..dd146b53d966
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/.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-cf-workers/.npmrc b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/.npmrc
new file mode 100644
index 000000000000..070f80f05092
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/.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-16-cf-workers/app/(nested-layout)/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/(nested-layout)/layout.tsx
new file mode 100644
index 000000000000..ace0c2f086b7
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/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-cf-workers/app/(nested-layout)/nested-layout/[dynamic]/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/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-cf-workers/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-cf-workers/app/(nested-layout)/nested-layout/[dynamic]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/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-cf-workers/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-cf-workers/app/(nested-layout)/nested-layout/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/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-cf-workers/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-cf-workers/app/(nested-layout)/nested-layout/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/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-cf-workers/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-cf-workers/app/api/endpoint-behind-middleware/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/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-cf-workers/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-cf-workers/app/favicon.ico b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/favicon.ico
new file mode 100644
index 000000000000..718d6fea4835
Binary files /dev/null and b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/favicon.ico differ
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/global-error.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/global-error.tsx
new file mode 100644
index 000000000000..20c175015b03
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/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-cf-workers/app/isr-test/[product]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/isr-test/[product]/page.tsx
new file mode 100644
index 000000000000..cd1e085e2763
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/isr-test/[product]/page.tsx
@@ -0,0 +1,17 @@
+export const revalidate = 60; // ISR: revalidate every 60 seconds
+export const dynamicParams = true; // Allow dynamic params beyond generateStaticParams
+
+export async function generateStaticParams(): Promise> {
+ return [{ product: 'laptop' }, { product: 'phone' }, { product: 'tablet' }];
+}
+
+export default async function ISRProductPage({ params }: { params: Promise<{ product: string }> }) {
+ const { product } = await params;
+
+ return (
+
+
ISR Product: {product}
+
{product}
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/isr-test/static/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/isr-test/static/page.tsx
new file mode 100644
index 000000000000..f49605bd9da4
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/isr-test/static/page.tsx
@@ -0,0 +1,15 @@
+export const revalidate = 60; // ISR: revalidate every 60 seconds
+export const dynamicParams = true;
+
+export async function generateStaticParams(): Promise {
+ return [];
+}
+
+export default function ISRStaticPage() {
+ return (
+
+
ISR Static Page
+
static-isr
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/layout.tsx
new file mode 100644
index 000000000000..c8f9cee0b787
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/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-cf-workers/app/metrics/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/metrics/page.tsx
new file mode 100644
index 000000000000..fdb7bc0a40a7
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/metrics/page.tsx
@@ -0,0 +1,34 @@
+'use client';
+
+import * as Sentry from '@sentry/nextjs';
+
+export default function Page() {
+ const handleClick = async () => {
+ Sentry.metrics.count('test.page.count', 1, {
+ attributes: {
+ page: '/metrics',
+ 'random.attribute': 'Apples',
+ },
+ });
+ Sentry.metrics.distribution('test.page.distribution', 100, {
+ attributes: {
+ page: '/metrics',
+ 'random.attribute': 'Manzanas',
+ },
+ });
+ Sentry.metrics.gauge('test.page.gauge', 200, {
+ attributes: {
+ page: '/metrics',
+ 'random.attribute': 'Mele',
+ },
+ });
+ await fetch('/metrics/route-handler');
+ };
+
+ return (
+
+
Metrics page
+
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/metrics/route-handler/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/metrics/route-handler/route.ts
new file mode 100644
index 000000000000..84e81960f9c9
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/metrics/route-handler/route.ts
@@ -0,0 +1,23 @@
+import * as Sentry from '@sentry/nextjs';
+
+export const GET = async () => {
+ Sentry.metrics.count('test.route.handler.count', 1, {
+ attributes: {
+ endpoint: '/metrics/route-handler',
+ 'random.attribute': 'Potatoes',
+ },
+ });
+ Sentry.metrics.distribution('test.route.handler.distribution', 100, {
+ attributes: {
+ endpoint: '/metrics/route-handler',
+ 'random.attribute': 'Patatas',
+ },
+ });
+ Sentry.metrics.gauge('test.route.handler.gauge', 200, {
+ attributes: {
+ endpoint: '/metrics/route-handler',
+ 'random.attribute': 'Patate',
+ },
+ });
+ return Response.json({ message: 'Bueno' });
+};
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/nested-rsc-error/[param]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/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-cf-workers/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-cf-workers/app/non-isr-test/[item]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/non-isr-test/[item]/page.tsx
new file mode 100644
index 000000000000..e0bafdb24181
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/non-isr-test/[item]/page.tsx
@@ -0,0 +1,11 @@
+// No generateStaticParams - this is NOT an ISR page
+export default async function NonISRPage({ params }: { params: Promise<{ item: string }> }) {
+ const { item } = await params;
+
+ return (
+
+
Non-ISR Dynamic Page: {item}
+
{item}
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/page.tsx
new file mode 100644
index 000000000000..2bc0a407a355
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/page.tsx
@@ -0,0 +1,3 @@
+export default function Page() {
+ return Next 16 test app
;
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/pageload-tracing/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/pageload-tracing/layout.tsx
new file mode 100644
index 000000000000..1f0cbe478f88
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/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-cf-workers/app/pageload-tracing/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/pageload-tracing/page.tsx
new file mode 100644
index 000000000000..689735d61ddf
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/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-cf-workers/app/parameterized/[one]/beep/[two]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/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-cf-workers/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-cf-workers/app/parameterized/[one]/beep/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/parameterized/[one]/beep/page.tsx
new file mode 100644
index 000000000000..a7d9164c8c03
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/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-cf-workers/app/parameterized/[one]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/parameterized/[one]/page.tsx
new file mode 100644
index 000000000000..9fa617a22381
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/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-cf-workers/app/parameterized/static/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/parameterized/static/page.tsx
new file mode 100644
index 000000000000..16ef0482d53b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/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-cf-workers/app/prefetching/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/prefetching/page.tsx
new file mode 100644
index 000000000000..4cb811ecf1b4
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/prefetching/page.tsx
@@ -0,0 +1,9 @@
+import Link from 'next/link';
+
+export default function Page() {
+ return (
+
+ link
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/prefetching/to-be-prefetched/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/prefetching/to-be-prefetched/page.tsx
new file mode 100644
index 000000000000..83aac90d65cf
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/prefetching/to-be-prefetched/page.tsx
@@ -0,0 +1,5 @@
+export const dynamic = 'force-dynamic';
+
+export default function Page() {
+ return Hello
;
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/redirect/destination/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/redirect/destination/page.tsx
new file mode 100644
index 000000000000..5583d36b04b0
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/redirect/destination/page.tsx
@@ -0,0 +1,7 @@
+export default function RedirectDestinationPage() {
+ return (
+
+
Redirect Destination
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/redirect/origin/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/redirect/origin/page.tsx
new file mode 100644
index 000000000000..52615e0a054b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/redirect/origin/page.tsx
@@ -0,0 +1,18 @@
+import { redirect } from 'next/navigation';
+
+async function redirectAction() {
+ 'use server';
+
+ redirect('/redirect/destination');
+}
+
+export default function RedirectOriginPage() {
+ return (
+ <>
+ {/* @ts-ignore */}
+
+ >
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/app/route-handler/[xoxo]/edge/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/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-cf-workers/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-cf-workers/app/route-handler/[xoxo]/node/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/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-cf-workers/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-cf-workers/app/streaming-rsc-error/[param]/client-page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/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-cf-workers/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-cf-workers/app/streaming-rsc-error/[param]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/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-cf-workers/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-cf-workers/eslint.config.mjs b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/eslint.config.mjs
new file mode 100644
index 000000000000..60f7af38f6c2
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/eslint.config.mjs
@@ -0,0 +1,19 @@
+import { dirname } from 'path';
+import { fileURLToPath } from 'url';
+import { FlatCompat } from '@eslint/eslintrc';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+const compat = new FlatCompat({
+ baseDirectory: __dirname,
+});
+
+const eslintConfig = [
+ ...compat.extends('next/core-web-vitals', 'next/typescript'),
+ {
+ ignores: ['node_modules/**', '.next/**', 'out/**', 'build/**', 'next-env.d.ts'],
+ },
+];
+
+export default eslintConfig;
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/instrumentation-client.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/instrumentation-client.ts
new file mode 100644
index 000000000000..ae4e3195a2a1
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/instrumentation-client.ts
@@ -0,0 +1,16 @@
+import * as Sentry from '@sentry/nextjs';
+import type { Log } 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,
+ // Verify Log type is available
+ beforeSendLog(log: Log) {
+ return log;
+ },
+});
+
+export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/instrumentation.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/instrumentation.ts
new file mode 100644
index 000000000000..964f937c439a
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/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-cf-workers/middleware.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/middleware.ts
new file mode 100644
index 000000000000..f5980e4231c1
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/middleware.ts
@@ -0,0 +1,26 @@
+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 middleware(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'],
+};
+
+export const runtime = 'experimental-edge';
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/next.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/next.config.ts
new file mode 100644
index 000000000000..6699b3dd2c33
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/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-cf-workers/open-next.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/open-next.config.ts
new file mode 100644
index 000000000000..a68b3c089829
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/open-next.config.ts
@@ -0,0 +1,9 @@
+import { defineCloudflareConfig } from '@opennextjs/cloudflare';
+
+export default defineCloudflareConfig({
+ // Uncomment to enable R2 cache,
+ // It should be imported as:
+ // `import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";`
+ // See https://opennext.js.org/cloudflare/caching for more details
+ // incrementalCache: r2IncrementalCache,
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/package.json
new file mode 100644
index 000000000000..c48695371649
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/package.json
@@ -0,0 +1,55 @@
+{
+ "name": "nextjs-16-cf-workers",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "cf:build": "opennextjs-cloudflare build",
+ "cf:preview": "opennextjs-cloudflare preview",
+ "build": "next build",
+ "clean": "npx rimraf node_modules pnpm-lock.yaml .tmp_dev_server_logs",
+ "start": "pnpm cf:preview",
+ "lint": "eslint",
+ "test:prod": "TEST_ENV=production playwright test",
+ "test:build": "pnpm install && pnpm cf:build",
+ "test:build-canary": "pnpm install && pnpm add next@canary && pnpm cf:build",
+ "test:build-latest": "pnpm install && pnpm add next@latest && pnpm cf:build",
+ "test:assert": "pnpm test:prod"
+ },
+ "dependencies": {
+ "@opennextjs/cloudflare": "^1.14.9",
+ "@sentry/nextjs": "latest || *",
+ "@sentry/core": "latest || *",
+ "next": "16.0.10",
+ "react": "19.1.0",
+ "react-dom": "19.1.0"
+ },
+ "devDependencies": {
+ "@playwright/test": "~1.56.0",
+ "@sentry-internal/test-utils": "link:../../../test-utils",
+ "@types/node": "^20",
+ "@types/react": "^19",
+ "@types/react-dom": "^19",
+ "eslint": "^9",
+ "eslint-config-next": "canary",
+ "typescript": "^5",
+ "wrangler": "^4.59.2"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ },
+ "sentryTest": {
+ "variants": [
+ {
+ "build-command": "pnpm test:build-latest",
+ "label": "nextjs-16-cf-workers (latest)"
+ }
+ ],
+ "optionalVariants": [
+ {
+ "build-command": "pnpm test:build-canary",
+ "label": "nextjs-16-cf-workers (canary)"
+ }
+ ]
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/playwright.config.mjs
new file mode 100644
index 000000000000..0f15639161dd
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/playwright.config.mjs
@@ -0,0 +1,21 @@
+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 === 'production') {
+ return 'pnpm cf:preview --port 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-cf-workers/public/file.svg b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/public/file.svg
new file mode 100644
index 000000000000..004145cddf3f
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/public/file.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/public/globe.svg b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/public/globe.svg
new file mode 100644
index 000000000000..567f17b0d7c7
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/public/globe.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/public/next.svg b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/public/next.svg
new file mode 100644
index 000000000000..5174b28c565c
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/public/next.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/public/vercel.svg b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/public/vercel.svg
new file mode 100644
index 000000000000..77053960334e
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/public/vercel.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/public/window.svg b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/public/window.svg
new file mode 100644
index 000000000000..b2b2a44f6ebc
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/public/window.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/sentry.edge.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/sentry.edge.config.ts
new file mode 100644
index 000000000000..2199afc46eaf
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/sentry.edge.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,
+ // debug: true,
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/sentry.server.config.ts
new file mode 100644
index 000000000000..8f0b4d0f7800
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/sentry.server.config.ts
@@ -0,0 +1,16 @@
+import * as Sentry from '@sentry/nextjs';
+import { Log } 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,
+ // debug: true,
+ integrations: [Sentry.vercelAIIntegration()],
+ // Verify Log type is available
+ beforeSendLog(log: Log) {
+ return log;
+ },
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/start-event-proxy.mjs
new file mode 100644
index 000000000000..efb664370443
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/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-cf-workers',
+ envelopeDumpPath: path.join(
+ process.cwd(),
+ `event-dumps/next-16-v${packageJson.dependencies.next}-${process.env.TEST_ENV}.dump`,
+ ),
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/async-params.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/async-params.test.ts
new file mode 100644
index 000000000000..e8160d12aded
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/async-params.test.ts
@@ -0,0 +1,14 @@
+import { expect, test } from '@playwright/test';
+import fs from 'fs';
+import { isDevMode } from './isDevMode';
+
+test('should not print warning for async params', async ({ page }) => {
+ test.skip(!isDevMode, 'should be skipped for non-dev mode');
+ await page.goto('/');
+
+ // If the server exits with code 1, the test will fail (see instrumentation.ts)
+ const devStdout = fs.readFileSync('.tmp_dev_server_logs', 'utf-8');
+ expect(devStdout).not.toContain('`params` should be awaited before using its properties.');
+
+ await expect(page.getByText('Next 16 test app')).toBeVisible();
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/isDevMode.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/isDevMode.ts
new file mode 100644
index 000000000000..d2be94232110
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/isDevMode.ts
@@ -0,0 +1 @@
+export const isDevMode = !!process.env.TEST_ENV && process.env.TEST_ENV.includes('development');
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/isr-routes.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/isr-routes.test.ts
new file mode 100644
index 000000000000..b42d2cd61b93
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/isr-routes.test.ts
@@ -0,0 +1,94 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+test('should remove sentry-trace and baggage meta tags on ISR dynamic route page load', async ({ page }) => {
+ // Navigate to ISR page
+ await page.goto('/isr-test/laptop');
+
+ // Wait for page to be fully loaded
+ await expect(page.locator('#isr-product-id')).toHaveText('laptop');
+
+ // Check that sentry-trace and baggage meta tags are removed for ISR pages
+ await expect(page.locator('meta[name="sentry-trace"]')).toHaveCount(0);
+ await expect(page.locator('meta[name="baggage"]')).toHaveCount(0);
+});
+
+test('should remove sentry-trace and baggage meta tags on ISR static route', async ({ page }) => {
+ // Navigate to ISR static page
+ await page.goto('/isr-test/static');
+
+ // Wait for page to be fully loaded
+ await expect(page.locator('#isr-static-marker')).toHaveText('static-isr');
+
+ // Check that sentry-trace and baggage meta tags are removed for ISR pages
+ await expect(page.locator('meta[name="sentry-trace"]')).toHaveCount(0);
+ await expect(page.locator('meta[name="baggage"]')).toHaveCount(0);
+});
+
+test('should remove meta tags for different ISR dynamic route values', async ({ page }) => {
+ // Test with 'phone' (one of the pre-generated static params)
+ await page.goto('/isr-test/phone');
+ await expect(page.locator('#isr-product-id')).toHaveText('phone');
+
+ await expect(page.locator('meta[name="sentry-trace"]')).toHaveCount(0);
+ await expect(page.locator('meta[name="baggage"]')).toHaveCount(0);
+
+ // Test with 'tablet'
+ await page.goto('/isr-test/tablet');
+ await expect(page.locator('#isr-product-id')).toHaveText('tablet');
+
+ await expect(page.locator('meta[name="sentry-trace"]')).toHaveCount(0);
+ await expect(page.locator('meta[name="baggage"]')).toHaveCount(0);
+});
+
+test('should create unique transactions for ISR pages on each visit', async ({ page }) => {
+ const traceIds: string[] = [];
+
+ // Load the same ISR page 5 times to ensure cached HTML meta tags are consistently removed
+ for (let i = 0; i < 5; i++) {
+ const transactionPromise = waitForTransaction('nextjs-16-cf-workers', async transactionEvent => {
+ return !!(
+ transactionEvent.transaction === '/isr-test/:product' && transactionEvent.contexts?.trace?.op === 'pageload'
+ );
+ });
+
+ if (i === 0) {
+ await page.goto('/isr-test/laptop');
+ } else {
+ await page.reload();
+ }
+
+ const transaction = await transactionPromise;
+ const traceId = transaction.contexts?.trace?.trace_id;
+
+ expect(traceId).toBeDefined();
+ expect(traceId).toMatch(/[a-f0-9]{32}/);
+ traceIds.push(traceId!);
+ }
+
+ // Verify all 5 page loads have unique trace IDs (no reuse of cached/stale meta tags)
+ const uniqueTraceIds = new Set(traceIds);
+ expect(uniqueTraceIds.size).toBe(5);
+});
+
+test('ISR route should be identified correctly in the route manifest', async ({ page }) => {
+ const transactionPromise = waitForTransaction('nextjs-16-cf-workers', async transactionEvent => {
+ return transactionEvent.transaction === '/isr-test/:product' && transactionEvent.contexts?.trace?.op === 'pageload';
+ });
+
+ await page.goto('/isr-test/laptop');
+ const transaction = await transactionPromise;
+
+ // Verify the transaction is properly parameterized
+ expect(transaction).toMatchObject({
+ transaction: '/isr-test/:product',
+ transaction_info: { source: 'route' },
+ contexts: {
+ trace: {
+ data: {
+ 'sentry.source': 'route',
+ },
+ },
+ },
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/metrics.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/metrics.test.ts
new file mode 100644
index 000000000000..6569c3d21890
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/metrics.test.ts
@@ -0,0 +1,135 @@
+import { expect, test } from '@playwright/test';
+import { waitForMetric } from '@sentry-internal/test-utils';
+
+// Metrics are not currently supported on Cloudflare Workers
+// TODO: Investigate and enable when metrics support is added for CF Workers
+test.skip('Should emit metrics from server and client', async ({ request, page }) => {
+ const clientCountPromise = waitForMetric('nextjs-16-cf-workers', async metric => {
+ return metric.name === 'test.page.count';
+ });
+
+ const clientDistributionPromise = waitForMetric('nextjs-16-cf-workers', async metric => {
+ return metric.name === 'test.page.distribution';
+ });
+
+ const clientGaugePromise = waitForMetric('nextjs-16-cf-workers', async metric => {
+ return metric.name === 'test.page.gauge';
+ });
+
+ const serverCountPromise = waitForMetric('nextjs-16-cf-workers', async metric => {
+ return metric.name === 'test.route.handler.count';
+ });
+
+ const serverDistributionPromise = waitForMetric('nextjs-16-cf-workers', async metric => {
+ return metric.name === 'test.route.handler.distribution';
+ });
+
+ const serverGaugePromise = waitForMetric('nextjs-16-cf-workers', async metric => {
+ return metric.name === 'test.route.handler.gauge';
+ });
+
+ await page.goto('/metrics');
+ await page.getByText('Emit').click();
+ const clientCount = await clientCountPromise;
+ const clientDistribution = await clientDistributionPromise;
+ const clientGauge = await clientGaugePromise;
+ const serverCount = await serverCountPromise;
+ const serverDistribution = await serverDistributionPromise;
+ const serverGauge = await serverGaugePromise;
+
+ expect(clientCount).toMatchObject({
+ timestamp: expect.any(Number),
+ trace_id: expect.any(String),
+ span_id: expect.any(String),
+ name: 'test.page.count',
+ type: 'counter',
+ value: 1,
+ attributes: {
+ page: { value: '/metrics', type: 'string' },
+ 'random.attribute': { value: 'Apples', type: 'string' },
+ 'sentry.environment': { value: 'qa', type: 'string' },
+ 'sentry.sdk.name': { value: 'sentry.javascript.nextjs', type: 'string' },
+ 'sentry.sdk.version': { value: expect.any(String), type: 'string' },
+ },
+ });
+
+ expect(clientDistribution).toMatchObject({
+ timestamp: expect.any(Number),
+ trace_id: expect.any(String),
+ span_id: expect.any(String),
+ name: 'test.page.distribution',
+ type: 'distribution',
+ value: 100,
+ attributes: {
+ page: { value: '/metrics', type: 'string' },
+ 'random.attribute': { value: 'Manzanas', type: 'string' },
+ 'sentry.environment': { value: 'qa', type: 'string' },
+ 'sentry.sdk.name': { value: 'sentry.javascript.nextjs', type: 'string' },
+ 'sentry.sdk.version': { value: expect.any(String), type: 'string' },
+ },
+ });
+
+ expect(clientGauge).toMatchObject({
+ timestamp: expect.any(Number),
+ trace_id: expect.any(String),
+ span_id: expect.any(String),
+ name: 'test.page.gauge',
+ type: 'gauge',
+ value: 200,
+ attributes: {
+ page: { value: '/metrics', type: 'string' },
+ 'random.attribute': { value: 'Mele', type: 'string' },
+ 'sentry.environment': { value: 'qa', type: 'string' },
+ 'sentry.sdk.name': { value: 'sentry.javascript.nextjs', type: 'string' },
+ 'sentry.sdk.version': { value: expect.any(String), type: 'string' },
+ },
+ });
+
+ expect(serverCount).toMatchObject({
+ timestamp: expect.any(Number),
+ trace_id: expect.any(String),
+ name: 'test.route.handler.count',
+ type: 'counter',
+ value: 1,
+ attributes: {
+ 'server.address': { value: expect.any(String), type: 'string' },
+ 'random.attribute': { value: 'Potatoes', type: 'string' },
+ endpoint: { value: '/metrics/route-handler', type: 'string' },
+ 'sentry.environment': { value: 'qa', type: 'string' },
+ 'sentry.sdk.name': { value: 'sentry.javascript.nextjs', type: 'string' },
+ 'sentry.sdk.version': { value: expect.any(String), type: 'string' },
+ },
+ });
+
+ expect(serverDistribution).toMatchObject({
+ timestamp: expect.any(Number),
+ trace_id: expect.any(String),
+ name: 'test.route.handler.distribution',
+ type: 'distribution',
+ value: 100,
+ attributes: {
+ 'server.address': { value: expect.any(String), type: 'string' },
+ 'random.attribute': { value: 'Patatas', type: 'string' },
+ endpoint: { value: '/metrics/route-handler', type: 'string' },
+ 'sentry.environment': { value: 'qa', type: 'string' },
+ 'sentry.sdk.name': { value: 'sentry.javascript.nextjs', type: 'string' },
+ 'sentry.sdk.version': { value: expect.any(String), type: 'string' },
+ },
+ });
+
+ expect(serverGauge).toMatchObject({
+ timestamp: expect.any(Number),
+ trace_id: expect.any(String),
+ name: 'test.route.handler.gauge',
+ type: 'gauge',
+ value: 200,
+ attributes: {
+ 'server.address': { value: expect.any(String), type: 'string' },
+ 'random.attribute': { value: 'Patate', type: 'string' },
+ endpoint: { value: '/metrics/route-handler', type: 'string' },
+ 'sentry.environment': { value: 'qa', type: 'string' },
+ 'sentry.sdk.name': { value: 'sentry.javascript.nextjs', type: 'string' },
+ 'sentry.sdk.version': { value: expect.any(String), type: 'string' },
+ },
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/middleware.test.ts
new file mode 100644
index 000000000000..f769874a3d34
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/middleware.test.ts
@@ -0,0 +1,77 @@
+import { expect, test } from '@playwright/test';
+import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
+import { isDevMode } from './isDevMode';
+
+// TODO: Middleware tests need SDK adjustments for Cloudflare Workers edge runtime
+test.skip('Should create a transaction for middleware', async ({ request }) => {
+ const middlewareTransactionPromise = waitForTransaction('nextjs-16-cf-workers', 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('vercel-edge');
+ expect(middlewareTransaction.transaction_info?.source).toBe('route');
+
+ // Assert that isolation scope works properly
+ expect(middlewareTransaction.tags?.['my-isolated-tag']).toBe(true);
+ // TODO: Isolation scope is not working properly yet
+ // expect(middlewareTransaction.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
+});
+
+// TODO: Middleware tests need SDK adjustments for Cloudflare Workers edge runtime
+test.skip('Faulty middlewares', async ({ request }) => {
+ test.skip(isDevMode, 'Throwing crashes the dev server atm'); // https://github.com/vercel/next.js/issues/85261
+ const middlewareTransactionPromise = waitForTransaction('nextjs-16-cf-workers', async transactionEvent => {
+ return transactionEvent?.transaction === 'middleware GET';
+ });
+
+ const errorEventPromise = waitForError('nextjs-16-cf-workers', errorEvent => {
+ return errorEvent?.exception?.values?.[0]?.value === 'Middleware Error';
+ });
+
+ request.get('/api/endpoint-behind-middleware', { headers: { 'x-should-throw': '1' } }).catch(() => {
+ // Noop
+ });
+
+ await test.step('should record transactions', async () => {
+ const middlewareTransaction = await middlewareTransactionPromise;
+ expect(middlewareTransaction.contexts?.trace?.status).toBe('internal_error');
+ expect(middlewareTransaction.contexts?.trace?.op).toBe('http.server.middleware');
+ expect(middlewareTransaction.contexts?.runtime?.name).toBe('vercel-edge');
+ expect(middlewareTransaction.transaction_info?.source).toBe('route');
+ });
+});
+
+// TODO: Middleware tests need SDK adjustments for Cloudflare Workers edge runtime
+test.skip('Should trace outgoing fetch requests inside middleware and create breadcrumbs for it', async ({
+ request,
+}) => {
+ test.skip(isDevMode, 'The fetch requests ends up in a separate tx in dev atm');
+ const middlewareTransactionPromise = waitForTransaction('nextjs-16-cf-workers', async transactionEvent => {
+ return transactionEvent?.transaction === 'middleware GET';
+ });
+
+ request.get('/api/endpoint-behind-middleware', { headers: { 'x-should-make-request': '1' } }).catch(() => {
+ // Noop
+ });
+
+ const middlewareTransaction = await middlewareTransactionPromise;
+
+ // Breadcrumbs should always be created for the fetch request
+ expect(middlewareTransaction.breadcrumbs).toEqual(
+ expect.arrayContaining([
+ {
+ category: 'http',
+ data: { 'http.method': 'GET', status_code: 200, url: 'http://localhost:3030/' },
+ timestamp: expect.any(Number),
+ type: 'http',
+ },
+ ]),
+ );
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/nested-rsc-error.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/nested-rsc-error.test.ts
new file mode 100644
index 000000000000..9c9de3b350a8
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/nested-rsc-error.test.ts
@@ -0,0 +1,39 @@
+import { expect, test } from '@playwright/test';
+import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
+
+// TODO: Flakey on CI
+test.skip('Should capture errors from nested server components when `Sentry.captureRequestError` is added to the `onRequestError` hook', async ({
+ page,
+}) => {
+ const errorEventPromise = waitForError('nextjs-16-cf-workers', errorEvent => {
+ return !!errorEvent?.exception?.values?.some(value => value.value === 'I am technically uncatchable');
+ });
+
+ const serverTransactionPromise = waitForTransaction('nextjs-16-cf-workers', 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-cf-workers/tests/pageload-tracing.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/pageload-tracing.test.ts
new file mode 100644
index 000000000000..55f78630ef2d
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/pageload-tracing.test.ts
@@ -0,0 +1,56 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+// TODO: Flakey on CI
+test.skip('App router transactions should be attached to the pageload request span', async ({ page }) => {
+ const serverTransactionPromise = waitForTransaction('nextjs-16-cf-workers', async transactionEvent => {
+ return transactionEvent?.transaction === 'GET /pageload-tracing';
+ });
+
+ const pageloadTransactionPromise = waitForTransaction('nextjs-16-cf-workers', 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);
+});
+
+// TODO: HTTP request headers are not extracted as span attributes on Cloudflare Workers
+test.skip('extracts HTTP request headers as span attributes', async ({ baseURL }) => {
+ const serverTransactionPromise = waitForTransaction('nextjs-16-cf-workers', async transactionEvent => {
+ return transactionEvent?.transaction === 'GET /pageload-tracing';
+ });
+
+ await fetch(`${baseURL}/pageload-tracing`, {
+ headers: {
+ 'User-Agent': 'Custom-NextJS-Agent/15.0',
+ 'Content-Type': 'text/html',
+ 'X-NextJS-Test': 'nextjs-header-value',
+ Accept: 'text/html, application/xhtml+xml',
+ 'X-Framework': 'Next.js',
+ 'X-Request-ID': 'nextjs-789',
+ },
+ });
+
+ const serverTransaction = await serverTransactionPromise;
+
+ expect(serverTransaction.contexts?.trace?.data).toEqual(
+ expect.objectContaining({
+ 'http.request.header.user_agent': 'Custom-NextJS-Agent/15.0',
+ 'http.request.header.content_type': 'text/html',
+ 'http.request.header.x_nextjs_test': 'nextjs-header-value',
+ 'http.request.header.accept': 'text/html, application/xhtml+xml',
+ 'http.request.header.x_framework': 'Next.js',
+ 'http.request.header.x_request_id': 'nextjs-789',
+ }),
+ );
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/parameterized-routes.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/parameterized-routes.test.ts
new file mode 100644
index 000000000000..5d2925375688
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/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-cf-workers', 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-cf-workers', 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-cf-workers', 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-cf-workers', 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-cf-workers/tests/prefetch-spans.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/prefetch-spans.test.ts
new file mode 100644
index 000000000000..f48158a54697
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/prefetch-spans.test.ts
@@ -0,0 +1,25 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+import { isDevMode } from './isDevMode';
+
+test('Prefetch client spans should have a http.request.prefetch attribute', async ({ page }) => {
+ test.skip(isDevMode, "Prefetch requests don't have the prefetch header in dev mode");
+
+ const pageloadTransactionPromise = waitForTransaction('nextjs-16-cf-workers', async transactionEvent => {
+ return transactionEvent?.transaction === '/prefetching';
+ });
+
+ await page.goto(`/prefetching`);
+
+ // Make it more likely that nextjs prefetches
+ await page.hover('#prefetch-link');
+
+ expect((await pageloadTransactionPromise).spans).toContainEqual(
+ expect.objectContaining({
+ op: 'http.client',
+ data: expect.objectContaining({
+ 'http.request.prefetch': true,
+ }),
+ }),
+ );
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/route-handler.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/route-handler.test.ts
new file mode 100644
index 000000000000..16368e5be57b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/route-handler.test.ts
@@ -0,0 +1,37 @@
+import test, { expect } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+test.skip('Should create a transaction for node route handlers', async ({ request }) => {
+ const routehandlerTransactionPromise = waitForTransaction('nextjs-16-cf-workers', 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');
+
+ // Custom headers are not captured on Cloudflare Workers
+ // This assertion is skipped for CF Workers environment
+});
+
+test('Should create a transaction for edge route handlers', async ({ request }) => {
+ // This test only works for webpack builds on non-async param extraction
+ // todo: check if we can set request headers for edge on sdkProcessingMetadata
+ test.skip();
+ const routehandlerTransactionPromise = waitForTransaction('nextjs-16-cf-workers', async transactionEvent => {
+ return transactionEvent?.transaction === 'GET /route-handler/[xoxo]/edge';
+ });
+
+ const response = await request.get('/route-handler/123/edge', { headers: { 'x-charly': 'gomez' } });
+ expect(await response.json()).toStrictEqual({ message: 'Hello Edge Route Handler' });
+
+ const routehandlerTransaction = await routehandlerTransactionPromise;
+
+ expect(routehandlerTransaction.contexts?.trace?.status).toBe('ok');
+ expect(routehandlerTransaction.contexts?.trace?.op).toBe('http.server');
+ expect(routehandlerTransaction.contexts?.trace?.data?.['http.request.header.x_charly']).toBe('gomez');
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/server-action-redirect.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/server-action-redirect.test.ts
new file mode 100644
index 000000000000..09ae79cc60a7
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/server-action-redirect.test.ts
@@ -0,0 +1,47 @@
+import { expect, test } from '@playwright/test';
+import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
+
+test.skip('Should handle server action redirect without capturing errors', async ({ page }) => {
+ // Wait for the initial page load transaction
+ const pageLoadTransactionPromise = waitForTransaction('nextjs-16-cf-workers', async transactionEvent => {
+ return transactionEvent?.transaction === '/redirect/origin';
+ });
+
+ // Navigate to the origin page
+ await page.goto('/redirect/origin');
+
+ const pageLoadTransaction = await pageLoadTransactionPromise;
+ expect(pageLoadTransaction).toBeDefined();
+
+ // Wait for the redirect transaction
+ const redirectTransactionPromise = waitForTransaction('nextjs-16-cf-workers', async transactionEvent => {
+ return transactionEvent?.transaction === 'GET /redirect/destination';
+ });
+
+ // No error should be captured
+ const redirectErrorPromise = waitForError('nextjs-16-cf-workers', async errorEvent => {
+ return !!errorEvent;
+ });
+
+ // Click the redirect button
+ await page.click('button[type="submit"]');
+
+ await redirectTransactionPromise;
+
+ // Verify we got redirected to the destination page
+ await expect(page).toHaveURL('/redirect/destination');
+
+ // Wait for potential errors with a 2 second timeout
+ const errorTimeout = new Promise((_, reject) =>
+ setTimeout(() => reject(new Error('No error captured (timeout)')), 2000),
+ );
+
+ // We expect this to timeout since no error should be captured during the redirect
+ try {
+ await Promise.race([redirectErrorPromise, errorTimeout]);
+ throw new Error('Expected no error to be captured, but an error was found');
+ } catch (e) {
+ // If we get a timeout error (as expected), no error was captured
+ expect((e as Error).message).toBe('No error captured (timeout)');
+ }
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/server-components.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/server-components.test.ts
new file mode 100644
index 000000000000..f5c9f0fb6f96
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/server-components.test.ts
@@ -0,0 +1,101 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+// TODO: Server component tests need SDK adjustments for Cloudflare Workers
+test.skip('Sends a transaction for a request to app router with URL', async ({ page }) => {
+ const serverComponentTransactionPromise = waitForTransaction('nextjs-16-cf-workers', 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);
+});
+
+// TODO: Server component span tests need SDK adjustments for Cloudflare Workers
+test.skip('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-cf-workers', 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');
+});
+
+// TODO: Server component span tests need SDK adjustments for Cloudflare Workers
+test.skip('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-cf-workers', 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-cf-workers/tests/streaming-rsc-error.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tests/streaming-rsc-error.test.ts
new file mode 100644
index 000000000000..ba42d9fadbb9
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/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-cf-workers', errorEvent => {
+ return !!errorEvent?.exception?.values?.some(value => value.value === 'I am a data streaming error');
+ });
+
+ const serverTransactionPromise = waitForTransaction('nextjs-16-cf-workers', 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-cf-workers/tsconfig.json b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/tsconfig.json
new file mode 100644
index 000000000000..cc9ed39b5aa2
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/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"]
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/wrangler.jsonc b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/wrangler.jsonc
new file mode 100644
index 000000000000..062a8e7881e3
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cf-workers/wrangler.jsonc
@@ -0,0 +1,68 @@
+/**
+ * For more details on how to configure Wrangler, refer to:
+ * https://developers.cloudflare.com/workers/wrangler/configuration/
+ */
+/**
+ * For more details on how to configure Wrangler, refer to:
+ * https://developers.cloudflare.com/workers/wrangler/configuration/
+ */
+{
+ "$schema": "node_modules/wrangler/config-schema.json",
+ "name": "next-cf",
+ "main": ".open-next/worker.js",
+ "compatibility_date": "2025-12-01",
+ "compatibility_flags": [
+ "nodejs_compat",
+ "global_fetch_strictly_public"
+ ],
+ "assets": {
+ "binding": "ASSETS",
+ "directory": ".open-next/assets"
+ },
+ "images": {
+ // Enable image optimization
+ // see https://opennext.js.org/cloudflare/howtos/image
+ "binding": "IMAGES"
+ },
+ "services": [
+ {
+ // Self-reference service binding, the service name must match the worker name
+ // see https://opennext.js.org/cloudflare/caching
+ "binding": "WORKER_SELF_REFERENCE",
+ "service": "next-cf"
+ }
+ ],
+ "observability": {
+ "enabled": true
+ }
+ /**
+ * Smart Placement
+ * Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement
+ */
+ // "placement": { "mode": "smart" }
+ /**
+ * Bindings
+ * Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including
+ * databases, object storage, AI inference, real-time communication and more.
+ * https://developers.cloudflare.com/workers/runtime-apis/bindings/
+ */
+ /**
+ * Environment Variables
+ * https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables
+ */
+ // "vars": { "MY_VARIABLE": "production_value" }
+ /**
+ * Note: Use secrets to store sensitive data.
+ * https://developers.cloudflare.com/workers/configuration/secrets/
+ */
+ /**
+ * Static Assets
+ * https://developers.cloudflare.com/workers/static-assets/binding/
+ */
+ // "assets": { "directory": "./public/", "binding": "ASSETS" }
+ /**
+ * Service Bindings (communicate between multiple Workers)
+ * https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings
+ */
+ // "services": [{ "binding": "MY_SERVICE", "service": "my-service" }]
+}
\ No newline at end of file