diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index bcbef8e06a8e..ee592f6b7eb8 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -1007,6 +1007,7 @@ jobs:
'node-express-esm-without-loader',
'nextjs-app-dir',
'nextjs-14',
+ 'nextjs-15',
'react-create-hash-router',
'react-router-6-use-routes',
'react-router-5',
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/.gitignore b/dev-packages/e2e-tests/test-applications/nextjs-15/.gitignore
new file mode 100644
index 000000000000..e799cc33c4e7
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15/.gitignore
@@ -0,0 +1,45 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
+
+# local env files
+.env*.local
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
+
+!*.d.ts
+
+# Sentry
+.sentryclirc
+
+.vscode
+
+test-results
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/.npmrc b/dev-packages/e2e-tests/test-applications/nextjs-15/.npmrc
new file mode 100644
index 000000000000..070f80f05092
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15/.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-15/app/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-15/app/layout.tsx
new file mode 100644
index 000000000000..c8f9cee0b787
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15/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-15/app/pageload-tracing/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-15/app/pageload-tracing/layout.tsx
new file mode 100644
index 000000000000..1f0cbe478f88
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15/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-15/app/pageload-tracing/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-15/app/pageload-tracing/page.tsx
new file mode 100644
index 000000000000..4d2763b992b5
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15/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('http://example.com/')).text();
+
+ return {
+ title: 'my title',
+ };
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/globals.d.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/globals.d.ts
new file mode 100644
index 000000000000..109dbcd55648
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15/globals.d.ts
@@ -0,0 +1,4 @@
+interface Window {
+ recordedTransactions?: string[];
+ capturedExceptionId?: string;
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/instrumentation.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/instrumentation.ts
new file mode 100644
index 000000000000..7b89a972e157
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15/instrumentation.ts
@@ -0,0 +1,9 @@
+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');
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/next-env.d.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/next-env.d.ts
new file mode 100644
index 000000000000..4f11a03dc6cc
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15/next-env.d.ts
@@ -0,0 +1,5 @@
+///
+///
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/basic-features/typescript for more information.
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/next.config.js b/dev-packages/e2e-tests/test-applications/nextjs-15/next.config.js
new file mode 100644
index 000000000000..1098c2ce5a4f
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15/next.config.js
@@ -0,0 +1,8 @@
+const { withSentryConfig } = require('@sentry/nextjs');
+
+/** @type {import('next').NextConfig} */
+const nextConfig = {};
+
+module.exports = withSentryConfig(nextConfig, {
+ silent: true,
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/package.json b/dev-packages/e2e-tests/test-applications/nextjs-15/package.json
new file mode 100644
index 000000000000..7c3f4a9f8fb1
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15/package.json
@@ -0,0 +1,46 @@
+{
+ "name": "create-next-app",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "build": "next build > .tmp_build_stdout 2> .tmp_build_stderr || (cat .tmp_build_stdout && cat .tmp_build_stderr && exit 1)",
+ "clean": "npx rimraf node_modules pnpm-lock.yaml",
+ "test:prod": "TEST_ENV=production playwright test",
+ "test:dev": "TEST_ENV=development playwright test",
+ "test:build": "pnpm install && npx playwright install && pnpm build",
+ "test:build-canary": "pnpm install && pnpm add next@canary && npx playwright install && pnpm build",
+ "test:build-latest": "pnpm install && pnpm add next@latest && npx playwright install && pnpm build",
+ "test:assert": "pnpm test:prod && pnpm test:dev"
+ },
+ "dependencies": {
+ "@playwright/test": "^1.27.1",
+ "@sentry/nextjs": "latest || *",
+ "@types/node": "18.11.17",
+ "@types/react": "18.0.26",
+ "@types/react-dom": "18.0.9",
+ "next": "14.3.0-canary.73",
+ "react": "beta",
+ "react-dom": "beta",
+ "typescript": "4.9.5",
+ "wait-port": "1.0.4"
+ },
+ "devDependencies": {
+ "@sentry-internal/event-proxy-server": "link:../../../event-proxy-server",
+ "@sentry-internal/feedback": "latest || *",
+ "@sentry-internal/replay-canvas": "latest || *",
+ "@sentry-internal/browser-utils": "latest || *",
+ "@sentry/browser": "latest || *",
+ "@sentry/core": "latest || *",
+ "@sentry/nextjs": "latest || *",
+ "@sentry/node": "latest || *",
+ "@sentry/opentelemetry": "latest || *",
+ "@sentry/react": "latest || *",
+ "@sentry-internal/replay": "latest || *",
+ "@sentry/types": "latest || *",
+ "@sentry/utils": "latest || *",
+ "@sentry/vercel-edge": "latest || *"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/playwright.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/playwright.config.ts
new file mode 100644
index 000000000000..0709f27158b4
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15/playwright.config.ts
@@ -0,0 +1,80 @@
+import os from 'os';
+import type { PlaywrightTestConfig } from '@playwright/test';
+import { devices } from '@playwright/test';
+
+// Fix urls not resolving to localhost on Node v17+
+// See: https://github.com/axios/axios/issues/3821#issuecomment-1413727575
+import { setDefaultResultOrder } from 'dns';
+setDefaultResultOrder('ipv4first');
+
+const testEnv = process.env.TEST_ENV;
+
+if (!testEnv) {
+ throw new Error('No test env defined');
+}
+
+const nextPort = 3030;
+const eventProxyPort = 3031;
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+const config: PlaywrightTestConfig = {
+ testDir: './tests',
+ /* Maximum time one test can run for. */
+ timeout: 30_000,
+ expect: {
+ /**
+ * Maximum time expect() should wait for the condition to be met.
+ * For example in `await expect(locator).toHaveText();`
+ */
+ timeout: 10000,
+ },
+ /* Run tests in files in parallel */
+ fullyParallel: true,
+ /* Defaults to half the number of CPUs. The tests are not really CPU-bound but rather I/O-bound with all the polling we do so we increase the concurrency to the CPU count. */
+ workers: os.cpus().length,
+ /* Fail the build on CI if you accidentally left test.only in the source code. */
+ forbidOnly: !!process.env.CI,
+ /* `next dev` is incredibly buggy with the app dir */
+ retries: testEnv === 'development' ? 3 : 0,
+ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
+ reporter: 'list',
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+ use: {
+ /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
+ actionTimeout: 0,
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ baseURL: `http://localhost:${nextPort}`,
+ trace: 'retain-on-failure',
+ },
+
+ /* Configure projects for major browsers */
+ projects: [
+ {
+ name: 'chromium',
+ use: {
+ ...devices['Desktop Chrome'],
+ },
+ },
+ ],
+
+ /* Run your local dev server before starting the tests */
+ webServer: [
+ {
+ command: 'node start-event-proxy.mjs',
+ port: eventProxyPort,
+ },
+ {
+ command:
+ testEnv === 'development'
+ ? `pnpm wait-port ${eventProxyPort} && pnpm next dev -p ${nextPort}`
+ : `pnpm wait-port ${eventProxyPort} && pnpm next start -p ${nextPort}`,
+ port: nextPort,
+ stdout: 'pipe',
+ stderr: 'pipe',
+ },
+ ],
+};
+
+export default config;
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/sentry.client.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/sentry.client.config.ts
new file mode 100644
index 000000000000..85bd765c9c44
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15/sentry.client.config.ts
@@ -0,0 +1,9 @@
+import * as Sentry from '@sentry/nextjs';
+
+Sentry.init({
+ environment: 'qa', // dynamic sampling bias to keep transactions
+ dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN,
+ tunnel: `http://localhost:3031/`, // proxy server
+ tracesSampleRate: 1.0,
+ sendDefaultPii: true,
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/sentry.edge.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/sentry.edge.config.ts
new file mode 100644
index 000000000000..067d2ead0b8b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15/sentry.edge.config.ts
@@ -0,0 +1,13 @@
+import * as Sentry from '@sentry/nextjs';
+
+Sentry.init({
+ environment: 'qa', // dynamic sampling bias to keep transactions
+ dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN,
+ tunnel: `http://localhost:3031/`, // proxy server
+ tracesSampleRate: 1.0,
+ sendDefaultPii: true,
+ transportOptions: {
+ // We are doing a lot of events at once in this test
+ bufferSize: 1000,
+ },
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/sentry.server.config.ts
new file mode 100644
index 000000000000..067d2ead0b8b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15/sentry.server.config.ts
@@ -0,0 +1,13 @@
+import * as Sentry from '@sentry/nextjs';
+
+Sentry.init({
+ environment: 'qa', // dynamic sampling bias to keep transactions
+ dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN,
+ tunnel: `http://localhost:3031/`, // proxy server
+ tracesSampleRate: 1.0,
+ sendDefaultPii: true,
+ transportOptions: {
+ // We are doing a lot of events at once in this test
+ bufferSize: 1000,
+ },
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nextjs-15/start-event-proxy.mjs
new file mode 100644
index 000000000000..56744b35c7e6
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15/start-event-proxy.mjs
@@ -0,0 +1,6 @@
+import { startEventProxyServer } from '@sentry-internal/event-proxy-server';
+
+startEventProxyServer({
+ port: 3031,
+ proxyServerName: 'nextjs-15',
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/pageload-tracing.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/pageload-tracing.test.ts
new file mode 100644
index 000000000000..7893633d3b48
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/pageload-tracing.test.ts
@@ -0,0 +1,37 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/event-proxy-server';
+
+test('all server component transactions should be attached to the pageload request span', async ({ page }) => {
+ const pageServerComponentTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => {
+ return transactionEvent?.transaction === 'Page Server Component (/pageload-tracing)';
+ });
+
+ const layoutServerComponentTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => {
+ return transactionEvent?.transaction === 'Layout Server Component (/pageload-tracing)';
+ });
+
+ const metadataTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => {
+ return transactionEvent?.transaction === 'Page.generateMetadata (/pageload-tracing)';
+ });
+
+ const pageloadTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => {
+ return transactionEvent?.transaction === '/pageload-tracing';
+ });
+
+ await page.goto(`/pageload-tracing`);
+
+ const [pageServerComponentTransaction, layoutServerComponentTransaction, metadataTransaction, pageloadTransaction] =
+ await Promise.all([
+ pageServerComponentTransactionPromise,
+ layoutServerComponentTransactionPromise,
+ metadataTransactionPromise,
+ pageloadTransactionPromise,
+ ]);
+
+ const pageloadTraceId = pageloadTransaction.contexts?.trace?.trace_id;
+
+ expect(pageloadTraceId).toBeTruthy();
+ expect(pageServerComponentTransaction.contexts?.trace?.trace_id).toBe(pageloadTraceId);
+ expect(layoutServerComponentTransaction.contexts?.trace?.trace_id).toBe(pageloadTraceId);
+ expect(metadataTransaction.contexts?.trace?.trace_id).toBe(pageloadTraceId);
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/tsconfig.json b/dev-packages/e2e-tests/test-applications/nextjs-15/tsconfig.json
new file mode 100644
index 000000000000..ef9e351d7a7b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "target": "es2018",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "incremental": true
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "next.config.js", ".next/types/**/*.ts"],
+ "exclude": ["node_modules", "playwright.config.ts"]
+}
diff --git a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts
index f3998b693b38..0c76821afa28 100644
--- a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts
+++ b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts
@@ -3,6 +3,7 @@ import {
SPAN_STATUS_ERROR,
SPAN_STATUS_OK,
captureException,
+ getActiveSpan,
getClient,
handleCallbackErrors,
startSpanManual,
@@ -10,7 +11,7 @@ import {
withScope,
} from '@sentry/core';
import type { WebFetchHeaders } from '@sentry/types';
-import { propagationContextFromHeaders, winterCGHeadersToDict } from '@sentry/utils';
+import { propagationContextFromHeaders, uuid4, winterCGHeadersToDict } from '@sentry/utils';
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
import type { GenerationFunctionContext } from '../common/types';
@@ -32,6 +33,7 @@ export function wrapGenerationFunctionWithSentry a
const { requestAsyncStorage, componentRoute, componentType, generationFunctionIdentifier } = context;
return new Proxy(generationFunction, {
apply: (originalFunction, thisArg, args) => {
+ const requestTraceId = getActiveSpan()?.spanContext().traceId;
return escapeNextjsTracing(() => {
let headers: WebFetchHeaders | undefined = undefined;
// We try-catch here just in case anything goes wrong with the async storage here goes wrong since it is Next.js internal API
@@ -50,23 +52,30 @@ export function wrapGenerationFunctionWithSentry a
data = { params, searchParams };
}
- const incomingPropagationContext = propagationContextFromHeaders(
- headers?.get('sentry-trace') ?? undefined,
- headers?.get('baggage'),
- );
+ const headersDict = headers ? winterCGHeadersToDict(headers) : undefined;
const isolationScope = commonObjectToIsolationScope(headers);
- const propagationContext = commonObjectToPropagationContext(headers, incomingPropagationContext);
return withIsolationScope(isolationScope, () => {
return withScope(scope => {
scope.setTransactionName(`${componentType}.${generationFunctionIdentifier} (${componentRoute})`);
+
isolationScope.setSDKProcessingMetadata({
request: {
- headers: headers ? winterCGHeadersToDict(headers) : undefined,
+ headers: headersDict,
},
});
+ const propagationContext = commonObjectToPropagationContext(
+ headers,
+ headersDict?.['sentry-trace']
+ ? propagationContextFromHeaders(headersDict['sentry-trace'], headersDict['baggage'])
+ : {
+ traceId: requestTraceId || uuid4(),
+ spanId: uuid4().substring(16),
+ },
+ );
+
scope.setExtra('route_data', data);
scope.setPropagationContext(propagationContext);
diff --git a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts
index 1234ea448a3d..fe185679528d 100644
--- a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts
+++ b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts
@@ -3,12 +3,13 @@ import {
SPAN_STATUS_ERROR,
SPAN_STATUS_OK,
captureException,
+ getActiveSpan,
handleCallbackErrors,
startSpanManual,
withIsolationScope,
withScope,
} from '@sentry/core';
-import { propagationContextFromHeaders, winterCGHeadersToDict } from '@sentry/utils';
+import { propagationContextFromHeaders, uuid4, winterCGHeadersToDict } from '@sentry/utils';
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
import { isNotFoundNavigationError, isRedirectNavigationError } from '../common/nextNavigationErrorUtils';
@@ -34,30 +35,32 @@ export function wrapServerComponentWithSentry any>
// hook. 🤯
return new Proxy(appDirComponent, {
apply: (originalFunction, thisArg, args) => {
+ const requestTraceId = getActiveSpan()?.spanContext().traceId;
return escapeNextjsTracing(() => {
const isolationScope = commonObjectToIsolationScope(context.headers);
- const completeHeadersDict: Record = context.headers
- ? winterCGHeadersToDict(context.headers)
- : {};
+ const headersDict = context.headers ? winterCGHeadersToDict(context.headers) : undefined;
isolationScope.setSDKProcessingMetadata({
request: {
- headers: completeHeadersDict,
+ headers: headersDict,
},
});
- const incomingPropagationContext = propagationContextFromHeaders(
- completeHeadersDict['sentry-trace'],
- completeHeadersDict['baggage'],
- );
-
- const propagationContext = commonObjectToPropagationContext(context.headers, incomingPropagationContext);
-
return withIsolationScope(isolationScope, () => {
return withScope(scope => {
scope.setTransactionName(`${componentType} Server Component (${componentRoute})`);
+ const propagationContext = commonObjectToPropagationContext(
+ context.headers,
+ headersDict?.['sentry-trace']
+ ? propagationContextFromHeaders(headersDict['sentry-trace'], headersDict['baggage'])
+ : {
+ traceId: requestTraceId || uuid4(),
+ spanId: uuid4().substring(16),
+ },
+ );
+
scope.setPropagationContext(propagationContext);
return startSpanManual(
{
diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts
index f748c5beb115..57601279997c 100644
--- a/packages/nextjs/src/config/types.ts
+++ b/packages/nextjs/src/config/types.ts
@@ -46,6 +46,7 @@ export type NextConfigObject = {
// Next.js experimental options
experimental?: {
instrumentationHook?: boolean;
+ clientTraceMetadata?: string[];
};
};
diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts
index f9c815fe6efb..72b44e16199a 100644
--- a/packages/nextjs/src/config/withSentryConfig.ts
+++ b/packages/nextjs/src/config/withSentryConfig.ts
@@ -1,5 +1,7 @@
-import { isThenable } from '@sentry/utils';
+import { isThenable, parseSemver } from '@sentry/utils';
+import * as fs from 'fs';
+import { sync as resolveSync } from 'resolve';
import type {
ExportedNextConfig as NextConfig,
NextConfigFunction,
@@ -82,6 +84,19 @@ function getFinalConfigObject(
...incomingUserNextConfigObject.experimental,
};
+ // Add the `clientTraceMetadata` experimental option based on Next.js version. The option got introduced in Next.js version 15.0.0 (actually 14.3.0-canary.64).
+ // Adding the option on lower versions will cause Next.js to print nasty warnings we wouldn't confront our users with.
+ const nextJsVersion = getNextjsVersion();
+ if (nextJsVersion) {
+ const { major, minor } = parseSemver(nextJsVersion);
+ if (major && minor && (major >= 15 || (major === 14 && minor >= 3))) {
+ incomingUserNextConfigObject.experimental = {
+ clientTraceMetadata: ['baggage', 'sentry-trace'],
+ ...incomingUserNextConfigObject.experimental,
+ };
+ }
+ }
+
return {
...incomingUserNextConfigObject,
webpack: constructWebpackConfigFunction(incomingUserNextConfigObject, userSentryOptions),
@@ -163,3 +178,28 @@ function setUpTunnelRewriteRules(userNextConfig: NextConfigObject, tunnelPath: s
}
};
}
+
+function getNextjsVersion(): string | undefined {
+ const nextjsPackageJsonPath = resolveNextjsPackageJson();
+ if (nextjsPackageJsonPath) {
+ try {
+ const nextjsPackageJson: { version: string } = JSON.parse(
+ fs.readFileSync(nextjsPackageJsonPath, { encoding: 'utf-8' }),
+ );
+ return nextjsPackageJson.version;
+ } catch {
+ // noop
+ }
+ }
+
+ return undefined;
+}
+
+function resolveNextjsPackageJson(): string | undefined {
+ try {
+ return resolveSync('next/package.json', { basedir: process.cwd() });
+ } catch {
+ // Should not happen in theory
+ return undefined;
+ }
+}