From 3499e28eb8938850b359da95f3088cc30bf591fd Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Mon, 12 Oct 2026 09:20:48 +0000 Subject: [PATCH] fix(v10/nextjs): Apply top-level `bundleSizeOptimizations` to webpack and Turbopack builds Backport of: #24818 --- packages/nextjs/src/client/index.ts | 3 +- packages/nextjs/src/config/types.ts | 6 +- packages/nextjs/src/config/webpack.ts | 41 ++++++++----- .../withSentryConfig/getFinalConfigObject.ts | 2 + .../getFinalConfigObjectBundlerUtils.ts | 31 +++++++++- packages/nextjs/src/edge/index.ts | 3 +- packages/nextjs/src/server/index.ts | 3 +- .../webpack/constructWebpackConfig.test.ts | 35 +++++++++++ .../test/config/withSentryConfig.test.ts | 61 ++++++++++++++++++- 9 files changed, 163 insertions(+), 22 deletions(-) diff --git a/packages/nextjs/src/client/index.ts b/packages/nextjs/src/client/index.ts index b7f9c482b816..6476e846b1da 100644 --- a/packages/nextjs/src/client/index.ts +++ b/packages/nextjs/src/client/index.ts @@ -51,9 +51,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 `withSentryConfig({ disableLogger: true })` / `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`, `withSentryConfig({ disableLogger: 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 4f3bfcb1faf4..23ec8d4d8e8d 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -57,6 +57,7 @@ export type NextConfigObject = { outputFileTracingIncludes?: Record; // next >= v15.0.0 turbopack?: TurbopackOptions; compiler?: { + define?: Record; runAfterProductionCompile?: (context: { distDir: string; projectDir: string }) => Promise | void; }; }; @@ -110,6 +111,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?: { /** @@ -146,7 +150,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 to be passed directly to the Sentry Webpack Plugin (`@sentry/webpack-plugin`) that ships with the Sentry SDK. diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index b13dfac5c11d..1d6697f2e105 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -420,9 +420,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 || []; @@ -912,38 +910,49 @@ function addEdgeRuntimePolyfills(newConfig: WebpackConfigObjectWithModuleRules, } /** - * 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 e96ecbf1719f..ca4da139bea1 100644 --- a/packages/nextjs/src/config/withSentryConfig/getFinalConfigObject.ts +++ b/packages/nextjs/src/config/withSentryConfig/getFinalConfigObject.ts @@ -9,6 +9,7 @@ import { getWebpackPatch, maybeConstructTurbopackConfig, maybeEnableTurbopackSourcemaps, + maybeSetTurbopackTreeshakeDefines, maybeSetUpRunAfterProductionCompileHook, maybeWarnAboutUnsupportedRunAfterProductionCompileHook, maybeWarnAboutUnsupportedTurbopack, @@ -84,6 +85,7 @@ export function getFinalConfigObject( }); maybeEnableTurbopackSourcemaps(incomingUserNextConfigObject, userSentryOptions, bundlerInfo); + maybeSetTurbopackTreeshakeDefines(incomingUserNextConfigObject, userSentryOptions, bundlerInfo); const useDiagnosticsChannelInjection = userSentryOptions._experimental?.useDiagnosticsChannelInjection ?? false; diff --git a/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectBundlerUtils.ts b/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectBundlerUtils.ts index 18460dda5ec2..f6767744d03a 100644 --- a/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectBundlerUtils.ts +++ b/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectBundlerUtils.ts @@ -8,7 +8,7 @@ import type { RouteManifest } from '../manifest/types'; import { constructTurbopackConfig } from '../turbopack'; import type { NextConfigObject, SentryBuildOptions, TurbopackOptions } from '../types'; import { detectActiveBundler, supportsProductionCompileHook } from '../util'; -import { constructWebpackConfigFunction } from '../webpack'; +import { constructWebpackConfigFunction, getTreeshakeDefines } from '../webpack'; import { DEFAULT_SERVER_EXTERNAL_PACKAGES } from './constants'; import type { VercelCronsConfigResult } from './getFinalConfigObjectUtils'; @@ -301,6 +301,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 4025f43dc777..75a7d51b53cc 100644 --- a/packages/nextjs/src/edge/index.ts +++ b/packages/nextjs/src/edge/index.ts @@ -58,9 +58,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 `withSentryConfig({ disableLogger: true })` / `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`, `withSentryConfig({ disableLogger: 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 d4038fa0e6e1..736ffd235865 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -126,9 +126,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 `withSentryConfig({ disableLogger: true })` / `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`, `withSentryConfig({ disableLogger: 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 00149b640bb3..42efbd997e9e 100644 --- a/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts +++ b/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts @@ -649,6 +649,41 @@ describe('constructWebpackConfigFunction()', () => { expect(treeshakePlugin.definitions.__SENTRY_EXCLUDE_REPLAY_WORKER__).toBe(true); }); + it('adds flags from the top-level bundleSizeOptimizations option', async () => { + vi.spyOn(core, '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(core, 'loadModule').mockImplementation(() => ({ sentryWebpackPlugin: () => ({ diff --git a/packages/nextjs/test/config/withSentryConfig.test.ts b/packages/nextjs/test/config/withSentryConfig.test.ts index 7dfa68ccbcde..94bfd01cf887 100644 --- a/packages/nextjs/test/config/withSentryConfig.test.ts +++ b/packages/nextjs/test/config/withSentryConfig.test.ts @@ -398,7 +398,7 @@ describe('withSentryConfig', () => { disableLogger: true, }; - materializeFinalNextConfig(exportedNextConfig, undefined, sentryOptions); + materializeFinalNextConfig({ ...exportedNextConfig }, undefined, sentryOptions); expect(consoleWarnSpy).toHaveBeenCalledWith( expect.stringContaining('Use webpack.treeshake.removeDebugLogging instead. (Not supported with Turbopack.)'), @@ -623,6 +623,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;