diff --git a/packages/nextjs/src/client/index.ts b/packages/nextjs/src/client/index.ts index 828a4f0a541a..2921f66afe41 100644 --- a/packages/nextjs/src/client/index.ts +++ b/packages/nextjs/src/client/index.ts @@ -48,9 +48,10 @@ export function init(options: BrowserOptions): Client | undefined { if (!DEBUG_BUILD && options.debug) { consoleSandbox(() => { + // TODO(v12): Remove the `webpack.treeshake` mention together with that option // eslint-disable-next-line no-console console.warn( - '[@sentry/nextjs] You have enabled `debug: true`, but Sentry debug logging was removed from your bundle (likely via `webpack.treeshake.removeDebugLogging: true`). Set that option to `false` to see Sentry debug output.', + '[@sentry/nextjs] You have enabled `debug: true`, but Sentry debug logging was removed from your bundle (likely via `bundleSizeOptimizations.excludeDebugStatements: true` or `webpack.treeshake.removeDebugLogging: true`). Set that option to `false` to see Sentry debug output.', ); }); } diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index 2ad05c994433..f1d3a8441bcb 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -60,6 +60,7 @@ export type NextConfigObject = { serverExternalPackages?: string[]; // next >= v15.0.0 turbopack?: TurbopackOptions; compiler?: { + define?: Record; runAfterProductionCompile?: (context: { distDir: string; projectDir: string }) => Promise | void; }; }; @@ -113,6 +114,9 @@ export type SentryBuildWebpackOptions = { /** * Tree-shaking options to help reduce the size of the Sentry SDK bundle. + * + * Prefer the top-level `bundleSizeOptimizations` option, which works for both webpack and Turbopack builds. + * If both are set, `bundleSizeOptimizations` takes precedence. */ treeshake?: { /** @@ -149,7 +153,7 @@ export type SentryBuildWebpackOptions = { * This has no effect if you did not add `replayIntegration`. */ excludeReplayCompressionWorker?: boolean; - }; + }; // TODO: Deprecate this option and its nested options in favor of `bundleSizeOptimizations` closer to v12 /** * Options related to react component name annotations. @@ -393,7 +397,7 @@ export type SentryBuildOptions = Omit< * @deprecated Use the top-level `reactComponentAnnotation` option instead, which works for both webpack and Turbopack builds. */ ignoredComponents?: string[]; - }; // TODO(v12): remove this option + }; // TODO: Deprecate this option and its nested options in favor of `bundleSizeOptimizations` closer to v12 }>; /** diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index 9f594973b41c..833965b69518 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -419,9 +419,7 @@ export function constructWebpackConfigFunction({ } } - if (userSentryOptions.webpack?.treeshake) { - setupTreeshakingFromConfig(userSentryOptions, newConfig, buildContext); - } + setupTreeshakingFromConfig(userSentryOptions, newConfig, buildContext); // We inject a map of dependencies that the nextjs app has, as we cannot reliably extract them at runtime, sadly newConfig.plugins = newConfig.plugins || []; @@ -888,38 +886,49 @@ function prependOrchestrionRuntimeExternals(newConfig: WebpackConfigObjectWithMo } /** - * Sets up the tree-shaking flags based on the user's configuration. + * Returns the tree-shaking flags to define, based on the user's configuration. * https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/tree-shaking/ */ -function setupTreeshakingFromConfig( - userSentryOptions: SentryBuildOptions, - newConfig: WebpackConfigObjectWithModuleRules, - buildContext: BuildContext, -): void { +export function getTreeshakeDefines(userSentryOptions: SentryBuildOptions): Record { const defines: Record = {}; + const bundleSizeOptimizations = userSentryOptions.bundleSizeOptimizations; + const treeshake = userSentryOptions.webpack?.treeshake; - newConfig.plugins = newConfig.plugins || []; - - if (userSentryOptions.webpack?.treeshake?.removeDebugLogging) { + if (bundleSizeOptimizations?.excludeDebugStatements ?? treeshake?.removeDebugLogging) { defines.__SENTRY_DEBUG__ = false; } - if (userSentryOptions.webpack?.treeshake?.removeTracing) { + if (bundleSizeOptimizations?.excludeTracing ?? treeshake?.removeTracing) { defines.__SENTRY_TRACING__ = false; } - if (userSentryOptions.webpack?.treeshake?.excludeReplayIframe) { + if (bundleSizeOptimizations?.excludeReplayIframe ?? treeshake?.excludeReplayIframe) { defines.__RRWEB_EXCLUDE_IFRAME__ = true; } - if (userSentryOptions.webpack?.treeshake?.excludeReplayShadowDOM) { + if (bundleSizeOptimizations?.excludeReplayShadowDom ?? treeshake?.excludeReplayShadowDOM) { defines.__RRWEB_EXCLUDE_SHADOW_DOM__ = true; } - if (userSentryOptions.webpack?.treeshake?.excludeReplayCompressionWorker) { + if (bundleSizeOptimizations?.excludeReplayWorker ?? treeshake?.excludeReplayCompressionWorker) { defines.__SENTRY_EXCLUDE_REPLAY_WORKER__ = true; } + return defines; +} + +/** + * Sets up the tree-shaking flags based on the user's configuration. + */ +function setupTreeshakingFromConfig( + userSentryOptions: SentryBuildOptions, + newConfig: WebpackConfigObjectWithModuleRules, + buildContext: BuildContext, +): void { + const defines = getTreeshakeDefines(userSentryOptions); + + newConfig.plugins = newConfig.plugins || []; + // Only add DefinePlugin if there are actual defines to set if (Object.keys(defines).length > 0) { newConfig.plugins.push(new buildContext.webpack.DefinePlugin(defines)); diff --git a/packages/nextjs/src/config/withSentryConfig/getFinalConfigObject.ts b/packages/nextjs/src/config/withSentryConfig/getFinalConfigObject.ts index 7e6e33682af8..fb1b9f616aeb 100644 --- a/packages/nextjs/src/config/withSentryConfig/getFinalConfigObject.ts +++ b/packages/nextjs/src/config/withSentryConfig/getFinalConfigObject.ts @@ -8,6 +8,7 @@ import { getWebpackPatch, maybeConstructTurbopackConfig, maybeEnableTurbopackSourcemaps, + maybeSetTurbopackTreeshakeDefines, maybeSetUpRunAfterProductionCompileHook, maybeWarnAboutUnsupportedRunAfterProductionCompileHook, maybeWarnAboutTurbopackModuleMetadata, @@ -85,6 +86,7 @@ export function getFinalConfigObject( }); maybeEnableTurbopackSourcemaps(incomingUserNextConfigObject, userSentryOptions, bundlerInfo); + maybeSetTurbopackTreeshakeDefines(incomingUserNextConfigObject, userSentryOptions, bundlerInfo); const buildTimeInstrumentation = resolveBuildTimeInstrumentationOption(userSentryOptions, bundlerInfo, nextJsVersion); diff --git a/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectBundlerUtils.ts b/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectBundlerUtils.ts index 76432027ae56..3e266a8fccb8 100644 --- a/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectBundlerUtils.ts +++ b/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectBundlerUtils.ts @@ -9,7 +9,7 @@ import type { RouteManifest } from '../manifest/types'; import { constructTurbopackConfig } from '../turbopack'; import type { NextConfigObject, SentryBuildOptions, TurbopackOptions } from '../types'; import { detectActiveBundler, supportsProductionCompileHook, supportsTurbopackRuleCondition } from '../util'; -import { constructWebpackConfigFunction } from '../webpack'; +import { constructWebpackConfigFunction, getTreeshakeDefines } from '../webpack'; import { DEFAULT_SERVER_EXTERNAL_PACKAGES } from './constants'; import type { VercelCronsConfigResult } from './getFinalConfigObjectUtils'; @@ -353,6 +353,35 @@ export function getWebpackPatch({ }; } +/** + * Applies the `bundleSizeOptimizations` flags to Turbopack builds through `compiler.define`. + * + * Note: this mutates `incomingUserNextConfigObject`. + */ +export function maybeSetTurbopackTreeshakeDefines( + incomingUserNextConfigObject: NextConfigObject, + userSentryOptions: SentryBuildOptions, + bundlerInfo: BundlerInfo, +): void { + if (!bundlerInfo.isTurbopackSupported || !bundlerInfo.isTurbopack) { + return; + } + + const defines = getTreeshakeDefines(userSentryOptions); + if (Object.keys(defines).length === 0) { + return; + } + + incomingUserNextConfigObject.compiler = { + ...incomingUserNextConfigObject.compiler, + define: { + // Must stay booleans: Turbopack injects string values as string literals, so `'false'` would be truthy. + ...defines, + ...incomingUserNextConfigObject.compiler?.define, + }, + }; +} + /** * Returns the patch for adding turbopack config (if enabled and supported). */ diff --git a/packages/nextjs/src/edge/index.ts b/packages/nextjs/src/edge/index.ts index c971bbafec91..bd1a93617587 100644 --- a/packages/nextjs/src/edge/index.ts +++ b/packages/nextjs/src/edge/index.ts @@ -72,9 +72,10 @@ export function init(options: VercelEdgeOptions = {}): void { } if (!DEBUG_BUILD && options.debug) { + // TODO(v12): Remove the `webpack.treeshake` mention together with that option // eslint-disable-next-line no-console console.warn( - '[@sentry/nextjs] You have enabled `debug: true`, but Sentry debug logging was removed from your bundle (likely via `webpack.treeshake.removeDebugLogging: true`). Set that option to `false` to see Sentry debug output.', + '[@sentry/nextjs] You have enabled `debug: true`, but Sentry debug logging was removed from your bundle (likely via `bundleSizeOptimizations.excludeDebugStatements: true` or `webpack.treeshake.removeDebugLogging: true`). Set that option to `false` to see Sentry debug output.', ); } diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index d7ce162236fb..fd888e787254 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -113,9 +113,10 @@ export function init(options: NodeOptions): NodeClient | undefined { } if (!DEBUG_BUILD && options.debug) { + // TODO(v12): Remove the `webpack.treeshake` mention together with that option // eslint-disable-next-line no-console console.warn( - '[@sentry/nextjs] You have enabled `debug: true`, but Sentry debug logging was removed from your bundle (likely via `webpack.treeshake.removeDebugLogging: true`). Set that option to `false` to see Sentry debug output.', + '[@sentry/nextjs] You have enabled `debug: true`, but Sentry debug logging was removed from your bundle (likely via `bundleSizeOptimizations.excludeDebugStatements: true` or `webpack.treeshake.removeDebugLogging: true`). Set that option to `false` to see Sentry debug output.', ); } diff --git a/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts b/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts index 3e224c76947f..bab62624c86a 100644 --- a/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts +++ b/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts @@ -543,6 +543,41 @@ describe('constructWebpackConfigFunction()', () => { expect(treeshakePlugin.definitions.__SENTRY_EXCLUDE_REPLAY_WORKER__).toBe(true); }); + it('adds flags from the top-level bundleSizeOptimizations option', async () => { + vi.spyOn(coreServer, 'loadModule').mockImplementation(() => ({ + sentryWebpackPlugin: () => ({ + _name: 'sentry-webpack-plugin', + }), + })); + + const finalWebpackConfig = await materializeFinalWebpackConfig({ + exportedNextConfig, + incomingWebpackConfig: serverWebpackConfig, + incomingWebpackBuildContext: serverBuildContext, + sentryBuildTimeOptions: { + bundleSizeOptimizations: { + excludeDebugStatements: true, + excludeTracing: true, + excludeReplayIframe: true, + excludeReplayShadowDom: true, + excludeReplayWorker: true, + }, + }, + }); + + const definePlugin = finalWebpackConfig.plugins?.find( + plugin => plugin.constructor.name === 'DefinePlugin' && plugin.definitions?.__SENTRY_TRACING__ !== undefined, + ) as any; + + expect(definePlugin.definitions).toEqual({ + __SENTRY_DEBUG__: false, + __SENTRY_TRACING__: false, + __RRWEB_EXCLUDE_IFRAME__: true, + __RRWEB_EXCLUDE_SHADOW_DOM__: true, + __SENTRY_EXCLUDE_REPLAY_WORKER__: true, + }); + }); + it('does not add flags when treeshake options are false', async () => { vi.spyOn(coreServer, 'loadModule').mockImplementation(() => ({ sentryWebpackPlugin: () => ({ diff --git a/packages/nextjs/test/config/withSentryConfig.test.ts b/packages/nextjs/test/config/withSentryConfig.test.ts index 977bd4ae72ed..cc749732eb35 100644 --- a/packages/nextjs/test/config/withSentryConfig.test.ts +++ b/packages/nextjs/test/config/withSentryConfig.test.ts @@ -481,6 +481,65 @@ describe('withSentryConfig', () => { }); }); + describe('turbopack treeshake configuration', () => { + const originalTurbopack = process.env.TURBOPACK; + + afterEach(() => { + vi.restoreAllMocks(); + process.env.TURBOPACK = originalTurbopack; + }); + + it('sets treeshake flags as boolean defines and keeps user defines', () => { + process.env.TURBOPACK = '1'; + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('16.0.0'); + + const finalConfig = materializeFinalNextConfig( + { ...exportedNextConfig, compiler: { define: { __SENTRY_DEBUG__: true, USER_FLAG: 'x' } } }, + undefined, + { bundleSizeOptimizations: { excludeTracing: true, excludeDebugStatements: true } }, + ); + + expect(finalConfig.compiler?.define).toEqual({ + __SENTRY_TRACING__: false, + __SENTRY_DEBUG__: true, + USER_FLAG: 'x', + }); + }); + + it('supports the webpack.treeshake option', () => { + process.env.TURBOPACK = '1'; + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('16.0.0'); + + const finalConfig = materializeFinalNextConfig({ ...exportedNextConfig }, undefined, { + webpack: { treeshake: { removeTracing: true } }, + }); + + expect(finalConfig.compiler?.define).toEqual({ __SENTRY_TRACING__: false }); + }); + + it('prefers bundleSizeOptimizations over the webpack.treeshake option', () => { + process.env.TURBOPACK = '1'; + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('16.0.0'); + + const finalConfig = materializeFinalNextConfig({ ...exportedNextConfig }, undefined, { + bundleSizeOptimizations: { excludeTracing: false }, + webpack: { treeshake: { removeTracing: true } }, + }); + + expect(finalConfig.compiler?.define).toBeUndefined(); + }); + + it('does not set defines on webpack builds', () => { + delete process.env.TURBOPACK; + + const finalConfig = materializeFinalNextConfig({ ...exportedNextConfig }, undefined, { + bundleSizeOptimizations: { excludeTracing: true }, + }); + + expect(finalConfig.compiler?.define).toBeUndefined(); + }); + }); + describe('turbopack sourcemap configuration', () => { const originalTurbopack = process.env.TURBOPACK;