diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/pageload-tracing/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/pageload-tracing/page.tsx
new file mode 100644
index 000000000000..d0a7c77e38c8
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/app/pageload-tracing/page.tsx
@@ -0,0 +1,37 @@
+import { Suspense } from 'react';
+import { headers } from 'next/headers';
+import * as Sentry from '@sentry/nextjs';
+
+async function CachedContent() {
+ const getTodos = async () => {
+ return Sentry.startSpan({ name: 'getTodos', op: 'get.todos' }, async () => {
+ 'use cache';
+ await new Promise(resolve => setTimeout(resolve, 100));
+ return [1, 2, 3, 4, 5];
+ });
+ };
+
+ const todos = await getTodos();
+
+ return
Todos fetched: {todos.length}
;
+}
+
+async function DynamicContent() {
+ await headers();
+ return (
+ <>
+
+ >
+ );
+}
+
+export default function Page() {
+ return (
+ <>
+ Cache Pageload Tracing
+ Loading...}>
+
+
+ >
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheComponents.spec.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheComponents.spec.ts
index 9a60ac59cd8f..19cb544bd941 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheComponents.spec.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheComponents.spec.ts
@@ -52,3 +52,33 @@ test('Should generate metadata async', async ({ page }) => {
await expect(page.locator('#todos-fetched')).toHaveText('Todos fetched: 5');
await expect(page).toHaveTitle('Product: 1');
});
+
+test('Metatag injection produces exactly one pageload trace with cache components', async ({ page }) => {
+ const serverTxPromise = waitForTransaction('nextjs-16-cacheComponents', async transactionEvent => {
+ return (
+ transactionEvent.contexts?.trace?.op === 'http.server' &&
+ transactionEvent.transaction === 'GET /pageload-tracing'
+ );
+ });
+
+ const pageloadTxPromise = waitForTransaction('nextjs-16-cacheComponents', async transactionEvent => {
+ return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.transaction === '/pageload-tracing';
+ });
+
+ await page.goto('/pageload-tracing');
+
+ await expect(page.locator('#todos-fetched')).toHaveText('Todos fetched: 5');
+
+ const [serverTx, pageloadTx] = await Promise.all([serverTxPromise, pageloadTxPromise]);
+
+ const serverTraceId = serverTx.contexts?.trace?.trace_id;
+ const pageloadTraceId = pageloadTx.contexts?.trace?.trace_id;
+
+ // Server and client pageload must share the same trace via metatag injection
+ expect(pageloadTraceId).toBeTruthy();
+ expect(serverTraceId).toBe(pageloadTraceId);
+
+ // Exactly one set of trace meta tags — no duplicates that would cause multiple pageloads
+ expect(await page.locator('meta[name="sentry-trace"]').count()).toBe(1);
+ expect(await page.locator('meta[name="baggage"]').count()).toBe(1);
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/.gitignore b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/.gitignore
new file mode 100644
index 000000000000..dd146b53d966
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/.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-streaming-cacheComponents/app/cache/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/cache/page.tsx
new file mode 100644
index 000000000000..6cf490b75430
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/cache/page.tsx
@@ -0,0 +1,25 @@
+import { Suspense } from 'react';
+import * as Sentry from '@sentry/nextjs';
+
+export default function Page() {
+ return (
+ <>
+ This will be pre-rendered
+
+ >
+ );
+}
+
+async function DynamicContent() {
+ const getTodos = async () => {
+ return Sentry.startSpan({ name: 'getTodos', op: 'get.todos' }, async () => {
+ 'use cache';
+ await new Promise(resolve => setTimeout(resolve, 100));
+ return [1, 2, 3, 4, 5];
+ });
+ };
+
+ const todos = await getTodos();
+
+ return Todos fetched: {todos.length}
;
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/favicon.ico b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/favicon.ico
new file mode 100644
index 000000000000..718d6fea4835
Binary files /dev/null and b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/favicon.ico differ
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/global-error.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/global-error.tsx
new file mode 100644
index 000000000000..20c175015b03
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/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-streaming-cacheComponents/app/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/layout.tsx
new file mode 100644
index 000000000000..c8f9cee0b787
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/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-streaming-cacheComponents/app/metadata-async/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/metadata-async/page.tsx
new file mode 100644
index 000000000000..03201cdccf60
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/metadata-async/page.tsx
@@ -0,0 +1,37 @@
+import * as Sentry from '@sentry/nextjs';
+
+function fetchPost() {
+ return Promise.resolve({ id: '1', title: 'Post 1' });
+}
+
+export async function generateMetadata() {
+ const { id } = await fetchPost();
+ const product = `Product: ${id}`;
+
+ return {
+ title: product,
+ };
+}
+
+export default function Page() {
+ return (
+ <>
+ This will be pre-rendered
+
+ >
+ );
+}
+
+async function DynamicContent() {
+ const getTodos = async () => {
+ return Sentry.startSpan({ name: 'getTodos', op: 'get.todos' }, async () => {
+ 'use cache';
+ await new Promise(resolve => setTimeout(resolve, 100));
+ return [1, 2, 3, 4, 5];
+ });
+ };
+
+ const todos = await getTodos();
+
+ return Todos fetched: {todos.length}
;
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/metadata/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/metadata/page.tsx
new file mode 100644
index 000000000000..7bcdbd0474e0
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/metadata/page.tsx
@@ -0,0 +1,35 @@
+import * as Sentry from '@sentry/nextjs';
+
+/**
+ * Tests generateMetadata function with cache components, this calls the propagation context to be set
+ * Which will generate and set a trace id in the propagation context, which should trigger the random API error if unpatched
+ * See: https://github.com/getsentry/sentry-javascript/issues/18392
+ */
+export function generateMetadata() {
+ return {
+ title: 'Cache Components Metadata Test',
+ };
+}
+
+export default function Page() {
+ return (
+ <>
+ This will be pre-rendered
+
+ >
+ );
+}
+
+async function DynamicContent() {
+ const getTodos = async () => {
+ return Sentry.startSpan({ name: 'getTodos', op: 'get.todos' }, async () => {
+ 'use cache';
+ await new Promise(resolve => setTimeout(resolve, 100));
+ return [1, 2, 3, 4, 5];
+ });
+ };
+
+ const todos = await getTodos();
+
+ return Todos fetched: {todos.length}
;
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/page.tsx
new file mode 100644
index 000000000000..433db8283beb
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/page.tsx
@@ -0,0 +1,3 @@
+export default function Page() {
+ return Next 16 CacheComponents test app
;
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/pageload-tracing/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/pageload-tracing/page.tsx
new file mode 100644
index 000000000000..d0a7c77e38c8
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/pageload-tracing/page.tsx
@@ -0,0 +1,37 @@
+import { Suspense } from 'react';
+import { headers } from 'next/headers';
+import * as Sentry from '@sentry/nextjs';
+
+async function CachedContent() {
+ const getTodos = async () => {
+ return Sentry.startSpan({ name: 'getTodos', op: 'get.todos' }, async () => {
+ 'use cache';
+ await new Promise(resolve => setTimeout(resolve, 100));
+ return [1, 2, 3, 4, 5];
+ });
+ };
+
+ const todos = await getTodos();
+
+ return Todos fetched: {todos.length}
;
+}
+
+async function DynamicContent() {
+ await headers();
+ return (
+ <>
+
+ >
+ );
+}
+
+export default function Page() {
+ return (
+ <>
+ Cache Pageload Tracing
+ Loading...}>
+
+
+ >
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/suspense/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/suspense/page.tsx
new file mode 100644
index 000000000000..32e5a73afc14
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/app/suspense/page.tsx
@@ -0,0 +1,26 @@
+import { Suspense } from 'react';
+import * as Sentry from '@sentry/nextjs';
+
+export default function Page() {
+ return (
+ <>
+ This will be pre-rendered
+ Loading...}>
+
+
+ >
+ );
+}
+
+async function DynamicContent() {
+ const getTodos = async () => {
+ return Sentry.startSpan({ name: 'getTodos', op: 'get.todos' }, async () => {
+ await new Promise(resolve => setTimeout(resolve, 100));
+ return [1, 2, 3, 4, 5];
+ });
+ };
+
+ const todos = await getTodos();
+
+ return Todos fetched: {todos.length}
;
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/eslint.config.mjs b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/eslint.config.mjs
new file mode 100644
index 000000000000..60f7af38f6c2
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/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-streaming-cacheComponents/instrumentation-client.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/instrumentation-client.ts
new file mode 100644
index 000000000000..e2009fc21abb
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/instrumentation-client.ts
@@ -0,0 +1,12 @@
+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,
+ integrations: [Sentry.spanStreamingIntegration()],
+});
+
+export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/instrumentation.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/instrumentation.ts
new file mode 100644
index 000000000000..964f937c439a
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/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-streaming-cacheComponents/next.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/next.config.ts
new file mode 100644
index 000000000000..2841f1c0c5da
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/next.config.ts
@@ -0,0 +1,10 @@
+import { withSentryConfig } from '@sentry/nextjs';
+import type { NextConfig } from 'next';
+
+const nextConfig: NextConfig = {
+ cacheComponents: true,
+};
+
+export default withSentryConfig(nextConfig, {
+ silent: true,
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/package.json
new file mode 100644
index 000000000000..a9111dd07271
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/package.json
@@ -0,0 +1,51 @@
+{
+ "name": "nextjs-16-streaming-cacheComponents",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "clean": "npx rimraf node_modules pnpm-lock.yaml .tmp_dev_server_logs",
+ "dev:webpack": "next dev --webpack",
+ "build-webpack": "next build --webpack",
+ "start": "next start",
+ "lint": "eslint",
+ "test:prod": "TEST_ENV=production playwright test",
+ "test:dev": "TEST_ENV=development playwright test",
+ "test:dev-webpack": "TEST_ENV=development-webpack playwright test",
+ "test:build": "pnpm install && pnpm build",
+ "test:build-webpack": "pnpm install && pnpm build-webpack",
+ "test:build-canary": "pnpm install && pnpm add next@canary && pnpm build",
+ "test:build-latest": "pnpm install && pnpm add next@latest && pnpm build",
+ "test:build-latest-webpack": "pnpm install && pnpm add next@latest && pnpm build-webpack",
+ "test:build-canary-webpack": "pnpm install && pnpm add next@canary && pnpm build-webpack",
+ "test:assert": "pnpm test:prod && pnpm test:dev",
+ "test:assert-webpack": "pnpm test:prod && pnpm test:dev-webpack"
+ },
+ "dependencies": {
+ "@sentry/nextjs": "file:../../packed/sentry-nextjs-packed.tgz",
+ "@sentry/core": "file:../../packed/sentry-core-packed.tgz",
+ "import-in-the-middle": "^1",
+ "next": "16.2.3",
+ "react": "19.1.0",
+ "react-dom": "19.1.0",
+ "require-in-the-middle": "^7",
+ "zod": "^3.22.4"
+ },
+ "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": "^16",
+ "typescript": "^5"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ },
+ "sentryTest": {
+ "//": "TODO: Add variants for webpack once supported"
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/playwright.config.mjs
new file mode 100644
index 000000000000..797418b8cf7d
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/playwright.config.mjs
@@ -0,0 +1,29 @@
+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-webpack') {
+ return 'pnpm next dev -p 3030 --webpack 2>&1 | tee .tmp_dev_server_logs';
+ }
+
+ if (testEnv === 'development') {
+ return 'pnpm next dev -p 3030 2>&1 | tee .tmp_dev_server_logs';
+ }
+
+ if (testEnv === 'production') {
+ return 'pnpm 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-streaming-cacheComponents/proxy.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/proxy.ts
new file mode 100644
index 000000000000..60722f329fa0
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/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-streaming-cacheComponents/public/file.svg b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/public/file.svg
new file mode 100644
index 000000000000..004145cddf3f
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/public/file.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/public/globe.svg b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/public/globe.svg
new file mode 100644
index 000000000000..567f17b0d7c7
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/public/globe.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/public/next.svg b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/public/next.svg
new file mode 100644
index 000000000000..5174b28c565c
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/public/next.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/public/vercel.svg b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/public/vercel.svg
new file mode 100644
index 000000000000..77053960334e
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/public/vercel.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/public/window.svg b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/public/window.svg
new file mode 100644
index 000000000000..b2b2a44f6ebc
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/public/window.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/sentry.edge.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/sentry.edge.config.ts
new file mode 100644
index 000000000000..f2e946f81728
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/sentry.edge.config.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,
+ traceLifecycle: 'stream',
+ integrations: [Sentry.spanStreamingIntegration()],
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/sentry.server.config.ts
new file mode 100644
index 000000000000..d370665d7dfd
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/sentry.server.config.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,
+ traceLifecycle: 'stream',
+ integrations: [Sentry.vercelAIIntegration(), Sentry.spanStreamingIntegration()],
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/start-event-proxy.mjs
new file mode 100644
index 000000000000..493d4fff94fe
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/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-streaming-cacheComponents',
+ envelopeDumpPath: path.join(
+ process.cwd(),
+ `event-dumps/next-16-cacheComponents-v${packageJson.dependencies.next}-${process.env.TEST_ENV}.dump`,
+ ),
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheComponents.spec.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheComponents.spec.ts
new file mode 100644
index 000000000000..a9caa316a26b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheComponents.spec.ts
@@ -0,0 +1,90 @@
+import { expect, test } from '@playwright/test';
+import { waitForStreamedSpan, waitForStreamedSpans, getSpanOp } from '@sentry-internal/test-utils';
+
+test('Should render cached component', async ({ page }) => {
+ const spansPromise = waitForStreamedSpans('nextjs-16-streaming-cacheComponents', spans => {
+ return spans.some(
+ span => span.name.startsWith('GET /cache') && getSpanOp(span) === 'http.server' && span.is_segment,
+ );
+ });
+
+ await page.goto('/cache');
+
+ const spans = await spansPromise;
+
+ // we want to skip creating spans in cached environments
+ expect(spans.filter(span => getSpanOp(span) === 'get.todos')).toHaveLength(0);
+ await expect(page.locator('#todos-fetched')).toHaveText('Todos fetched: 5');
+});
+
+test('Should render suspense component', async ({ page }) => {
+ const spansPromise = waitForStreamedSpans('nextjs-16-streaming-cacheComponents', spans => {
+ return spans.some(
+ span => span.name.startsWith('GET /suspense') && getSpanOp(span) === 'http.server' && span.is_segment,
+ );
+ });
+
+ await page.goto('/suspense');
+
+ const spans = await spansPromise;
+
+ // this will be called several times in development mode, so we need to check for at least one span
+ expect(spans.filter(span => getSpanOp(span) === 'get.todos').length).toBeGreaterThan(0);
+ await expect(page.locator('#todos-fetched')).toHaveText('Todos fetched: 5');
+});
+
+test('Should generate metadata', async ({ page }) => {
+ const spansPromise = waitForStreamedSpans('nextjs-16-streaming-cacheComponents', spans => {
+ return spans.some(
+ span => span.name.startsWith('GET /metadata') && getSpanOp(span) === 'http.server' && span.is_segment,
+ );
+ });
+
+ await page.goto('/metadata');
+
+ const spans = await spansPromise;
+
+ expect(spans.filter(span => getSpanOp(span) === 'get.todos')).toHaveLength(0);
+ await expect(page.locator('#todos-fetched')).toHaveText('Todos fetched: 5');
+ await expect(page).toHaveTitle('Cache Components Metadata Test');
+});
+
+test('Should generate metadata async', async ({ page }) => {
+ const spansPromise = waitForStreamedSpans('nextjs-16-streaming-cacheComponents', spans => {
+ return spans.some(
+ span => span.name.startsWith('GET /metadata-async') && getSpanOp(span) === 'http.server' && span.is_segment,
+ );
+ });
+
+ await page.goto('/metadata-async');
+
+ const spans = await spansPromise;
+
+ expect(spans.filter(span => getSpanOp(span) === 'get.todos')).toHaveLength(0);
+ await expect(page.locator('#todos-fetched')).toHaveText('Todos fetched: 5');
+ await expect(page).toHaveTitle('Product: 1');
+});
+
+test('Metatag injection produces exactly one pageload trace with cache components', async ({ page }) => {
+ const serverSpanPromise = waitForStreamedSpan('nextjs-16-streaming-cacheComponents', span => {
+ return span.name === 'GET /pageload-tracing' && getSpanOp(span) === 'http.server' && span.is_segment;
+ });
+
+ const pageloadSpanPromise = waitForStreamedSpan('nextjs-16-streaming-cacheComponents', span => {
+ return span.name === '/pageload-tracing' && getSpanOp(span) === 'pageload' && span.is_segment;
+ });
+
+ await page.goto('/pageload-tracing');
+
+ await expect(page.locator('#todos-fetched')).toHaveText('Todos fetched: 5');
+
+ const [serverSpan, pageloadSpan] = await Promise.all([serverSpanPromise, pageloadSpanPromise]);
+
+ // Server and client pageload spans must share the same trace via metatag injection
+ expect(pageloadSpan.trace_id).toBeTruthy();
+ expect(serverSpan.trace_id).toBe(pageloadSpan.trace_id);
+
+ // Exactly one set of trace meta tags — no duplicates that would cause multiple pageloads
+ expect(await page.locator('meta[name="sentry-trace"]').count()).toBe(1);
+ expect(await page.locator('meta[name="baggage"]').count()).toBe(1);
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tsconfig.json b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tsconfig.json
new file mode 100644
index 000000000000..cc9ed39b5aa2
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/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"]
+}