diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/layout.tsx index 999836e58b3b..c8f9cee0b787 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/layout.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/layout.tsx @@ -1,12 +1,7 @@ -import { HackComponentToRunSideEffectsInSentryClientConfig } from '../sentry.client.config'; - export default function Layout({ children }: { children: React.ReactNode }) { return ( - - - {children} - + {children} ); } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/pageload-transaction/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/pageload-transaction/page.tsx new file mode 100644 index 000000000000..4d692cbabd9b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/app/pageload-transaction/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return

Hello World!

; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/instrumentation-client.ts b/dev-packages/e2e-tests/test-applications/nextjs-turbo/instrumentation-client.ts new file mode 100644 index 000000000000..85bd765c9c44 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/instrumentation-client.ts @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/nextjs'; + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN, + tunnel: `http://localhost:3031/`, // proxy server + tracesSampleRate: 1.0, + sendDefaultPii: true, +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json b/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json index d2cdcfb89561..243c719da4c2 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json @@ -17,7 +17,7 @@ "@types/node": "^18.19.1", "@types/react": "18.0.26", "@types/react-dom": "18.0.9", - "next": "15.0.0", + "next": "15.3.0-canary.8", "react": "rc", "react-dom": "rc", "typescript": "~5.0.0" diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/pages/_app.tsx b/dev-packages/e2e-tests/test-applications/nextjs-turbo/pages/_app.tsx index 6b90ee6bc586..7f0b03d2959a 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-turbo/pages/_app.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/pages/_app.tsx @@ -1,5 +1,4 @@ import type { AppProps } from 'next/app'; -import '../sentry.client.config'; export default function CustomApp({ Component, pageProps }: AppProps) { return ; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/sentry.client.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-turbo/sentry.client.config.ts deleted file mode 100644 index 7a49f1b55e11..000000000000 --- a/dev-packages/e2e-tests/test-applications/nextjs-turbo/sentry.client.config.ts +++ /dev/null @@ -1,17 +0,0 @@ -'use client'; - -import * as Sentry from '@sentry/nextjs'; - -if (typeof window !== 'undefined') { - Sentry.init({ - environment: 'qa', // dynamic sampling bias to keep transactions - dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN, - tunnel: `http://localhost:3031/`, // proxy server - tracesSampleRate: 1.0, - sendDefaultPii: true, - }); -} - -export function HackComponentToRunSideEffectsInSentryClientConfig() { - return null; -} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/tests/app-router/pageload-transaction.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-turbo/tests/app-router/pageload-transaction.test.ts new file mode 100644 index 000000000000..62a072b4ae7f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/tests/app-router/pageload-transaction.test.ts @@ -0,0 +1,27 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; +import { parseSemver } from '@sentry/core'; + +const packageJson = require('../../package.json'); +const nextjsVersion = packageJson.dependencies.next; +const { major, minor } = parseSemver(nextjsVersion); + +test('Should record pageload transactions (this test verifies that the client SDK is initialized)', async ({ + page, +}) => { + // TODO: Remove this skippage when Next.js 15.3.0 is released and bump version in package json to 15.3.0 + test.skip( + major === 15 && minor !== undefined && minor < 3, + 'Next.js version does not support clientside instrumentation', + ); + + const pageloadTransactionPromise = waitForTransaction('nextjs-turbo', async transactionEvent => { + return transactionEvent?.transaction === '/pageload-transaction'; + }); + + await page.goto(`/pageload-transaction`); + + const pageloadTransaction = await pageloadTransactionPromise; + + expect(pageloadTransaction).toBeDefined(); +}); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/tests/pages-router/client-trace-propagation.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-turbo/tests/pages-router/client-trace-propagation.test.ts index 20a9181d7f8e..52e492b3f234 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-turbo/tests/pages-router/client-trace-propagation.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/tests/pages-router/client-trace-propagation.test.ts @@ -1,7 +1,18 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; +import { parseSemver } from '@sentry/core'; + +const packageJson = require('../../package.json'); +const nextjsVersion = packageJson.dependencies.next; +const { major, minor } = parseSemver(nextjsVersion); test('Should propagate traces from server to client in pages router', async ({ page }) => { + // TODO: Remove this skippage when Next.js 15.3.0 is released and bump version in package json to 15.3.0 + test.skip( + major === 15 && minor !== undefined && minor < 3, + 'Next.js version does not support clientside instrumentation', + ); + const serverTransactionPromise = waitForTransaction('nextjs-turbo', async transactionEvent => { return transactionEvent?.transaction === 'GET /[param]/pages-router-client-trace-propagation'; }); diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index 965233d08b76..95c15b887573 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -44,6 +44,7 @@ export type NextConfigObject = { // Next.js experimental options experimental?: { instrumentationHook?: boolean; + clientInstrumentationHook?: boolean; clientTraceMetadata?: string[]; }; productionBrowserSourceMaps?: boolean; diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index de047c0b8cf4..cababfa63002 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -339,6 +339,14 @@ export function constructWebpackConfigFunction( // be fixed by using `bind`, but this is way simpler.) const origEntryProperty = newConfig.entry; newConfig.entry = async () => addSentryToClientEntryProperty(origEntryProperty, buildContext); + + const clientSentryConfigFileName = getClientSentryConfigFile(projectDir); + if (clientSentryConfigFileName) { + // eslint-disable-next-line no-console + console.warn( + `[@sentry/nextjs] DEPRECATION WARNING: It is recommended renaming your \`${clientSentryConfigFileName}\` file, or moving its content to \`instrumentation-client.ts\`. When using Turbopack \`${clientSentryConfigFileName}\` will no longer work. Read more about the \`instrumentation-client.ts\` file: https://nextjs.org/docs/app/api-reference/config/next-config-js/clientInstrumentationHook`, + ); + } } // We don't want to do any webpack plugin stuff OR any source maps stuff in dev mode. @@ -430,9 +438,17 @@ async function addSentryToClientEntryProperty( typeof currentEntryProperty === 'function' ? await currentEntryProperty() : { ...currentEntryProperty }; const clientSentryConfigFileName = getClientSentryConfigFile(projectDir); + const instrumentationClientFileName = getInstrumentationClientFile(projectDir); - // we need to turn the filename into a path so webpack can find it - const filesToInject = clientSentryConfigFileName ? [`./${clientSentryConfigFileName}`] : []; + const filesToInject = []; + if (clientSentryConfigFileName) { + // we need to turn the filename into a path so webpack can find it + filesToInject.push(`./${clientSentryConfigFileName}`); + } + if (instrumentationClientFileName) { + // we need to turn the filename into a path so webpack can find it + filesToInject.push(`./${instrumentationClientFileName}`); + } // inject into all entry points which might contain user's code for (const entryPointName in newEntryProperty) { @@ -530,7 +546,7 @@ function warnAboutDeprecatedConfigFiles( * * @param projectDir The root directory of the project, where config files would be located */ -export function getClientSentryConfigFile(projectDir: string): string | void { +function getClientSentryConfigFile(projectDir: string): string | void { const possibilities = ['sentry.client.config.ts', 'sentry.client.config.js']; for (const filename of possibilities) { @@ -540,6 +556,26 @@ export function getClientSentryConfigFile(projectDir: string): string | void { } } +/** + * Searches for a `instrumentation-client.ts|js` file and returns its file name if it finds one. (ts being prioritized) + * + * @param projectDir The root directory of the project, where config files would be located + */ +function getInstrumentationClientFile(projectDir: string): string | void { + const possibilities = [ + ['src', 'instrumentation-client.js'], + ['src', 'instrumentation-client.ts'], + ['instrumentation-client.js'], + ['instrumentation-client.ts'], + ]; + + for (const pathParts of possibilities) { + if (fs.existsSync(path.resolve(projectDir, ...pathParts))) { + return path.join(...pathParts); + } + } +} + /** * Add files to a specific element of the given `entry` webpack config property. * diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts index e811697cbe86..7250af15ff8e 100644 --- a/packages/nextjs/src/config/withSentryConfig.ts +++ b/packages/nextjs/src/config/withSentryConfig.ts @@ -153,14 +153,57 @@ function getFinalConfigObject( } } - if (process.env.TURBOPACK && !process.env.SENTRY_SUPPRESS_TURBOPACK_WARNING) { + if (nextJsVersion) { + const { major, minor, patch, prerelease } = parseSemver(nextJsVersion); + const isSupportedVersion = + major !== undefined && + minor !== undefined && + patch !== undefined && + (major > 15 || + (major === 15 && minor > 3) || + (major === 15 && minor === 3 && patch > 0 && prerelease === undefined)); + const isSupportedCanary = + major !== undefined && + minor !== undefined && + patch !== undefined && + prerelease !== undefined && + major === 15 && + minor === 3 && + patch === 0 && + prerelease.startsWith('canary.') && + parseInt(prerelease.split('.')[1] || '', 10) >= 8; + const supportsClientInstrumentation = isSupportedCanary || isSupportedVersion; + + if (supportsClientInstrumentation) { + incomingUserNextConfigObject.experimental = { + clientInstrumentationHook: true, + ...incomingUserNextConfigObject.experimental, + }; + } else if (process.env.TURBOPACK) { + if (process.env.NODE_ENV === 'development') { + // eslint-disable-next-line no-console + console.warn( + `[@sentry/nextjs] WARNING: You are using the Sentry SDK with Turbopack (\`next dev --turbo\`). The Sentry SDK is compatible with Turbopack on Next.js version 15.3.0 or later. You are currently on ${nextJsVersion}. Please upgrade to a newer Next.js version to use the Sentry SDK with Turbopack. Note that the SDK will continue to work for non-Turbopack production builds. This warning is only about dev-mode.`, + ); + } else if (process.env.NODE_ENV === 'production') { + // eslint-disable-next-line no-console + console.warn( + `[@sentry/nextjs] WARNING: You are using the Sentry SDK with Turbopack (\`next build --turbo\`). The Sentry SDK is compatible with Turbopack on Next.js version 15.3.0 or later. You are currently on ${nextJsVersion}. Please upgrade to a newer Next.js version to use the Sentry SDK with Turbopack. Note that as Turbopack is still experimental for production builds, some of the Sentry SDK features like source maps will not work. Follow this issue for progress on Sentry + Turbopack: https://github.com/getsentry/sentry-javascript/issues/8105.`, + ); + } + } + } else { + // If we cannot detect a Next.js version for whatever reason, the sensible default is still to set the `experimental.instrumentationHook`. + incomingUserNextConfigObject.experimental = { + clientInstrumentationHook: true, + ...incomingUserNextConfigObject.experimental, + }; + } + + if (incomingUserNextConfigObject.experimental?.clientInstrumentationHook === false) { // eslint-disable-next-line no-console console.warn( - `[@sentry/nextjs] WARNING: You are using the Sentry SDK with \`next ${ - process.env.NODE_ENV === 'development' ? 'dev' : 'build' - } --turbo\`. The Sentry SDK doesn't yet fully support Turbopack. The SDK will not be loaded in the browser, and serverside instrumentation will be inaccurate or incomplete. ${ - process.env.NODE_ENV === 'development' ? 'Production builds without `--turbo` will still fully work. ' : '' - }If you are just trying out Sentry or attempting to configure the SDK, we recommend temporarily removing the \`--turbo\` flag while you are developing locally. Follow this issue for progress on Sentry + Turbopack: https://github.com/getsentry/sentry-javascript/issues/8105. (You can suppress this warning by setting SENTRY_SUPPRESS_TURBOPACK_WARNING=1 as environment variable)`, + '[@sentry/nextjs] WARNING: You set the `experimental.clientInstrumentationHook` option to `false`. Note that Sentry will not be initialized if you did not set it up inside `instrumentation-client.(js|ts)`.', ); }