Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,4 @@ const nextConfig: NextConfig = {};

export default withSentryConfig(nextConfig, {
silent: true,
_experimental: { useDiagnosticsChannelInjection: true },
});
Original file line number Diff line number Diff line change
Expand Up @@ -123,15 +123,15 @@ 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'],
userSentryOptions: SentryBuildOptions | undefined,
nextJsVersion: string | undefined,
): TurbopackOptions['rules'] {
if (
!userSentryOptions?._experimental?.useDiagnosticsChannelInjection ||
userSentryOptions?.buildTimeInstrumentation === false ||
!nextJsVersion ||
!supportsTurbopackRuleCondition(nextJsVersion)
) {
Expand Down
23 changes: 11 additions & 12 deletions packages/nextjs/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
}>;

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/nextjs/src/config/webpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
5 changes: 0 additions & 5 deletions packages/nextjs/src/config/withSentryConfig/buildTime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
maybeSetUpRunAfterProductionCompileHook,
maybeWarnAboutUnsupportedRunAfterProductionCompileHook,
maybeWarnAboutUnsupportedTurbopack,
resolveBuildTimeInstrumentationOption,
resolveUseRunAfterProductionCompileHookOption,
} from './getFinalConfigObjectBundlerUtils';
import {
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -94,6 +94,29 @@ export function maybeConstructTurbopackConfig(
});
}

/**
* Resolves whether to wire up orchestrion build-time instrumentation.
*
* 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,
bundlerInfo: BundlerInfo,
nextJsVersion: string | undefined,
): boolean {
if (userSentryOptions.buildTimeInstrumentation === false) {
return false;
}

if (bundlerInfo.isTurbopack) {
return !!nextJsVersion && supportsTurbopackRuleCondition(nextJsVersion);
}

return !userSentryOptions.webpack?.disableSentryConfig;
}
Comment thread
chargome marked this conversation as resolved.

/**
* Resolves whether to use the `runAfterProductionCompile` hook based on options and bundler.
*/
Expand Down Expand Up @@ -232,19 +255,19 @@ export function maybeEnableTurbopackSourcemaps(
export function getServerExternalPackagesPatch(
incomingUserNextConfigObject: NextConfigObject,
nextMajor: number | undefined,
useDiagnosticsChannelInjection = false,
buildTimeInstrumentation = false,
): Partial<NextConfigObject> {
// 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 : []),
];
};

Expand Down
50 changes: 34 additions & 16 deletions packages/nextjs/test/config/diagnosticsChannelInjection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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 ?? [];
Expand All @@ -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');
Expand Down Expand Up @@ -90,20 +92,36 @@ 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.each([webpack, turbopack])('respects an explicit opt-out', bundlerInfo => {
expect(resolveBuildTimeInstrumentationOption({ buildTimeInstrumentation: false }, bundlerInfo, '16.0.0')).toBe(
false,
);
});

it('injects neither with the flag off', () => {
const nextConfig: NextConfigObject = {};
setUpBuildTimeVariables(nextConfig, {}, undefined);
// 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);
});

it('still applies on webpack when the Next.js version is unknown', () => {
expect(resolveBuildTimeInstrumentationOption({}, webpack, undefined)).toBe(true);
});

expect(nextConfig.env).not.toHaveProperty('_sentryUseDiagnosticsChannelInjection');
// 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,
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -1307,7 +1307,7 @@ describe('componentAnnotation with turbopackReactComponentAnnotation', () => {
});
});

describe('orchestrion diagnostics-channel injection', () => {
describe('orchestrion build-time instrumentation', () => {
function getOrchestrionOptions(result: ReturnType<typeof constructTurbopackConfig>): {
instrumentations: Array<{ module: { name: string; filePath: unknown } }>;
} {
Expand All @@ -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',
});

Expand All @@ -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',
});

Expand All @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -697,7 +697,7 @@ describe('constructWebpackConfigFunction()', () => {
exportedNextConfig,
incomingWebpackConfig: serverWebpackConfig,
incomingWebpackBuildContext: edgeBuildContext,
sentryBuildTimeOptions: { _experimental: { useDiagnosticsChannelInjection: true } },
sentryBuildTimeOptions: {},
});

expect(findOrchestrionPlugin(finalWebpackConfig)).toBeUndefined();
Expand All @@ -708,31 +708,31 @@ 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();
});
});

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<string | undefined>)[];
Expand All @@ -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();
Expand All @@ -760,7 +760,7 @@ describe('constructWebpackConfigFunction()', () => {
exportedNextConfig,
incomingWebpackConfig: serverWebpackConfig,
incomingWebpackBuildContext: edgeBuildContext,
sentryBuildTimeOptions: { _experimental: { useDiagnosticsChannelInjection: true } },
sentryBuildTimeOptions: {},
});

expect(finalWebpackConfig.externals).toBeUndefined();
Expand Down
Loading
Loading