From 040983d9bcb7f0f0626f30514162e5367e5d4072 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 31 Jul 2026 17:27:16 +0200 Subject: [PATCH 1/4] e2e --- .../test-applications/nextjs-16-orchestrion/next.config.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-orchestrion/next.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-orchestrion/next.config.ts index e5ac8ef816bc..6699b3dd2c33 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16-orchestrion/next.config.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16-orchestrion/next.config.ts @@ -5,5 +5,4 @@ const nextConfig: NextConfig = {}; export default withSentryConfig(nextConfig, { silent: true, - _experimental: { useDiagnosticsChannelInjection: true }, }); From c904fbd254cd1785a8c5714cc4ff7af313b8d47a Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 31 Jul 2026 17:29:46 +0200 Subject: [PATCH 2/4] flip experimental option --- .../turbopack/constructTurbopackConfig.ts | 4 +- packages/nextjs/src/config/types.ts | 23 +++++----- packages/nextjs/src/config/webpack.ts | 2 +- .../src/config/withSentryConfig/buildTime.ts | 5 --- .../withSentryConfig/getFinalConfigObject.ts | 5 ++- .../getFinalConfigObjectBundlerUtils.ts | 32 +++++++++++--- .../diagnosticsChannelInjection.test.ts | 43 ++++++++++++------- .../constructTurbopackConfig.test.ts | 20 ++++++--- .../webpack/constructWebpackConfig.test.ts | 22 +++++----- .../test/config/withSentryConfig.test.ts | 24 ++++++++--- 10 files changed, 115 insertions(+), 65 deletions(-) diff --git a/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts b/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts index 9e9aad687f45..60880163df74 100644 --- a/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts +++ b/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts @@ -123,7 +123,7 @@ export function constructTurbopackConfig({ } /** - * Adds the orchestrion code-transform loader rule when diagnostics-channel injection is enabled. + * Adds the orchestrion code-transform loader rule unless build-time instrumentation is turned off. */ function maybeAddOrchestrionRule( rules: TurbopackOptions['rules'], @@ -131,7 +131,7 @@ function maybeAddOrchestrionRule( nextJsVersion: string | undefined, ): TurbopackOptions['rules'] { if ( - !userSentryOptions?._experimental?.useDiagnosticsChannelInjection || + userSentryOptions?.buildTimeInstrumentation === false || !nextJsVersion || !supportsTurbopackRuleCondition(nextJsVersion) ) { diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index ea0656ffc338..a82b663c562c 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -464,6 +464,17 @@ export type SentryBuildOptions = { }; }; + /** + * Automatic instrumentation of server-side dependencies at build time. + * + * Set to `false` to turn it off. + * + * Turbopack support requires Next.js 16+; the webpack path works on earlier versions. + * + * @default true + */ + buildTimeInstrumentation?: boolean; + /** * A key that is used to identify the application in the Sentry bundler plugins. * This key is used by the `thirdPartyErrorFilterIntegration` to filter out errors @@ -765,18 +776,6 @@ export type SentryBuildOptions = { enabled?: boolean; ignoredComponents?: string[]; }; - /** - * EXPERIMENTAL: Wire up orchestrion diagnostics-channel instrumentation at build time. - * - * When enabled, `withSentryConfig` injects the orchestrion code-transform loader for bundled - * server packages and keeps the remaining instrumented packages external so the runtime module - * hook picks them up. - * - * Turbopack support requires Next.js 16+; the webpack path works on earlier versions. - * - * @experimental May change or be removed in any release. - */ - useDiagnosticsChannelInjection?: boolean; }>; /** diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index 950596b91c98..02d5e7249848 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -429,7 +429,7 @@ export function constructWebpackConfigFunction({ ); // Orchestrion code-transform loader — Node server runtime only, never the edge compilation - if (runtime === 'server' && userSentryOptions._experimental?.useDiagnosticsChannelInjection) { + if (runtime === 'server' && userSentryOptions.buildTimeInstrumentation !== false) { newConfig.plugins.push(sentryOrchestrionWebpackPlugin() as unknown as WebpackPluginInstance); prependOrchestrionRuntimeExternals(newConfig); } diff --git a/packages/nextjs/src/config/withSentryConfig/buildTime.ts b/packages/nextjs/src/config/withSentryConfig/buildTime.ts index b799c9becfaa..c468b4a1f18e 100644 --- a/packages/nextjs/src/config/withSentryConfig/buildTime.ts +++ b/packages/nextjs/src/config/withSentryConfig/buildTime.ts @@ -50,11 +50,6 @@ export function setUpBuildTimeVariables( buildTimeVariables._sentryRewritesTunnelPath = rewritesTunnelPath; } - // Marker read by the server SDK to warn if the runtime opt-in call is missing. - if (userSentryOptions._experimental?.useDiagnosticsChannelInjection) { - buildTimeVariables._sentryUseDiagnosticsChannelInjection = 'true'; - } - if (basePath) { buildTimeVariables._sentryBasePath = basePath; } diff --git a/packages/nextjs/src/config/withSentryConfig/getFinalConfigObject.ts b/packages/nextjs/src/config/withSentryConfig/getFinalConfigObject.ts index e96ecbf1719f..ee88fea9c3ea 100644 --- a/packages/nextjs/src/config/withSentryConfig/getFinalConfigObject.ts +++ b/packages/nextjs/src/config/withSentryConfig/getFinalConfigObject.ts @@ -12,6 +12,7 @@ import { maybeSetUpRunAfterProductionCompileHook, maybeWarnAboutUnsupportedRunAfterProductionCompileHook, maybeWarnAboutUnsupportedTurbopack, + resolveBuildTimeInstrumentationOption, resolveUseRunAfterProductionCompileHookOption, } from './getFinalConfigObjectBundlerUtils'; import { @@ -85,11 +86,11 @@ export function getFinalConfigObject( maybeEnableTurbopackSourcemaps(incomingUserNextConfigObject, userSentryOptions, bundlerInfo); - const useDiagnosticsChannelInjection = userSentryOptions._experimental?.useDiagnosticsChannelInjection ?? false; + const buildTimeInstrumentation = resolveBuildTimeInstrumentationOption(userSentryOptions, bundlerInfo, nextJsVersion); return { ...incomingUserNextConfigObject, - ...getServerExternalPackagesPatch(incomingUserNextConfigObject, nextMajor, useDiagnosticsChannelInjection), + ...getServerExternalPackagesPatch(incomingUserNextConfigObject, nextMajor, buildTimeInstrumentation), ...getWebpackPatch({ incomingUserNextConfigObject, userSentryOptions, diff --git a/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectBundlerUtils.ts b/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectBundlerUtils.ts index 18460dda5ec2..dc1a946ff68e 100644 --- a/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectBundlerUtils.ts +++ b/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectBundlerUtils.ts @@ -7,7 +7,7 @@ import { handleRunAfterProductionCompile } from '../handleRunAfterProductionComp import type { RouteManifest } from '../manifest/types'; import { constructTurbopackConfig } from '../turbopack'; import type { NextConfigObject, SentryBuildOptions, TurbopackOptions } from '../types'; -import { detectActiveBundler, supportsProductionCompileHook } from '../util'; +import { detectActiveBundler, supportsProductionCompileHook, supportsTurbopackRuleCondition } from '../util'; import { constructWebpackConfigFunction } from '../webpack'; import { DEFAULT_SERVER_EXTERNAL_PACKAGES } from './constants'; import type { VercelCronsConfigResult } from './getFinalConfigObjectUtils'; @@ -94,6 +94,28 @@ export function maybeConstructTurbopackConfig( }); } +/** + * Resolves whether to wire up orchestrion build-time instrumentation. + * + * Turbopack needs rule `condition`s (Next.js 16+) to run the transform. Without it, un-externalizing + * the bundle-safe packages would leave them bundled *and* uninstrumented, so keep the feature off. + */ +export function resolveBuildTimeInstrumentationOption( + userSentryOptions: SentryBuildOptions, + bundlerInfo: BundlerInfo, + nextJsVersion: string | undefined, +): boolean { + if (userSentryOptions.buildTimeInstrumentation === false) { + return false; + } + + if (bundlerInfo.isTurbopack) { + return !!nextJsVersion && supportsTurbopackRuleCondition(nextJsVersion); + } + + return true; +} + /** * Resolves whether to use the `runAfterProductionCompile` hook based on options and bundler. */ @@ -232,19 +254,19 @@ export function maybeEnableTurbopackSourcemaps( export function getServerExternalPackagesPatch( incomingUserNextConfigObject: NextConfigObject, nextMajor: number | undefined, - useDiagnosticsChannelInjection = false, + buildTimeInstrumentation = false, ): Partial { - // Diagnostics-channel injection: only bundle-safe packages leave OUR defaults (→ build-time + // With build-time instrumentation, only bundle-safe packages leave OUR defaults (→ build-time // loader); everything else stays external (→ runtime module hook), including the orchestrion // machinery itself, which breaks when bundled. const mergeExternals = (userProvided: string[] | undefined): string[] => { - const defaults = useDiagnosticsChannelInjection + const defaults = buildTimeInstrumentation ? filterInstrumentedExternals(DEFAULT_SERVER_EXTERNAL_PACKAGES, BUNDLE_SAFE_INSTRUMENTED_PACKAGES) : DEFAULT_SERVER_EXTERNAL_PACKAGES; return [ ...(userProvided || []), ...defaults, - ...(useDiagnosticsChannelInjection ? ORCHESTRION_RUNTIME_EXTERNAL_PACKAGES : []), + ...(buildTimeInstrumentation ? ORCHESTRION_RUNTIME_EXTERNAL_PACKAGES : []), ]; }; diff --git a/packages/nextjs/test/config/diagnosticsChannelInjection.test.ts b/packages/nextjs/test/config/diagnosticsChannelInjection.test.ts index 50e4e371e85d..a2c69aadfcbb 100644 --- a/packages/nextjs/test/config/diagnosticsChannelInjection.test.ts +++ b/packages/nextjs/test/config/diagnosticsChannelInjection.test.ts @@ -6,9 +6,11 @@ import { externalizeOrchestrionRuntimePackages, filterInstrumentedExternals, } from '../../src/config/diagnosticsChannelInjection'; -import { setUpBuildTimeVariables } from '../../src/config/withSentryConfig/buildTime'; -import { getServerExternalPackagesPatch } from '../../src/config/withSentryConfig/getFinalConfigObjectBundlerUtils'; -import type { NextConfigObject } from '../../src/config/types'; +import type { BundlerInfo } from '../../src/config/withSentryConfig/getFinalConfigObjectBundlerUtils'; +import { + getServerExternalPackagesPatch, + resolveBuildTimeInstrumentationOption, +} from '../../src/config/withSentryConfig/getFinalConfigObjectBundlerUtils'; describe('filterInstrumentedExternals', () => { it('removes the given packages, keeps the rest', () => { @@ -22,7 +24,7 @@ describe('filterInstrumentedExternals', () => { }); }); -describe('getServerExternalPackagesPatch (diagnostics-channel injection)', () => { +describe('getServerExternalPackagesPatch (build-time instrumentation)', () => { it('keeps everything external except the bundle-safe allowlist, and adds the runtime machinery', () => { const patch = getServerExternalPackagesPatch({}, 16, true); const externals = patch.serverExternalPackages ?? []; @@ -44,7 +46,7 @@ describe('getServerExternalPackagesPatch (diagnostics-channel injection)', () => expect(patch.serverExternalPackages).toContain('ioredis'); }); - it('is unchanged with the flag off', () => { + it('is unchanged when build-time instrumentation is off', () => { const patch = getServerExternalPackagesPatch({}, 16, false); const externals = patch.serverExternalPackages ?? []; expect(externals).toContain('ioredis'); @@ -90,20 +92,29 @@ describe('externalizeOrchestrionRuntimePackages', () => { }); }); -describe('setUpBuildTimeVariables (diagnostics-channel injection)', () => { - it('injects the flag marker and the tracing-hooks location', () => { - const nextConfig: NextConfigObject = {}; - setUpBuildTimeVariables(nextConfig, { _experimental: { useDiagnosticsChannelInjection: true } }, undefined); +describe('resolveBuildTimeInstrumentationOption', () => { + const webpack: BundlerInfo = { isWebpack: true, isTurbopack: false, isTurbopackSupported: true }; + const turbopack: BundlerInfo = { isWebpack: false, isTurbopack: true, isTurbopackSupported: true }; - expect(nextConfig.env).toMatchObject({ - _sentryUseDiagnosticsChannelInjection: 'true', - }); + it('is on by default', () => { + expect(resolveBuildTimeInstrumentationOption({}, webpack, '15.0.0')).toBe(true); + expect(resolveBuildTimeInstrumentationOption({}, turbopack, '16.0.0')).toBe(true); }); - it('injects neither with the flag off', () => { - const nextConfig: NextConfigObject = {}; - setUpBuildTimeVariables(nextConfig, {}, undefined); + it.each([webpack, turbopack])('respects an explicit opt-out', bundlerInfo => { + expect(resolveBuildTimeInstrumentationOption({ buildTimeInstrumentation: false }, bundlerInfo, '16.0.0')).toBe( + false, + ); + }); + + // Un-externalizing the bundle-safe packages without a transform to instrument them would leave + // them bundled *and* uninstrumented, so the whole feature has to stay off. + it('stays off under Turbopack below Next.js 16, where the transform rule cannot run', () => { + expect(resolveBuildTimeInstrumentationOption({}, turbopack, '15.4.1')).toBe(false); + expect(resolveBuildTimeInstrumentationOption({}, turbopack, undefined)).toBe(false); + }); - expect(nextConfig.env).not.toHaveProperty('_sentryUseDiagnosticsChannelInjection'); + it('still applies on webpack when the Next.js version is unknown', () => { + expect(resolveBuildTimeInstrumentationOption({}, webpack, undefined)).toBe(true); }); }); diff --git a/packages/nextjs/test/config/turbopack/constructTurbopackConfig.test.ts b/packages/nextjs/test/config/turbopack/constructTurbopackConfig.test.ts index fadb4cdd4820..9b4a6c6b1d66 100644 --- a/packages/nextjs/test/config/turbopack/constructTurbopackConfig.test.ts +++ b/packages/nextjs/test/config/turbopack/constructTurbopackConfig.test.ts @@ -1307,7 +1307,7 @@ describe('componentAnnotation with turbopackReactComponentAnnotation', () => { }); }); -describe('orchestrion diagnostics-channel injection', () => { +describe('orchestrion build-time instrumentation', () => { function getOrchestrionOptions(result: ReturnType): { instrumentations: Array<{ module: { name: string; filePath: unknown } }>; } { @@ -1320,7 +1320,7 @@ describe('orchestrion diagnostics-channel injection', () => { it('serializes a RegExp filePath so it survives Turbopack JSON loader options', () => { const result = constructTurbopackConfig({ userNextConfig: {}, - userSentryOptions: { _experimental: { useDiagnosticsChannelInjection: true } }, + userSentryOptions: {}, nextJsVersion: '16.0.0', }); @@ -1340,7 +1340,7 @@ describe('orchestrion diagnostics-channel injection', () => { it('restricts the orchestrion rule to the node environment', () => { const result = constructTurbopackConfig({ userNextConfig: {}, - userSentryOptions: { _experimental: { useDiagnosticsChannelInjection: true } }, + userSentryOptions: {}, nextJsVersion: '16.0.0', }); @@ -1350,15 +1350,25 @@ describe('orchestrion diagnostics-channel injection', () => { expect(rule.condition).toBe('node'); }); - it('does not add the orchestrion rule when injection is not opted in', () => { + it('does not add the orchestrion rule when build-time instrumentation is turned off', () => { const result = constructTurbopackConfig({ userNextConfig: {}, - userSentryOptions: {}, + userSentryOptions: { buildTimeInstrumentation: false }, nextJsVersion: '16.0.0', }); expect(result.rules!['*.{js,mjs,cjs}']).toBeUndefined(); }); + + it('does not add the orchestrion rule below Next.js 16, where rule conditions are unsupported', () => { + const result = constructTurbopackConfig({ + userNextConfig: {}, + userSentryOptions: {}, + nextJsVersion: '15.4.1', + }); + + expect(result.rules?.['*.{js,mjs,cjs}']).toBeUndefined(); + }); }); describe('safelyAddTurbopackRule', () => { diff --git a/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts b/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts index a31a8ca4490a..4f87e7cddcc8 100644 --- a/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts +++ b/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts @@ -681,12 +681,12 @@ describe('constructWebpackConfigFunction()', () => { const findOrchestrionPlugin = (config: { plugins?: unknown[] }): unknown => config.plugins?.find(plugin => (plugin as { _name?: string })._name === 'sentry-orchestrion-webpack-plugin'); - it('adds the plugin to the node server build when diagnostics-channel injection is enabled', async () => { + it('adds the plugin to the node server build by default', async () => { const finalWebpackConfig = await materializeFinalWebpackConfig({ exportedNextConfig, incomingWebpackConfig: serverWebpackConfig, incomingWebpackBuildContext: serverBuildContext, - sentryBuildTimeOptions: { _experimental: { useDiagnosticsChannelInjection: true } }, + sentryBuildTimeOptions: {}, }); expect(findOrchestrionPlugin(finalWebpackConfig)).toBeDefined(); @@ -697,7 +697,7 @@ describe('constructWebpackConfigFunction()', () => { exportedNextConfig, incomingWebpackConfig: serverWebpackConfig, incomingWebpackBuildContext: edgeBuildContext, - sentryBuildTimeOptions: { _experimental: { useDiagnosticsChannelInjection: true } }, + sentryBuildTimeOptions: {}, }); expect(findOrchestrionPlugin(finalWebpackConfig)).toBeUndefined(); @@ -708,18 +708,18 @@ describe('constructWebpackConfigFunction()', () => { exportedNextConfig, incomingWebpackConfig: clientWebpackConfig, incomingWebpackBuildContext: clientBuildContext, - sentryBuildTimeOptions: { _experimental: { useDiagnosticsChannelInjection: true } }, + sentryBuildTimeOptions: {}, }); expect(findOrchestrionPlugin(finalWebpackConfig)).toBeUndefined(); }); - it('does not add the plugin when diagnostics-channel injection is not enabled', async () => { + it('does not add the plugin when build-time instrumentation is turned off', async () => { const finalWebpackConfig = await materializeFinalWebpackConfig({ exportedNextConfig, incomingWebpackConfig: serverWebpackConfig, incomingWebpackBuildContext: serverBuildContext, - sentryBuildTimeOptions: {}, + sentryBuildTimeOptions: { buildTimeInstrumentation: false }, }); expect(findOrchestrionPlugin(finalWebpackConfig)).toBeUndefined(); @@ -727,12 +727,12 @@ describe('constructWebpackConfigFunction()', () => { }); describe('orchestrion runtime externals', () => { - it('prepends an externals handler that resolves runtime packages to absolute paths when diagnostics-channel injection is enabled', async () => { + it('prepends an externals handler that resolves runtime packages to absolute paths', async () => { const finalWebpackConfig = await materializeFinalWebpackConfig({ exportedNextConfig, incomingWebpackConfig: serverWebpackConfig, incomingWebpackBuildContext: serverBuildContext, - sentryBuildTimeOptions: { _experimental: { useDiagnosticsChannelInjection: true } }, + sentryBuildTimeOptions: {}, }); const externals = finalWebpackConfig.externals as ((data: { request?: string }) => Promise)[]; @@ -744,12 +744,12 @@ describe('constructWebpackConfigFunction()', () => { await expect(externals[0]({ request: 'some-other-package' })).resolves.toBeUndefined(); }); - it('does not touch `externals` when diagnostics-channel injection is not enabled', async () => { + it('does not touch `externals` when build-time instrumentation is turned off', async () => { const finalWebpackConfig = await materializeFinalWebpackConfig({ exportedNextConfig, incomingWebpackConfig: serverWebpackConfig, incomingWebpackBuildContext: serverBuildContext, - sentryBuildTimeOptions: {}, + sentryBuildTimeOptions: { buildTimeInstrumentation: false }, }); expect(finalWebpackConfig.externals).toBeUndefined(); @@ -760,7 +760,7 @@ describe('constructWebpackConfigFunction()', () => { exportedNextConfig, incomingWebpackConfig: serverWebpackConfig, incomingWebpackBuildContext: edgeBuildContext, - sentryBuildTimeOptions: { _experimental: { useDiagnosticsChannelInjection: true } }, + sentryBuildTimeOptions: {}, }); expect(finalWebpackConfig.externals).toBeUndefined(); diff --git a/packages/nextjs/test/config/withSentryConfig.test.ts b/packages/nextjs/test/config/withSentryConfig.test.ts index 0b0bd2ee50ed..6a226a564681 100644 --- a/packages/nextjs/test/config/withSentryConfig.test.ts +++ b/packages/nextjs/test/config/withSentryConfig.test.ts @@ -1,9 +1,21 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { + BUNDLE_SAFE_INSTRUMENTED_PACKAGES, + filterInstrumentedExternals, + ORCHESTRION_RUNTIME_EXTERNAL_PACKAGES, +} from '../../src/config/diagnosticsChannelInjection'; import * as util from '../../src/config/util'; import { DEFAULT_SERVER_EXTERNAL_PACKAGES } from '../../src/config/withSentryConfig'; import { defaultRuntimePhase, defaultsObject, exportedNextConfig, userNextConfig } from './fixtures'; import { materializeFinalNextConfig } from './testUtils'; +// Build-time instrumentation is on by default, so the bundle-safe packages are deliberately dropped +// from the externals defaults (the loader transforms them) and the orchestrion runtime is added. +const EXPECTED_DEFAULT_EXTERNALS = [ + ...filterInstrumentedExternals(DEFAULT_SERVER_EXTERNAL_PACKAGES, BUNDLE_SAFE_INSTRUMENTED_PACKAGES), + ...ORCHESTRION_RUNTIME_EXTERNAL_PACKAGES, +]; + describe('withSentryConfig', () => { it('includes expected properties', () => { const finalConfig = materializeFinalNextConfig(exportedNextConfig); @@ -30,7 +42,7 @@ describe('withSentryConfig', () => { expect(restOfFinalConfig).toEqual(restOfUserConfig); expect(webpack).toBeInstanceOf(Function); - expect(serverExternalPackages).toEqual(expect.arrayContaining(DEFAULT_SERVER_EXTERNAL_PACKAGES)); + expect(serverExternalPackages).toEqual(expect.arrayContaining(EXPECTED_DEFAULT_EXTERNALS)); }); it("works when user's overall config is a function", () => { @@ -44,7 +56,7 @@ describe('withSentryConfig', () => { expect(restOfFinalConfig).toEqual(restOfUserConfig); expect(webpack).toBeInstanceOf(Function); - expect(serverExternalPackages).toEqual(expect.arrayContaining(DEFAULT_SERVER_EXTERNAL_PACKAGES)); + expect(serverExternalPackages).toEqual(expect.arrayContaining(EXPECTED_DEFAULT_EXTERNALS)); }); it('correctly passes `phase` and `defaultConfig` through to functional `userNextConfig`', () => { @@ -90,7 +102,7 @@ describe('withSentryConfig', () => { const finalConfig = materializeFinalNextConfig(exportedNextConfig); expect(finalConfig.serverExternalPackages).toBeDefined(); - expect(finalConfig.serverExternalPackages).toEqual(expect.arrayContaining(DEFAULT_SERVER_EXTERNAL_PACKAGES)); + expect(finalConfig.serverExternalPackages).toEqual(expect.arrayContaining(EXPECTED_DEFAULT_EXTERNALS)); expect(finalConfig.experimental?.serverComponentsExternalPackages).toBeUndefined(); }); @@ -101,7 +113,7 @@ describe('withSentryConfig', () => { expect(finalConfig.serverExternalPackages).toBeUndefined(); expect(finalConfig.experimental?.serverComponentsExternalPackages).toBeDefined(); expect(finalConfig.experimental?.serverComponentsExternalPackages).toEqual( - expect.arrayContaining(DEFAULT_SERVER_EXTERNAL_PACKAGES), + expect.arrayContaining(EXPECTED_DEFAULT_EXTERNALS), ); }); @@ -114,7 +126,7 @@ describe('withSentryConfig', () => { serverExternalPackages: existingPackages, }); expect(config15.serverExternalPackages).toEqual( - expect.arrayContaining([...existingPackages, ...DEFAULT_SERVER_EXTERNAL_PACKAGES]), + expect.arrayContaining([...existingPackages, ...EXPECTED_DEFAULT_EXTERNALS]), ); vi.spyOn(util, 'getNextjsVersion').mockReturnValue('14.0.0'); @@ -125,7 +137,7 @@ describe('withSentryConfig', () => { }, }); expect(config14.experimental?.serverComponentsExternalPackages).toEqual( - expect.arrayContaining([...existingPackages, ...DEFAULT_SERVER_EXTERNAL_PACKAGES]), + expect.arrayContaining([...existingPackages, ...EXPECTED_DEFAULT_EXTERNALS]), ); }); }); From e862989f96953c3d5cffe3079b396fe131d687d1 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 31 Jul 2026 17:58:12 +0200 Subject: [PATCH 3/4] gate on disableSentryConfig --- .../withSentryConfig/getFinalConfigObjectBundlerUtils.ts | 7 ++++--- .../nextjs/test/config/diagnosticsChannelInjection.test.ts | 7 +++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectBundlerUtils.ts b/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectBundlerUtils.ts index dc1a946ff68e..2204c0dd483f 100644 --- a/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectBundlerUtils.ts +++ b/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectBundlerUtils.ts @@ -97,8 +97,9 @@ export function maybeConstructTurbopackConfig( /** * Resolves whether to wire up orchestrion build-time instrumentation. * - * Turbopack needs rule `condition`s (Next.js 16+) to run the transform. Without it, un-externalizing - * the bundle-safe packages would leave them bundled *and* uninstrumented, so keep the feature off. + * Only on when the transform can actually run: Turbopack needs rule `condition`s (Next.js 16+), and + * webpack needs Sentry's config. Otherwise un-externalizing the bundle-safe packages would leave + * them bundled *and* uninstrumented, so keep the feature off. */ export function resolveBuildTimeInstrumentationOption( userSentryOptions: SentryBuildOptions, @@ -113,7 +114,7 @@ export function resolveBuildTimeInstrumentationOption( return !!nextJsVersion && supportsTurbopackRuleCondition(nextJsVersion); } - return true; + return !userSentryOptions.webpack?.disableSentryConfig; } /** diff --git a/packages/nextjs/test/config/diagnosticsChannelInjection.test.ts b/packages/nextjs/test/config/diagnosticsChannelInjection.test.ts index a2c69aadfcbb..234b86b5d0ea 100644 --- a/packages/nextjs/test/config/diagnosticsChannelInjection.test.ts +++ b/packages/nextjs/test/config/diagnosticsChannelInjection.test.ts @@ -117,4 +117,11 @@ describe('resolveBuildTimeInstrumentationOption', () => { it('still applies on webpack when the Next.js version is unknown', () => { expect(resolveBuildTimeInstrumentationOption({}, webpack, undefined)).toBe(true); }); + + // Same reasoning as the Turbopack gate: without Sentry's webpack config there is no transform. + it("stays off on webpack when Sentry's webpack config is disabled", () => { + expect(resolveBuildTimeInstrumentationOption({ webpack: { disableSentryConfig: true } }, webpack, '15.0.0')).toBe( + false, + ); + }); }); From d71fdbe58d2066409e9dab16a85868e4c4eeeed0 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 31 Jul 2026 18:20:18 +0200 Subject: [PATCH 4/4] test fixes --- .../test/config/withSentryConfig.test.ts | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/packages/nextjs/test/config/withSentryConfig.test.ts b/packages/nextjs/test/config/withSentryConfig.test.ts index 6a226a564681..5a128716c5b5 100644 --- a/packages/nextjs/test/config/withSentryConfig.test.ts +++ b/packages/nextjs/test/config/withSentryConfig.test.ts @@ -11,6 +11,7 @@ import { materializeFinalNextConfig } from './testUtils'; // Build-time instrumentation is on by default, so the bundle-safe packages are deliberately dropped // from the externals defaults (the loader transforms them) and the orchestrion runtime is added. +// Asserted by exact equality, so a regression that left them external fails here. const EXPECTED_DEFAULT_EXTERNALS = [ ...filterInstrumentedExternals(DEFAULT_SERVER_EXTERNAL_PACKAGES, BUNDLE_SAFE_INSTRUMENTED_PACKAGES), ...ORCHESTRION_RUNTIME_EXTERNAL_PACKAGES, @@ -42,7 +43,7 @@ describe('withSentryConfig', () => { expect(restOfFinalConfig).toEqual(restOfUserConfig); expect(webpack).toBeInstanceOf(Function); - expect(serverExternalPackages).toEqual(expect.arrayContaining(EXPECTED_DEFAULT_EXTERNALS)); + expect(serverExternalPackages).toEqual(EXPECTED_DEFAULT_EXTERNALS); }); it("works when user's overall config is a function", () => { @@ -56,7 +57,7 @@ describe('withSentryConfig', () => { expect(restOfFinalConfig).toEqual(restOfUserConfig); expect(webpack).toBeInstanceOf(Function); - expect(serverExternalPackages).toEqual(expect.arrayContaining(EXPECTED_DEFAULT_EXTERNALS)); + expect(serverExternalPackages).toEqual(EXPECTED_DEFAULT_EXTERNALS); }); it('correctly passes `phase` and `defaultConfig` through to functional `userNextConfig`', () => { @@ -101,8 +102,7 @@ describe('withSentryConfig', () => { vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.0.0'); const finalConfig = materializeFinalNextConfig(exportedNextConfig); - expect(finalConfig.serverExternalPackages).toBeDefined(); - expect(finalConfig.serverExternalPackages).toEqual(expect.arrayContaining(EXPECTED_DEFAULT_EXTERNALS)); + expect(finalConfig.serverExternalPackages).toEqual(EXPECTED_DEFAULT_EXTERNALS); expect(finalConfig.experimental?.serverComponentsExternalPackages).toBeUndefined(); }); @@ -111,10 +111,7 @@ describe('withSentryConfig', () => { const finalConfig = materializeFinalNextConfig(exportedNextConfig); expect(finalConfig.serverExternalPackages).toBeUndefined(); - expect(finalConfig.experimental?.serverComponentsExternalPackages).toBeDefined(); - expect(finalConfig.experimental?.serverComponentsExternalPackages).toEqual( - expect.arrayContaining(EXPECTED_DEFAULT_EXTERNALS), - ); + expect(finalConfig.experimental?.serverComponentsExternalPackages).toEqual(EXPECTED_DEFAULT_EXTERNALS); }); it('preserves existing packages in both versions', () => { @@ -125,9 +122,7 @@ describe('withSentryConfig', () => { ...exportedNextConfig, serverExternalPackages: existingPackages, }); - expect(config15.serverExternalPackages).toEqual( - expect.arrayContaining([...existingPackages, ...EXPECTED_DEFAULT_EXTERNALS]), - ); + expect(config15.serverExternalPackages).toEqual([...existingPackages, ...EXPECTED_DEFAULT_EXTERNALS]); vi.spyOn(util, 'getNextjsVersion').mockReturnValue('14.0.0'); const config14 = materializeFinalNextConfig({ @@ -136,9 +131,10 @@ describe('withSentryConfig', () => { serverComponentsExternalPackages: existingPackages, }, }); - expect(config14.experimental?.serverComponentsExternalPackages).toEqual( - expect.arrayContaining([...existingPackages, ...EXPECTED_DEFAULT_EXTERNALS]), - ); + expect(config14.experimental?.serverComponentsExternalPackages).toEqual([ + ...existingPackages, + ...EXPECTED_DEFAULT_EXTERNALS, + ]); }); });