diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index c7472c08fc20..325e3f1d3187 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -112,6 +112,36 @@ export type SentryBuildWebpackOptions = { * Removes Sentry SDK logger statements from the bundle. Note that this doesn't affect Sentry Logs. */ removeDebugLogging?: boolean; + + /** + * Setting this to true will treeshake any SDK code that is related to tracing and performance monitoring. + */ + removeTracing?: boolean; + + /** + * Setting this flag to `true` will tree shake any SDK code related to capturing iframe content with Session Replay. + * It's only relevant when using Session Replay. Enable this flag if you don't want to record any iframes. + * This has no effect if you did not add `replayIntegration`. + */ + excludeReplayIframe?: boolean; + + /** + * Setting this flag to `true` will tree shake any SDK code related to capturing shadow dom elements with Session Replay. + * It's only relevant when using Session Replay. + * Enable this flag if you don't want to record any shadow DOM elements. + * This has no effect if you did not add `replayIntegration`. + */ + excludeReplayShadowDOM?: boolean; + + /** + * Setting this flag to `true` will tree shake any SDK code that is related to the included compression web worker for Session Replay. + * It's only relevant when using Session Replay. + * Enable this flag if you want to host a compression worker yourself. + * See Using a Custom Compression Worker for details. + * We don't recommend enabling this flag unless you provide a custom worker URL. + * This has no effect if you did not add `replayIntegration`. + */ + excludeReplayCompressionWorker?: boolean; }; /** @@ -403,7 +433,7 @@ export type SentryBuildOptions = { */ bundleSizeOptimizations?: { /** - * If set to `true`, the Sentry SDK will attempt to tree-shake (remove) any debugging code within itself during the build. + * If set to `true`, the Sentry SDK will attempt to treeshake (remove) any debugging code within itself during the build. * Note that the success of this depends on tree shaking being enabled in your build tooling. * * Setting this option to `true` will disable features like the SDK's `debug` option. @@ -411,14 +441,14 @@ export type SentryBuildOptions = { excludeDebugStatements?: boolean; /** - * If set to `true`, the Sentry SDK will attempt to tree-shake (remove) code within itself that is related to tracing and performance monitoring. + * If set to `true`, the Sentry SDK will attempt to treeshake (remove) code within itself that is related to tracing and performance monitoring. * Note that the success of this depends on tree shaking being enabled in your build tooling. * **Notice:** Do not enable this when you're using any performance monitoring-related SDK features (e.g. `Sentry.startTransaction()`). */ excludeTracing?: boolean; /** - * If set to `true`, the Sentry SDK will attempt to tree-shake (remove) code related to the SDK's Session Replay Shadow DOM recording functionality. + * If set to `true`, the Sentry SDK will attempt to treeshake (remove) code related to the SDK's Session Replay Shadow DOM recording functionality. * Note that the success of this depends on tree shaking being enabled in your build tooling. * * This option is safe to be used when you do not want to capture any Shadow DOM activity via Sentry Session Replay. @@ -426,7 +456,7 @@ export type SentryBuildOptions = { excludeReplayShadowDom?: boolean; /** - * If set to `true`, the Sentry SDK will attempt to tree-shake (remove) code related to the SDK's Session Replay `iframe` recording functionality. + * If set to `true`, the Sentry SDK will attempt to treeshake (remove) code related to the SDK's Session Replay `iframe` recording functionality. * Note that the success of this depends on tree shaking being enabled in your build tooling. * * You can safely do this when you do not want to capture any `iframe` activity via Sentry Session Replay. @@ -434,7 +464,7 @@ export type SentryBuildOptions = { excludeReplayIframe?: boolean; /** - * If set to `true`, the Sentry SDK will attempt to tree-shake (remove) code related to the SDK's Session Replay's Compression Web Worker. + * If set to `true`, the Sentry SDK will attempt to treeshake (remove) code related to the SDK's Session Replay's Compression Web Worker. * Note that the success of this depends on tree shaking being enabled in your build tooling. * * **Notice:** You should only use this option if you manually host a compression worker and configure it in your Sentry Session Replay integration config via the `workerUrl` option. diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index 3630e1005c87..60f227b3c42c 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -428,13 +428,8 @@ export function constructWebpackConfigFunction({ } } - if (userSentryOptions.webpack?.treeshake?.removeDebugLogging) { - newConfig.plugins = newConfig.plugins || []; - newConfig.plugins.push( - new buildContext.webpack.DefinePlugin({ - __SENTRY_DEBUG__: false, - }), - ); + if (userSentryOptions.webpack?.treeshake) { + setupTreeshakingFromConfig(userSentryOptions, newConfig, buildContext); } // We inject a map of dependencies that the nextjs app has, as we cannot reliably extract them at runtime, sadly @@ -912,3 +907,42 @@ function _getModules(projectDir: string): Record { return {}; } } + +/** + * Sets up the tree-shaking flags 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 { + const defines: Record = {}; + + newConfig.plugins = newConfig.plugins || []; + + if (userSentryOptions.webpack?.treeshake?.removeDebugLogging) { + defines.__SENTRY_DEBUG__ = false; + } + + if (userSentryOptions.webpack?.treeshake?.removeTracing) { + defines.__SENTRY_TRACING__ = false; + } + + if (userSentryOptions.webpack?.treeshake?.excludeReplayIframe) { + defines.__RRWEB_EXCLUDE_IFRAME__ = true; + } + + if (userSentryOptions.webpack?.treeshake?.excludeReplayShadowDOM) { + defines.__RRWEB_EXCLUDE_SHADOW_DOM__ = true; + } + + if (userSentryOptions.webpack?.treeshake?.excludeReplayCompressionWorker) { + defines.__SENTRY_EXCLUDE_REPLAY_WORKER__ = true; + } + + // 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/test/config/fixtures.ts b/packages/nextjs/test/config/fixtures.ts index 5f5d4a2d3504..f8592148161c 100644 --- a/packages/nextjs/test/config/fixtures.ts +++ b/packages/nextjs/test/config/fixtures.ts @@ -101,7 +101,9 @@ export function getBuildContext( } as NextConfigObject, webpack: { version: webpackVersion, - DefinePlugin: class {} as any, + DefinePlugin: class { + constructor(public definitions: Record) {} + } as any, ProvidePlugin: class { constructor(public definitions: Record) {} } as any, diff --git a/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts b/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts index b8cfb4015512..debc0ed77ae4 100644 --- a/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts +++ b/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts @@ -383,4 +383,410 @@ describe('constructWebpackConfigFunction()', () => { vi.restoreAllMocks(); }); }); + + describe('treeshaking flags', () => { + it('does not add DefinePlugin when treeshake option is not set', async () => { + vi.spyOn(core, 'loadModule').mockImplementation(() => ({ + sentryWebpackPlugin: () => ({ + _name: 'sentry-webpack-plugin', + }), + })); + + const finalWebpackConfig = await materializeFinalWebpackConfig({ + exportedNextConfig, + incomingWebpackConfig: serverWebpackConfig, + incomingWebpackBuildContext: serverBuildContext, + sentryBuildTimeOptions: {}, + }); + + const definePlugin = finalWebpackConfig.plugins?.find( + plugin => plugin.constructor.name === 'DefinePlugin', + ) as any; + + // Should not have a DefinePlugin for treeshaking (may have one for __SENTRY_SERVER_MODULES__) + if (definePlugin) { + expect(definePlugin.definitions).not.toHaveProperty('__SENTRY_DEBUG__'); + expect(definePlugin.definitions).not.toHaveProperty('__SENTRY_TRACING__'); + expect(definePlugin.definitions).not.toHaveProperty('__RRWEB_EXCLUDE_IFRAME__'); + expect(definePlugin.definitions).not.toHaveProperty('__RRWEB_EXCLUDE_SHADOW_DOM__'); + expect(definePlugin.definitions).not.toHaveProperty('__SENTRY_EXCLUDE_REPLAY_WORKER__'); + } + }); + + it('does not add DefinePlugin when treeshake option is empty object', async () => { + vi.spyOn(core, 'loadModule').mockImplementation(() => ({ + sentryWebpackPlugin: () => ({ + _name: 'sentry-webpack-plugin', + }), + })); + + const finalWebpackConfig = await materializeFinalWebpackConfig({ + exportedNextConfig, + incomingWebpackConfig: serverWebpackConfig, + incomingWebpackBuildContext: serverBuildContext, + sentryBuildTimeOptions: { + webpack: { + treeshake: {}, + }, + }, + }); + + const definePlugin = finalWebpackConfig.plugins?.find( + plugin => plugin.constructor.name === 'DefinePlugin', + ) as any; + + // Should not have treeshaking flags in DefinePlugin + if (definePlugin) { + expect(definePlugin.definitions).not.toHaveProperty('__SENTRY_DEBUG__'); + expect(definePlugin.definitions).not.toHaveProperty('__SENTRY_TRACING__'); + expect(definePlugin.definitions).not.toHaveProperty('__RRWEB_EXCLUDE_IFRAME__'); + expect(definePlugin.definitions).not.toHaveProperty('__RRWEB_EXCLUDE_SHADOW_DOM__'); + expect(definePlugin.definitions).not.toHaveProperty('__SENTRY_EXCLUDE_REPLAY_WORKER__'); + } + }); + + it('adds __SENTRY_DEBUG__ flag when debugLogging is true', async () => { + vi.spyOn(core, 'loadModule').mockImplementation(() => ({ + sentryWebpackPlugin: () => ({ + _name: 'sentry-webpack-plugin', + }), + })); + + const finalWebpackConfig = await materializeFinalWebpackConfig({ + exportedNextConfig, + incomingWebpackConfig: serverWebpackConfig, + incomingWebpackBuildContext: serverBuildContext, + sentryBuildTimeOptions: { + webpack: { + treeshake: { + removeDebugLogging: true, + }, + }, + }, + }); + + const definePlugin = finalWebpackConfig.plugins?.find( + plugin => plugin.constructor.name === 'DefinePlugin' && plugin.definitions?.__SENTRY_DEBUG__ !== undefined, + ) as any; + + expect(definePlugin).toBeDefined(); + expect(definePlugin.definitions.__SENTRY_DEBUG__).toBe(false); + }); + + it('adds __SENTRY_TRACING__ flag when tracing is true', async () => { + vi.spyOn(core, 'loadModule').mockImplementation(() => ({ + sentryWebpackPlugin: () => ({ + _name: 'sentry-webpack-plugin', + }), + })); + + const finalWebpackConfig = await materializeFinalWebpackConfig({ + exportedNextConfig, + incomingWebpackConfig: serverWebpackConfig, + incomingWebpackBuildContext: serverBuildContext, + sentryBuildTimeOptions: { + webpack: { + treeshake: { + removeTracing: true, + }, + }, + }, + }); + + const definePlugin = finalWebpackConfig.plugins?.find( + plugin => plugin.constructor.name === 'DefinePlugin' && plugin.definitions?.__SENTRY_TRACING__ !== undefined, + ) as any; + + expect(definePlugin).toBeDefined(); + expect(definePlugin.definitions.__SENTRY_TRACING__).toBe(false); + }); + + it('adds __RRWEB_EXCLUDE_IFRAME__ flag when excludeReplayIframe is true', async () => { + vi.spyOn(core, 'loadModule').mockImplementation(() => ({ + sentryWebpackPlugin: () => ({ + _name: 'sentry-webpack-plugin', + }), + })); + + const finalWebpackConfig = await materializeFinalWebpackConfig({ + exportedNextConfig, + incomingWebpackConfig: serverWebpackConfig, + incomingWebpackBuildContext: serverBuildContext, + sentryBuildTimeOptions: { + webpack: { + treeshake: { + excludeReplayIframe: true, + }, + }, + }, + }); + + const definePlugin = finalWebpackConfig.plugins?.find( + plugin => + plugin.constructor.name === 'DefinePlugin' && plugin.definitions?.__RRWEB_EXCLUDE_IFRAME__ !== undefined, + ) as any; + + expect(definePlugin).toBeDefined(); + expect(definePlugin.definitions.__RRWEB_EXCLUDE_IFRAME__).toBe(true); + }); + + it('adds __RRWEB_EXCLUDE_SHADOW_DOM__ flag when excludeReplayShadowDOM is true', async () => { + vi.spyOn(core, 'loadModule').mockImplementation(() => ({ + sentryWebpackPlugin: () => ({ + _name: 'sentry-webpack-plugin', + }), + })); + + const finalWebpackConfig = await materializeFinalWebpackConfig({ + exportedNextConfig, + incomingWebpackConfig: serverWebpackConfig, + incomingWebpackBuildContext: serverBuildContext, + sentryBuildTimeOptions: { + webpack: { + treeshake: { + excludeReplayShadowDOM: true, + }, + }, + }, + }); + + const definePlugin = finalWebpackConfig.plugins?.find( + plugin => + plugin.constructor.name === 'DefinePlugin' && plugin.definitions?.__RRWEB_EXCLUDE_SHADOW_DOM__ !== undefined, + ) as any; + + expect(definePlugin).toBeDefined(); + expect(definePlugin.definitions.__RRWEB_EXCLUDE_SHADOW_DOM__).toBe(true); + }); + + it('adds __SENTRY_EXCLUDE_REPLAY_WORKER__ flag when excludeReplayCompressionWorker is true', async () => { + vi.spyOn(core, 'loadModule').mockImplementation(() => ({ + sentryWebpackPlugin: () => ({ + _name: 'sentry-webpack-plugin', + }), + })); + + const finalWebpackConfig = await materializeFinalWebpackConfig({ + exportedNextConfig, + incomingWebpackConfig: serverWebpackConfig, + incomingWebpackBuildContext: serverBuildContext, + sentryBuildTimeOptions: { + webpack: { + treeshake: { + excludeReplayCompressionWorker: true, + }, + }, + }, + }); + + const definePlugin = finalWebpackConfig.plugins?.find( + plugin => + plugin.constructor.name === 'DefinePlugin' && + plugin.definitions?.__SENTRY_EXCLUDE_REPLAY_WORKER__ !== undefined, + ) as any; + + expect(definePlugin).toBeDefined(); + expect(definePlugin.definitions.__SENTRY_EXCLUDE_REPLAY_WORKER__).toBe(true); + }); + + it('adds all flags when all treeshake options are enabled', async () => { + vi.spyOn(core, 'loadModule').mockImplementation(() => ({ + sentryWebpackPlugin: () => ({ + _name: 'sentry-webpack-plugin', + }), + })); + + const finalWebpackConfig = await materializeFinalWebpackConfig({ + exportedNextConfig, + incomingWebpackConfig: serverWebpackConfig, + incomingWebpackBuildContext: serverBuildContext, + sentryBuildTimeOptions: { + webpack: { + treeshake: { + removeDebugLogging: true, + removeTracing: true, + excludeReplayIframe: true, + excludeReplayShadowDOM: true, + excludeReplayCompressionWorker: true, + }, + }, + }, + }); + + const definePlugins = finalWebpackConfig.plugins?.filter( + plugin => plugin.constructor.name === 'DefinePlugin', + ) as any[]; + + // Find the plugin that has treeshaking flags (there may be another for __SENTRY_SERVER_MODULES__) + const treeshakePlugin = definePlugins.find( + plugin => + plugin.definitions.__SENTRY_DEBUG__ !== undefined || + plugin.definitions.__SENTRY_TRACING__ !== undefined || + plugin.definitions.__RRWEB_EXCLUDE_IFRAME__ !== undefined || + plugin.definitions.__RRWEB_EXCLUDE_SHADOW_DOM__ !== undefined || + plugin.definitions.__SENTRY_EXCLUDE_REPLAY_WORKER__ !== undefined, + ); + + expect(treeshakePlugin).toBeDefined(); + expect(treeshakePlugin.definitions.__SENTRY_DEBUG__).toBe(false); + expect(treeshakePlugin.definitions.__SENTRY_TRACING__).toBe(false); + expect(treeshakePlugin.definitions.__RRWEB_EXCLUDE_IFRAME__).toBe(true); + expect(treeshakePlugin.definitions.__RRWEB_EXCLUDE_SHADOW_DOM__).toBe(true); + expect(treeshakePlugin.definitions.__SENTRY_EXCLUDE_REPLAY_WORKER__).toBe(true); + }); + + it('does not add flags when treeshake options are false', async () => { + vi.spyOn(core, 'loadModule').mockImplementation(() => ({ + sentryWebpackPlugin: () => ({ + _name: 'sentry-webpack-plugin', + }), + })); + + const finalWebpackConfig = await materializeFinalWebpackConfig({ + exportedNextConfig, + incomingWebpackConfig: serverWebpackConfig, + incomingWebpackBuildContext: serverBuildContext, + sentryBuildTimeOptions: { + webpack: { + treeshake: { + removeDebugLogging: false, + removeTracing: false, + excludeReplayIframe: false, + excludeReplayShadowDOM: false, + excludeReplayCompressionWorker: false, + }, + }, + }, + }); + + const definePlugin = finalWebpackConfig.plugins?.find( + plugin => plugin.constructor.name === 'DefinePlugin', + ) as any; + + // Should not have treeshaking flags + if (definePlugin) { + expect(definePlugin.definitions).not.toHaveProperty('__SENTRY_DEBUG__'); + expect(definePlugin.definitions).not.toHaveProperty('__SENTRY_TRACING__'); + expect(definePlugin.definitions).not.toHaveProperty('__RRWEB_EXCLUDE_IFRAME__'); + expect(definePlugin.definitions).not.toHaveProperty('__RRWEB_EXCLUDE_SHADOW_DOM__'); + expect(definePlugin.definitions).not.toHaveProperty('__SENTRY_EXCLUDE_REPLAY_WORKER__'); + } + }); + + it('works for client builds', async () => { + vi.spyOn(core, 'loadModule').mockImplementation(() => ({ + sentryWebpackPlugin: () => ({ + _name: 'sentry-webpack-plugin', + }), + })); + + const finalWebpackConfig = await materializeFinalWebpackConfig({ + exportedNextConfig, + incomingWebpackConfig: clientWebpackConfig, + incomingWebpackBuildContext: clientBuildContext, + sentryBuildTimeOptions: { + webpack: { + treeshake: { + removeDebugLogging: true, + removeTracing: true, + }, + }, + }, + }); + + const definePlugins = finalWebpackConfig.plugins?.filter( + plugin => plugin.constructor.name === 'DefinePlugin', + ) as any[]; + + const treeshakePlugin = definePlugins.find( + plugin => + plugin.definitions.__SENTRY_DEBUG__ !== undefined || plugin.definitions.__SENTRY_TRACING__ !== undefined, + ); + + expect(treeshakePlugin).toBeDefined(); + expect(treeshakePlugin.definitions.__SENTRY_DEBUG__).toBe(false); + expect(treeshakePlugin.definitions.__SENTRY_TRACING__).toBe(false); + }); + + it('works for edge builds', async () => { + vi.spyOn(core, 'loadModule').mockImplementation(() => ({ + sentryWebpackPlugin: () => ({ + _name: 'sentry-webpack-plugin', + }), + })); + + const finalWebpackConfig = await materializeFinalWebpackConfig({ + exportedNextConfig, + incomingWebpackConfig: serverWebpackConfig, + incomingWebpackBuildContext: edgeBuildContext, + sentryBuildTimeOptions: { + webpack: { + treeshake: { + excludeReplayIframe: true, + excludeReplayShadowDOM: true, + }, + }, + }, + }); + + const definePlugins = finalWebpackConfig.plugins?.filter( + plugin => plugin.constructor.name === 'DefinePlugin', + ) as any[]; + + const treeshakePlugin = definePlugins.find( + plugin => + plugin.definitions.__RRWEB_EXCLUDE_IFRAME__ !== undefined || + plugin.definitions.__RRWEB_EXCLUDE_SHADOW_DOM__ !== undefined, + ); + + expect(treeshakePlugin).toBeDefined(); + expect(treeshakePlugin.definitions.__RRWEB_EXCLUDE_IFRAME__).toBe(true); + expect(treeshakePlugin.definitions.__RRWEB_EXCLUDE_SHADOW_DOM__).toBe(true); + }); + + it('only adds flags for enabled options', async () => { + vi.spyOn(core, 'loadModule').mockImplementation(() => ({ + sentryWebpackPlugin: () => ({ + _name: 'sentry-webpack-plugin', + }), + })); + + const finalWebpackConfig = await materializeFinalWebpackConfig({ + exportedNextConfig, + incomingWebpackConfig: serverWebpackConfig, + incomingWebpackBuildContext: serverBuildContext, + sentryBuildTimeOptions: { + webpack: { + treeshake: { + removeDebugLogging: true, + removeTracing: false, // disabled + excludeReplayIframe: true, + excludeReplayShadowDOM: false, // disabled + excludeReplayCompressionWorker: true, + }, + }, + }, + }); + + const definePlugins = finalWebpackConfig.plugins?.filter( + plugin => plugin.constructor.name === 'DefinePlugin', + ) as any[]; + + const treeshakePlugin = definePlugins.find( + plugin => + plugin.definitions.__SENTRY_DEBUG__ !== undefined || + plugin.definitions.__RRWEB_EXCLUDE_IFRAME__ !== undefined || + plugin.definitions.__SENTRY_EXCLUDE_REPLAY_WORKER__ !== undefined, + ); + + expect(treeshakePlugin).toBeDefined(); + // Should have enabled flags + expect(treeshakePlugin.definitions.__SENTRY_DEBUG__).toBe(false); + expect(treeshakePlugin.definitions.__RRWEB_EXCLUDE_IFRAME__).toBe(true); + expect(treeshakePlugin.definitions.__SENTRY_EXCLUDE_REPLAY_WORKER__).toBe(true); + // Should not have disabled flags + expect(treeshakePlugin.definitions).not.toHaveProperty('__SENTRY_TRACING__'); + expect(treeshakePlugin.definitions).not.toHaveProperty('__RRWEB_EXCLUDE_SHADOW_DOM__'); + }); + }); });