Merged
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 numberDiff line numberDiff line change
Expand Up@@ -29,6 +29,8 @@ const NODE_EXPORTS_IGNORE = [
// factories for that same Node-runtime-only opt-in, so it isn't surfaced
// through the framework / serverless SDKs either.
'diagnosticsChannelInjectionIntegrations',
// Companion to the above two, same reasoning (Next.js re-exports it via `export * from '@sentry/node'`)
'isDiagnosticsChannelInjectionEnabled',
// Internal helper only needed within integrations (e.g. bunRuntimeMetricsIntegration)
'_INTERNAL_normalizeCollectionInterval',
];
Expand Down
1 change: 1 addition & 0 deletions packages/nextjs/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,6 +85,7 @@
"@sentry/node": "10.65.0",
"@sentry/opentelemetry": "10.65.0",
"@sentry/react": "10.65.0",
"@sentry/server-utils": "10.65.0",
"@sentry/vercel-edge": "10.65.0",
"@sentry/webpack-plugin": "^5.3.0",
"rollup": "^4.60.3",
Expand Down
22 changes: 22 additions & 0 deletions packages/nextjs/src/config/diagnosticsChannelInjection.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
/**
* Instrumented packages verified (via e2e) to bundle correctly, removed from Sentry's own
* `serverExternalPackages` defaults so the build-time loader can transform them. Everything else
* stays external and is instrumented by the runtime module hook instead. Deliberately an
* allowlist — bundling can break packages outright (e.g. `mysql` 2.x under Turbopack).
*/
export const BUNDLE_SAFE_INSTRUMENTED_PACKAGES = ['ioredis'];

/**
* The orchestrion runtime machinery must stay external — its parser breaks when bundled, which
* silently disables the runtime module hook.
*/
export const ORCHESTRION_RUNTIME_EXTERNAL_PACKAGES = [
'@apm-js-collab/tracing-hooks',
'@apm-js-collab/code-transformer',
];

/** Remove the given packages from a `serverExternalPackages` list. */
export function filterInstrumentedExternals(externals: string[], packagesToBundle: string[]): string[] {
const set = new Set(packagesToBundle);
return externals.filter(name => !set.has(name));
}
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
import { debug } from '@sentry/core';
import * as path from 'path';
import { getOrchestrionLoaderPath, getSentryInstrumentations } from '@sentry/server-utils/orchestrion/webpack';
import type { VercelCronsConfig } from '../../common/types';
import type { RouteManifest } from '../manifest/types';
import type { NextConfigObject, SentryBuildOptions, TurbopackMatcherWithRule, TurbopackOptions } from '../types';
import type {
JSONValue,
NextConfigObject,
SentryBuildOptions,
TurbopackMatcherWithRule,
TurbopackOptions,
} from '../types';
import { supportsNativeDebugIds, supportsTurbopackRuleCondition } from '../util';
import { generateValueInjectionRules } from './generateValueInjectionRules';

Expand DownExpand Up@@ -106,9 +113,42 @@ export function constructTurbopackConfig({
});
}

newConfig.rules = maybeAddOrchestrionRule(newConfig.rules, userSentryOptions, nextJsVersion);

return newConfig;
}

/**
* Adds the orchestrion code-transform loader rule when diagnostics-channel injection is enabled.
*/
function maybeAddOrchestrionRule(
rules: TurbopackOptions['rules'],
userSentryOptions: SentryBuildOptions | undefined,
nextJsVersion: string | undefined,
): TurbopackOptions['rules'] {
if (
!userSentryOptions?._experimental?.useDiagnosticsChannelInjection ||
!nextJsVersion ||
!supportsTurbopackRuleCondition(nextJsVersion)
) {
return rules;
}

return safelyAddTurbopackRule(rules, {
matcher: '*.{js,mjs,cjs}',
rule: {
condition: 'node',
loaders: [
{
loader: getOrchestrionLoaderPath(),
// `instrumentations` is JSON-serializable
options: { instrumentations: getSentryInstrumentations() as unknown as JSONValue[] },
},
],
},
});
}

/**
* Safely add a Turbopack rule to the existing rules.
*
Expand Down
15 changes: 14 additions & 1 deletion packages/nextjs/src/config/types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ type NextRewrite = {
destination: string;
};

interface WebpackPluginInstance {
export interface WebpackPluginInstance {
[index: string]: unknown;
apply: (compiler: unknown) => void;
}
Expand DownExpand Up@@ -765,6 +765,19 @@ 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. You must still call `Sentry.experimentalUseDiagnosticsChannelInjection()`
* in your server config to record spans.
*
* 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
7 changes: 7 additions & 0 deletions packages/nextjs/src/config/webpack.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,7 +20,9 @@ import type {
WebpackConfigObject,
WebpackConfigObjectWithModuleRules,
WebpackEntryProperty,
WebpackPluginInstance,
} from './types';
import { sentryOrchestrionWebpackPlugin } from '@sentry/server-utils/orchestrion/webpack';
import { getNextjsVersion, getPackageModules } from './util';
import type { VercelCronsConfigResult } from './withSentryConfig/getFinalConfigObjectUtils';

Expand DownExpand Up@@ -429,6 +431,11 @@ export function constructWebpackConfigFunction({
}),
);

// Orchestrion code-transform loader — Node server runtime only, never the edge compilation
Comment thread
andreiborza marked this conversation as resolved.
if (runtime === 'server' && userSentryOptions._experimental?.useDiagnosticsChannelInjection) {
newConfig.plugins.push(sentryOrchestrionWebpackPlugin() as WebpackPluginInstance);
}
Comment thread
chargome marked this conversation as resolved.

return newConfig;
};
}
Expand Down
9 changes: 9 additions & 0 deletions packages/nextjs/src/config/withSentryConfig/buildTime.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import * as childProcess from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import { getTracingHooksDirectory } from '@sentry/server-utils/orchestrion/webpack';
import type { NextConfigObject, SentryBuildOptions } from '../types';

/**
Expand DownExpand Up@@ -50,6 +51,14 @@ 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';
// Resolved here (where the SDK is a real on-disk package) and inlined, because the runtime
// module hook can't resolve the bare specifier from a bundled server chunk under pnpm.
buildTimeVariables._sentryOrchestrionTracingHooksDir = getTracingHooksDirectory();
}

if (basePath) {
buildTimeVariables._sentryBasePath = basePath;
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,9 +85,11 @@ export function getFinalConfigObject(

maybeEnableTurbopackSourcemaps(incomingUserNextConfigObject, userSentryOptions, bundlerInfo);

const useDiagnosticsChannelInjection = userSentryOptions._experimental?.useDiagnosticsChannelInjection ?? false;

return {
...incomingUserNextConfigObject,
...getServerExternalPackagesPatch(incomingUserNextConfigObject, nextMajor),
...getServerExternalPackagesPatch(incomingUserNextConfigObject, nextMajor, useDiagnosticsChannelInjection),
...getWebpackPatch({
incomingUserNextConfigObject,
userSentryOptions,
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
import {
BUNDLE_SAFE_INSTRUMENTED_PACKAGES,
filterInstrumentedExternals,
ORCHESTRION_RUNTIME_EXTERNAL_PACKAGES,
} from '../diagnosticsChannelInjection';
import { handleRunAfterProductionCompile } from '../handleRunAfterProductionCompile';
import type { RouteManifest } from '../manifest/types';
import { constructTurbopackConfig } from '../turbopack';
Expand DownExpand Up@@ -227,23 +232,32 @@ export function maybeEnableTurbopackSourcemaps(
export function getServerExternalPackagesPatch(
incomingUserNextConfigObject: NextConfigObject,
nextMajor: number | undefined,
useDiagnosticsChannelInjection = false,
): Partial<NextConfigObject> {
// Diagnostics-channel injection: 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
? filterInstrumentedExternals(DEFAULT_SERVER_EXTERNAL_PACKAGES, BUNDLE_SAFE_INSTRUMENTED_PACKAGES)
: DEFAULT_SERVER_EXTERNAL_PACKAGES;
return [
...(userProvided || []),
...defaults,
...(useDiagnosticsChannelInjection ? ORCHESTRION_RUNTIME_EXTERNAL_PACKAGES : []),
];
};

if (nextMajor && nextMajor >= 15) {
return {
serverExternalPackages: [
...(incomingUserNextConfigObject.serverExternalPackages || []),
...DEFAULT_SERVER_EXTERNAL_PACKAGES,
],
};
return { serverExternalPackages: mergeExternals(incomingUserNextConfigObject.serverExternalPackages) };
}

return {
experimental: {
...incomingUserNextConfigObject.experimental,
serverComponentsExternalPackages: [
...(incomingUserNextConfigObject.experimental?.serverComponentsExternalPackages || []),
...DEFAULT_SERVER_EXTERNAL_PACKAGES,
],
serverComponentsExternalPackages: mergeExternals(
incomingUserNextConfigObject.experimental?.serverComponentsExternalPackages,
),
},
};
}
Expand Down
37 changes: 36 additions & 1 deletion packages/nextjs/src/server/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,13 @@ import {
SEMANTIC_ATTRIBUTE_SENTRY_OP,
} from '@sentry/core';
import type { NodeClient, NodeOptions } from '@sentry/node';
import { getDefaultIntegrations, httpIntegration, init as nodeInit } from '@sentry/node';
import {
experimentalUseDiagnosticsChannelInjection as nodeExperimentalUseDiagnosticsChannelInjection,
getDefaultIntegrations,
httpIntegration,
init as nodeInit,
isDiagnosticsChannelInjectionEnabled,
} from '@sentry/node';
import { DEBUG_BUILD } from '../common/debug-build';
import { devErrorSymbolicationEventProcessor } from '../common/devErrorSymbolicationEventProcessor';
import { getVercelEnv } from '../common/getVercelEnv';
Expand DownExpand Up@@ -41,8 +47,26 @@ export { startSpan, startSpanManual, startInactiveSpan } from '../common/utils/n
const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
_sentryRewriteFramesDistDir?: string;
_sentryRelease?: string;
_sentryUseDiagnosticsChannelInjection?: string;
_sentryOrchestrionTracingHooksDir?: string;
};

/**
* EXPERIMENTAL: Next.js-aware variant of `Sentry.experimentalUseDiagnosticsChannelInjection()`
* from `@sentry/node` (see its docs for behavior and caveats).
*
* Next.js bundles the SDK into the server build, from where the runtime module hook can't resolve
* the `@apm-js-collab/tracing-hooks` bare specifier under isolated installs (pnpm). This variant
* points the hook at the package location that `withSentryConfig` resolved at build time.
*
* @experimental May change or be removed in any release.
*/
export function experimentalUseDiagnosticsChannelInjection(): void {
const tracingHooksDir =
process.env._sentryOrchestrionTracingHooksDir || globalWithInjectedValues._sentryOrchestrionTracingHooksDir;
nodeExperimentalUseDiagnosticsChannelInjection(tracingHooksDir ? { tracingHooksDir } : undefined);
}

// Call at module level so `next build` prerender workers still register the runner without `init`
prepareSafeIdGeneratorContext();

Expand DownExpand Up@@ -138,6 +162,17 @@ export function init(options: NodeOptions): NodeClient | undefined {
customDefaultIntegrations.push(distDirRewriteFramesIntegration({ distDirName }));
}

// The build wired the orchestrion loader but the runtime opt-in is missing → no DB spans.
const useDiagnosticsChannelInjection =
process.env._sentryUseDiagnosticsChannelInjection || globalWithInjectedValues._sentryUseDiagnosticsChannelInjection;
if (DEBUG_BUILD && useDiagnosticsChannelInjection && !isDiagnosticsChannelInjectionEnabled()) {
debug.warn(
'[@sentry/nextjs] `useDiagnosticsChannelInjection` is enabled in `withSentryConfig`, but ' +
'`Sentry.experimentalUseDiagnosticsChannelInjection()` was not called before `Sentry.init()`. ' +
'Server DB spans will not be recorded.',
);
Comment thread
chargome marked this conversation as resolved.
}

// Detect if running on OpenNext/Cloudflare and get runtime config
const cloudflareConfig = getCloudflareRuntimeConfig();

Expand Down
73 changes: 73 additions & 0 deletions packages/nextjs/test/config/diagnosticsChannelInjection.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
import { describe, expect, it } from 'vitest';
import {
BUNDLE_SAFE_INSTRUMENTED_PACKAGES,
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';

describe('filterInstrumentedExternals', () => {
it('removes the given packages, keeps the rest', () => {
expect(
filterInstrumentedExternals(['express', 'pg', 'pg-pool', 'ioredis', 'mongodb'], ['pg', 'pg-pool', 'ioredis']),
).toEqual(['express', 'mongodb']);
});

it('is a no-op with an empty bundle list', () => {
expect(filterInstrumentedExternals(['express', 'pg'], [])).toEqual(['express', 'pg']);
});
});

describe('getServerExternalPackagesPatch (diagnostics-channel injection)', () => {
it('keeps everything external except the bundle-safe allowlist, and adds the runtime machinery', () => {
const patch = getServerExternalPackagesPatch({}, 16, true);
const externals = patch.serverExternalPackages ?? [];

// Only the verified bundle-safe packages leave the external list (→ build-time loader).
for (const name of BUNDLE_SAFE_INSTRUMENTED_PACKAGES) {
expect(externals).not.toContain(name);
}
// Other instrumented packages stay external (→ runtime module hook).
expect(externals).toContain('mysql');
expect(externals).toContain('pg');
expect(externals).toContain('pg-pool');
// The orchestrion machinery must be external for the runtime hook to work.
expect(externals).toContain('@apm-js-collab/tracing-hooks');
expect(externals).toContain('@apm-js-collab/code-transformer');
});

it('respects user-provided externals even for bundle-safe packages', () => {
const patch = getServerExternalPackagesPatch({ serverExternalPackages: ['ioredis'] }, 16, true);
expect(patch.serverExternalPackages).toContain('ioredis');
});

it('is unchanged with the flag off', () => {
const patch = getServerExternalPackagesPatch({}, 16, false);
const externals = patch.serverExternalPackages ?? [];
expect(externals).toContain('ioredis');
expect(externals).toContain('mysql');
expect(externals).not.toContain('@apm-js-collab/tracing-hooks');
});
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feat lacks integration or E2E

Medium Severity

This feature PR adds unit and webpack config tests but no integration or E2E test in the diff, though the description references separate e2e verification. Review guidelines expect at least one integration or E2E test for feat changes.

Fix in CursorFix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit a3a3595. Configure here.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handled in #22080


describe('setUpBuildTimeVariables (diagnostics-channel injection)', () => {
it('injects the flag marker and the tracing-hooks location', () => {
const nextConfig: NextConfigObject = {};
setUpBuildTimeVariables(nextConfig, { _experimental: { useDiagnosticsChannelInjection: true } }, undefined);

expect(nextConfig.env).toMatchObject({
_sentryUseDiagnosticsChannelInjection: 'true',
// The runtime module hook joins subpaths onto this, so it must be an absolute directory.
_sentryOrchestrionTracingHooksDir: expect.stringMatching(/@apm-js-collab[/+]tracing-hooks/),
});
});

it('injects neither with the flag off', () => {
const nextConfig: NextConfigObject = {};
setUpBuildTimeVariables(nextConfig, {}, undefined);

expect(nextConfig.env).not.toHaveProperty('_sentryUseDiagnosticsChannelInjection');
expect(nextConfig.env).not.toHaveProperty('_sentryOrchestrionTracingHooksDir');
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
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 numberDiff line numberDiff line change
Expand Up@@ -29,6 +29,8 @@ const NODE_EXPORTS_IGNORE = [
// factories for that same Node-runtime-only opt-in, so it isn't surfaced
// through the framework / serverless SDKs either.
'diagnosticsChannelInjectionIntegrations',
// Companion to the above two, same reasoning (Next.js re-exports it via `export * from '@sentry/node'`)
'isDiagnosticsChannelInjectionEnabled',
// Internal helper only needed within integrations (e.g. bunRuntimeMetricsIntegration)
'_INTERNAL_normalizeCollectionInterval',
];
Expand Down
1 change: 1 addition & 0 deletions packages/nextjs/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,6 +85,7 @@
"@sentry/node": "10.65.0",
"@sentry/opentelemetry": "10.65.0",
"@sentry/react": "10.65.0",
"@sentry/server-utils": "10.65.0",
"@sentry/vercel-edge": "10.65.0",
"@sentry/webpack-plugin": "^5.3.0",
"rollup": "^4.60.3",
Expand Down
22 changes: 22 additions & 0 deletions packages/nextjs/src/config/diagnosticsChannelInjection.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
/**
* Instrumented packages verified (via e2e) to bundle correctly, removed from Sentry's own
* `serverExternalPackages` defaults so the build-time loader can transform them. Everything else
* stays external and is instrumented by the runtime module hook instead. Deliberately an
* allowlist — bundling can break packages outright (e.g. `mysql` 2.x under Turbopack).
*/
export const BUNDLE_SAFE_INSTRUMENTED_PACKAGES = ['ioredis'];

/**
* The orchestrion runtime machinery must stay external — its parser breaks when bundled, which
* silently disables the runtime module hook.
*/
export const ORCHESTRION_RUNTIME_EXTERNAL_PACKAGES = [
'@apm-js-collab/tracing-hooks',
'@apm-js-collab/code-transformer',
];

/** Remove the given packages from a `serverExternalPackages` list. */
export function filterInstrumentedExternals(externals: string[], packagesToBundle: string[]): string[] {
const set = new Set(packagesToBundle);
return externals.filter(name => !set.has(name));
}
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
import { debug } from '@sentry/core';
import * as path from 'path';
import { getOrchestrionLoaderPath, getSentryInstrumentations } from '@sentry/server-utils/orchestrion/webpack';
import type { VercelCronsConfig } from '../../common/types';
import type { RouteManifest } from '../manifest/types';
import type { NextConfigObject, SentryBuildOptions, TurbopackMatcherWithRule, TurbopackOptions } from '../types';
import type {
JSONValue,
NextConfigObject,
SentryBuildOptions,
TurbopackMatcherWithRule,
TurbopackOptions,
} from '../types';
import { supportsNativeDebugIds, supportsTurbopackRuleCondition } from '../util';
import { generateValueInjectionRules } from './generateValueInjectionRules';

Expand DownExpand Up@@ -106,9 +113,42 @@ export function constructTurbopackConfig({
});
}

newConfig.rules = maybeAddOrchestrionRule(newConfig.rules, userSentryOptions, nextJsVersion);

return newConfig;
}

/**
* Adds the orchestrion code-transform loader rule when diagnostics-channel injection is enabled.
*/
function maybeAddOrchestrionRule(
rules: TurbopackOptions['rules'],
userSentryOptions: SentryBuildOptions | undefined,
nextJsVersion: string | undefined,
): TurbopackOptions['rules'] {
if (
!userSentryOptions?._experimental?.useDiagnosticsChannelInjection ||
!nextJsVersion ||
!supportsTurbopackRuleCondition(nextJsVersion)
) {
return rules;
}

return safelyAddTurbopackRule(rules, {
matcher: '*.{js,mjs,cjs}',
rule: {
condition: 'node',
loaders: [
{
loader: getOrchestrionLoaderPath(),
// `instrumentations` is JSON-serializable
options: { instrumentations: getSentryInstrumentations() as unknown as JSONValue[] },
},
],
},
});
}

/**
* Safely add a Turbopack rule to the existing rules.
*
Expand Down
15 changes: 14 additions & 1 deletion packages/nextjs/src/config/types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ type NextRewrite = {
destination: string;
};

interface WebpackPluginInstance {
export interface WebpackPluginInstance {
[index: string]: unknown;
apply: (compiler: unknown) => void;
}
Expand DownExpand Up@@ -765,6 +765,19 @@ 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. You must still call `Sentry.experimentalUseDiagnosticsChannelInjection()`
* in your server config to record spans.
*
* 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
7 changes: 7 additions & 0 deletions packages/nextjs/src/config/webpack.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,7 +20,9 @@ import type {
WebpackConfigObject,
WebpackConfigObjectWithModuleRules,
WebpackEntryProperty,
WebpackPluginInstance,
} from './types';
import { sentryOrchestrionWebpackPlugin } from '@sentry/server-utils/orchestrion/webpack';
import { getNextjsVersion, getPackageModules } from './util';
import type { VercelCronsConfigResult } from './withSentryConfig/getFinalConfigObjectUtils';

Expand DownExpand Up@@ -429,6 +431,11 @@ export function constructWebpackConfigFunction({
}),
);

// Orchestrion code-transform loader — Node server runtime only, never the edge compilation
Comment thread
andreiborza marked this conversation as resolved.
if (runtime === 'server' && userSentryOptions._experimental?.useDiagnosticsChannelInjection) {
newConfig.plugins.push(sentryOrchestrionWebpackPlugin() as WebpackPluginInstance);
}
Comment thread
chargome marked this conversation as resolved.

return newConfig;
};
}
Expand Down
9 changes: 9 additions & 0 deletions packages/nextjs/src/config/withSentryConfig/buildTime.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import * as childProcess from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import { getTracingHooksDirectory } from '@sentry/server-utils/orchestrion/webpack';
import type { NextConfigObject, SentryBuildOptions } from '../types';

/**
Expand DownExpand Up@@ -50,6 +51,14 @@ 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';
// Resolved here (where the SDK is a real on-disk package) and inlined, because the runtime
// module hook can't resolve the bare specifier from a bundled server chunk under pnpm.
buildTimeVariables._sentryOrchestrionTracingHooksDir = getTracingHooksDirectory();
}

if (basePath) {
buildTimeVariables._sentryBasePath = basePath;
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,9 +85,11 @@ export function getFinalConfigObject(

maybeEnableTurbopackSourcemaps(incomingUserNextConfigObject, userSentryOptions, bundlerInfo);

const useDiagnosticsChannelInjection = userSentryOptions._experimental?.useDiagnosticsChannelInjection ?? false;

return {
...incomingUserNextConfigObject,
...getServerExternalPackagesPatch(incomingUserNextConfigObject, nextMajor),
...getServerExternalPackagesPatch(incomingUserNextConfigObject, nextMajor, useDiagnosticsChannelInjection),
...getWebpackPatch({
incomingUserNextConfigObject,
userSentryOptions,
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
import {
BUNDLE_SAFE_INSTRUMENTED_PACKAGES,
filterInstrumentedExternals,
ORCHESTRION_RUNTIME_EXTERNAL_PACKAGES,
} from '../diagnosticsChannelInjection';
import { handleRunAfterProductionCompile } from '../handleRunAfterProductionCompile';
import type { RouteManifest } from '../manifest/types';
import { constructTurbopackConfig } from '../turbopack';
Expand DownExpand Up@@ -227,23 +232,32 @@ export function maybeEnableTurbopackSourcemaps(
export function getServerExternalPackagesPatch(
incomingUserNextConfigObject: NextConfigObject,
nextMajor: number | undefined,
useDiagnosticsChannelInjection = false,
): Partial<NextConfigObject> {
// Diagnostics-channel injection: 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
? filterInstrumentedExternals(DEFAULT_SERVER_EXTERNAL_PACKAGES, BUNDLE_SAFE_INSTRUMENTED_PACKAGES)
: DEFAULT_SERVER_EXTERNAL_PACKAGES;
return [
...(userProvided || []),
...defaults,
...(useDiagnosticsChannelInjection ? ORCHESTRION_RUNTIME_EXTERNAL_PACKAGES : []),
];
};

if (nextMajor && nextMajor >= 15) {
return {
serverExternalPackages: [
...(incomingUserNextConfigObject.serverExternalPackages || []),
...DEFAULT_SERVER_EXTERNAL_PACKAGES,
],
};
return { serverExternalPackages: mergeExternals(incomingUserNextConfigObject.serverExternalPackages) };
}

return {
experimental: {
...incomingUserNextConfigObject.experimental,
serverComponentsExternalPackages: [
...(incomingUserNextConfigObject.experimental?.serverComponentsExternalPackages || []),
...DEFAULT_SERVER_EXTERNAL_PACKAGES,
],
serverComponentsExternalPackages: mergeExternals(
incomingUserNextConfigObject.experimental?.serverComponentsExternalPackages,
),
},
};
}
Expand Down
37 changes: 36 additions & 1 deletion packages/nextjs/src/server/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,13 @@ import {
SEMANTIC_ATTRIBUTE_SENTRY_OP,
} from '@sentry/core';
import type { NodeClient, NodeOptions } from '@sentry/node';
import { getDefaultIntegrations, httpIntegration, init as nodeInit } from '@sentry/node';
import {
experimentalUseDiagnosticsChannelInjection as nodeExperimentalUseDiagnosticsChannelInjection,
getDefaultIntegrations,
httpIntegration,
init as nodeInit,
isDiagnosticsChannelInjectionEnabled,
} from '@sentry/node';
import { DEBUG_BUILD } from '../common/debug-build';
import { devErrorSymbolicationEventProcessor } from '../common/devErrorSymbolicationEventProcessor';
import { getVercelEnv } from '../common/getVercelEnv';
Expand DownExpand Up@@ -41,8 +47,26 @@ export { startSpan, startSpanManual, startInactiveSpan } from '../common/utils/n
const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
_sentryRewriteFramesDistDir?: string;
_sentryRelease?: string;
_sentryUseDiagnosticsChannelInjection?: string;
_sentryOrchestrionTracingHooksDir?: string;
};

/**
* EXPERIMENTAL: Next.js-aware variant of `Sentry.experimentalUseDiagnosticsChannelInjection()`
* from `@sentry/node` (see its docs for behavior and caveats).
*
* Next.js bundles the SDK into the server build, from where the runtime module hook can't resolve
* the `@apm-js-collab/tracing-hooks` bare specifier under isolated installs (pnpm). This variant
* points the hook at the package location that `withSentryConfig` resolved at build time.
*
* @experimental May change or be removed in any release.
*/
export function experimentalUseDiagnosticsChannelInjection(): void {
const tracingHooksDir =
process.env._sentryOrchestrionTracingHooksDir || globalWithInjectedValues._sentryOrchestrionTracingHooksDir;
nodeExperimentalUseDiagnosticsChannelInjection(tracingHooksDir ? { tracingHooksDir } : undefined);
}

// Call at module level so `next build` prerender workers still register the runner without `init`
prepareSafeIdGeneratorContext();

Expand DownExpand Up@@ -138,6 +162,17 @@ export function init(options: NodeOptions): NodeClient | undefined {
customDefaultIntegrations.push(distDirRewriteFramesIntegration({ distDirName }));
}

// The build wired the orchestrion loader but the runtime opt-in is missing → no DB spans.
const useDiagnosticsChannelInjection =
process.env._sentryUseDiagnosticsChannelInjection || globalWithInjectedValues._sentryUseDiagnosticsChannelInjection;
if (DEBUG_BUILD && useDiagnosticsChannelInjection && !isDiagnosticsChannelInjectionEnabled()) {
debug.warn(
'[@sentry/nextjs] `useDiagnosticsChannelInjection` is enabled in `withSentryConfig`, but ' +
'`Sentry.experimentalUseDiagnosticsChannelInjection()` was not called before `Sentry.init()`. ' +
'Server DB spans will not be recorded.',
);
Comment thread
chargome marked this conversation as resolved.
}

// Detect if running on OpenNext/Cloudflare and get runtime config
const cloudflareConfig = getCloudflareRuntimeConfig();

Expand Down
73 changes: 73 additions & 0 deletions packages/nextjs/test/config/diagnosticsChannelInjection.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
import { describe, expect, it } from 'vitest';
import {
BUNDLE_SAFE_INSTRUMENTED_PACKAGES,
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';

describe('filterInstrumentedExternals', () => {
it('removes the given packages, keeps the rest', () => {
expect(
filterInstrumentedExternals(['express', 'pg', 'pg-pool', 'ioredis', 'mongodb'], ['pg', 'pg-pool', 'ioredis']),
).toEqual(['express', 'mongodb']);
});

it('is a no-op with an empty bundle list', () => {
expect(filterInstrumentedExternals(['express', 'pg'], [])).toEqual(['express', 'pg']);
});
});

describe('getServerExternalPackagesPatch (diagnostics-channel injection)', () => {
it('keeps everything external except the bundle-safe allowlist, and adds the runtime machinery', () => {
const patch = getServerExternalPackagesPatch({}, 16, true);
const externals = patch.serverExternalPackages ?? [];

// Only the verified bundle-safe packages leave the external list (→ build-time loader).
for (const name of BUNDLE_SAFE_INSTRUMENTED_PACKAGES) {
expect(externals).not.toContain(name);
}
// Other instrumented packages stay external (→ runtime module hook).
expect(externals).toContain('mysql');
expect(externals).toContain('pg');
expect(externals).toContain('pg-pool');
// The orchestrion machinery must be external for the runtime hook to work.
expect(externals).toContain('@apm-js-collab/tracing-hooks');
expect(externals).toContain('@apm-js-collab/code-transformer');
});

it('respects user-provided externals even for bundle-safe packages', () => {
const patch = getServerExternalPackagesPatch({ serverExternalPackages: ['ioredis'] }, 16, true);
expect(patch.serverExternalPackages).toContain('ioredis');
});

it('is unchanged with the flag off', () => {
const patch = getServerExternalPackagesPatch({}, 16, false);
const externals = patch.serverExternalPackages ?? [];
expect(externals).toContain('ioredis');
expect(externals).toContain('mysql');
expect(externals).not.toContain('@apm-js-collab/tracing-hooks');
});
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feat lacks integration or E2E

Medium Severity

This feature PR adds unit and webpack config tests but no integration or E2E test in the diff, though the description references separate e2e verification. Review guidelines expect at least one integration or E2E test for feat changes.

Fix in CursorFix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit a3a3595. Configure here.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handled in #22080


describe('setUpBuildTimeVariables (diagnostics-channel injection)', () => {
it('injects the flag marker and the tracing-hooks location', () => {
const nextConfig: NextConfigObject = {};
setUpBuildTimeVariables(nextConfig, { _experimental: { useDiagnosticsChannelInjection: true } }, undefined);

expect(nextConfig.env).toMatchObject({
_sentryUseDiagnosticsChannelInjection: 'true',
// The runtime module hook joins subpaths onto this, so it must be an absolute directory.
_sentryOrchestrionTracingHooksDir: expect.stringMatching(/@apm-js-collab[/+]tracing-hooks/),
});
});

it('injects neither with the flag off', () => {
const nextConfig: NextConfigObject = {};
setUpBuildTimeVariables(nextConfig, {}, undefined);

expect(nextConfig.env).not.toHaveProperty('_sentryUseDiagnosticsChannelInjection');
expect(nextConfig.env).not.toHaveProperty('_sentryOrchestrionTracingHooksDir');
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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 numberDiff line numberDiff line change
Expand Up@@ -29,6 +29,8 @@ const NODE_EXPORTS_IGNORE = [
// factories for that same Node-runtime-only opt-in, so it isn't surfaced
// through the framework / serverless SDKs either.
'diagnosticsChannelInjectionIntegrations',
// Companion to the above two, same reasoning (Next.js re-exports it via `export * from '@sentry/node'`)
'isDiagnosticsChannelInjectionEnabled',
// Internal helper only needed within integrations (e.g. bunRuntimeMetricsIntegration)
'_INTERNAL_normalizeCollectionInterval',
];
Expand Down
1 change: 1 addition & 0 deletions packages/nextjs/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,6 +85,7 @@
"@sentry/node": "10.65.0",
"@sentry/opentelemetry": "10.65.0",
"@sentry/react": "10.65.0",
"@sentry/server-utils": "10.65.0",
"@sentry/vercel-edge": "10.65.0",
"@sentry/webpack-plugin": "^5.3.0",
"rollup": "^4.60.3",
Expand Down
22 changes: 22 additions & 0 deletions packages/nextjs/src/config/diagnosticsChannelInjection.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
/**
* Instrumented packages verified (via e2e) to bundle correctly, removed from Sentry's own
* `serverExternalPackages` defaults so the build-time loader can transform them. Everything else
* stays external and is instrumented by the runtime module hook instead. Deliberately an
* allowlist — bundling can break packages outright (e.g. `mysql` 2.x under Turbopack).
*/
export const BUNDLE_SAFE_INSTRUMENTED_PACKAGES = ['ioredis'];

/**
* The orchestrion runtime machinery must stay external — its parser breaks when bundled, which
* silently disables the runtime module hook.
*/
export const ORCHESTRION_RUNTIME_EXTERNAL_PACKAGES = [
'@apm-js-collab/tracing-hooks',
'@apm-js-collab/code-transformer',
];

/** Remove the given packages from a `serverExternalPackages` list. */
export function filterInstrumentedExternals(externals: string[], packagesToBundle: string[]): string[] {
const set = new Set(packagesToBundle);
return externals.filter(name => !set.has(name));
}
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
import { debug } from '@sentry/core';
import * as path from 'path';
import { getOrchestrionLoaderPath, getSentryInstrumentations } from '@sentry/server-utils/orchestrion/webpack';
import type { VercelCronsConfig } from '../../common/types';
import type { RouteManifest } from '../manifest/types';
import type { NextConfigObject, SentryBuildOptions, TurbopackMatcherWithRule, TurbopackOptions } from '../types';
import type {
JSONValue,
NextConfigObject,
SentryBuildOptions,
TurbopackMatcherWithRule,
TurbopackOptions,
} from '../types';
import { supportsNativeDebugIds, supportsTurbopackRuleCondition } from '../util';
import { generateValueInjectionRules } from './generateValueInjectionRules';

Expand DownExpand Up@@ -106,9 +113,42 @@ export function constructTurbopackConfig({
});
}

newConfig.rules = maybeAddOrchestrionRule(newConfig.rules, userSentryOptions, nextJsVersion);

return newConfig;
}

/**
* Adds the orchestrion code-transform loader rule when diagnostics-channel injection is enabled.
*/
function maybeAddOrchestrionRule(
rules: TurbopackOptions['rules'],
userSentryOptions: SentryBuildOptions | undefined,
nextJsVersion: string | undefined,
): TurbopackOptions['rules'] {
if (
!userSentryOptions?._experimental?.useDiagnosticsChannelInjection ||
!nextJsVersion ||
!supportsTurbopackRuleCondition(nextJsVersion)
) {
return rules;
}

return safelyAddTurbopackRule(rules, {
matcher: '*.{js,mjs,cjs}',
rule: {
condition: 'node',
loaders: [
{
loader: getOrchestrionLoaderPath(),
// `instrumentations` is JSON-serializable
options: { instrumentations: getSentryInstrumentations() as unknown as JSONValue[] },
},
],
},
});
}

/**
* Safely add a Turbopack rule to the existing rules.
*
Expand Down
15 changes: 14 additions & 1 deletion packages/nextjs/src/config/types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ type NextRewrite = {
destination: string;
};

interface WebpackPluginInstance {
export interface WebpackPluginInstance {
[index: string]: unknown;
apply: (compiler: unknown) => void;
}
Expand DownExpand Up@@ -765,6 +765,19 @@ 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. You must still call `Sentry.experimentalUseDiagnosticsChannelInjection()`
* in your server config to record spans.
*
* 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
7 changes: 7 additions & 0 deletions packages/nextjs/src/config/webpack.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,7 +20,9 @@ import type {
WebpackConfigObject,
WebpackConfigObjectWithModuleRules,
WebpackEntryProperty,
WebpackPluginInstance,
} from './types';
import { sentryOrchestrionWebpackPlugin } from '@sentry/server-utils/orchestrion/webpack';
import { getNextjsVersion, getPackageModules } from './util';
import type { VercelCronsConfigResult } from './withSentryConfig/getFinalConfigObjectUtils';

Expand DownExpand Up@@ -429,6 +431,11 @@ export function constructWebpackConfigFunction({
}),
);

// Orchestrion code-transform loader — Node server runtime only, never the edge compilation
Comment thread
andreiborza marked this conversation as resolved.
if (runtime === 'server' && userSentryOptions._experimental?.useDiagnosticsChannelInjection) {
newConfig.plugins.push(sentryOrchestrionWebpackPlugin() as WebpackPluginInstance);
}
Comment thread
chargome marked this conversation as resolved.

return newConfig;
};
}
Expand Down
9 changes: 9 additions & 0 deletions packages/nextjs/src/config/withSentryConfig/buildTime.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import * as childProcess from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import { getTracingHooksDirectory } from '@sentry/server-utils/orchestrion/webpack';
import type { NextConfigObject, SentryBuildOptions } from '../types';

/**
Expand DownExpand Up@@ -50,6 +51,14 @@ 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';
// Resolved here (where the SDK is a real on-disk package) and inlined, because the runtime
// module hook can't resolve the bare specifier from a bundled server chunk under pnpm.
buildTimeVariables._sentryOrchestrionTracingHooksDir = getTracingHooksDirectory();
}

if (basePath) {
buildTimeVariables._sentryBasePath = basePath;
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,9 +85,11 @@ export function getFinalConfigObject(

maybeEnableTurbopackSourcemaps(incomingUserNextConfigObject, userSentryOptions, bundlerInfo);

const useDiagnosticsChannelInjection = userSentryOptions._experimental?.useDiagnosticsChannelInjection ?? false;

return {
...incomingUserNextConfigObject,
...getServerExternalPackagesPatch(incomingUserNextConfigObject, nextMajor),
...getServerExternalPackagesPatch(incomingUserNextConfigObject, nextMajor, useDiagnosticsChannelInjection),
...getWebpackPatch({
incomingUserNextConfigObject,
userSentryOptions,
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
import {
BUNDLE_SAFE_INSTRUMENTED_PACKAGES,
filterInstrumentedExternals,
ORCHESTRION_RUNTIME_EXTERNAL_PACKAGES,
} from '../diagnosticsChannelInjection';
import { handleRunAfterProductionCompile } from '../handleRunAfterProductionCompile';
import type { RouteManifest } from '../manifest/types';
import { constructTurbopackConfig } from '../turbopack';
Expand DownExpand Up@@ -227,23 +232,32 @@ export function maybeEnableTurbopackSourcemaps(
export function getServerExternalPackagesPatch(
incomingUserNextConfigObject: NextConfigObject,
nextMajor: number | undefined,
useDiagnosticsChannelInjection = false,
): Partial<NextConfigObject> {
// Diagnostics-channel injection: 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
? filterInstrumentedExternals(DEFAULT_SERVER_EXTERNAL_PACKAGES, BUNDLE_SAFE_INSTRUMENTED_PACKAGES)
: DEFAULT_SERVER_EXTERNAL_PACKAGES;
return [
...(userProvided || []),
...defaults,
...(useDiagnosticsChannelInjection ? ORCHESTRION_RUNTIME_EXTERNAL_PACKAGES : []),
];
};

if (nextMajor && nextMajor >= 15) {
return {
serverExternalPackages: [
...(incomingUserNextConfigObject.serverExternalPackages || []),
...DEFAULT_SERVER_EXTERNAL_PACKAGES,
],
};
return { serverExternalPackages: mergeExternals(incomingUserNextConfigObject.serverExternalPackages) };
}

return {
experimental: {
...incomingUserNextConfigObject.experimental,
serverComponentsExternalPackages: [
...(incomingUserNextConfigObject.experimental?.serverComponentsExternalPackages || []),
...DEFAULT_SERVER_EXTERNAL_PACKAGES,
],
serverComponentsExternalPackages: mergeExternals(
incomingUserNextConfigObject.experimental?.serverComponentsExternalPackages,
),
},
};
}
Expand Down
37 changes: 36 additions & 1 deletion packages/nextjs/src/server/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,13 @@ import {
SEMANTIC_ATTRIBUTE_SENTRY_OP,
} from '@sentry/core';
import type { NodeClient, NodeOptions } from '@sentry/node';
import { getDefaultIntegrations, httpIntegration, init as nodeInit } from '@sentry/node';
import {
experimentalUseDiagnosticsChannelInjection as nodeExperimentalUseDiagnosticsChannelInjection,
getDefaultIntegrations,
httpIntegration,
init as nodeInit,
isDiagnosticsChannelInjectionEnabled,
} from '@sentry/node';
import { DEBUG_BUILD } from '../common/debug-build';
import { devErrorSymbolicationEventProcessor } from '../common/devErrorSymbolicationEventProcessor';
import { getVercelEnv } from '../common/getVercelEnv';
Expand DownExpand Up@@ -41,8 +47,26 @@ export { startSpan, startSpanManual, startInactiveSpan } from '../common/utils/n
const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
_sentryRewriteFramesDistDir?: string;
_sentryRelease?: string;
_sentryUseDiagnosticsChannelInjection?: string;
_sentryOrchestrionTracingHooksDir?: string;
};

/**
* EXPERIMENTAL: Next.js-aware variant of `Sentry.experimentalUseDiagnosticsChannelInjection()`
* from `@sentry/node` (see its docs for behavior and caveats).
*
* Next.js bundles the SDK into the server build, from where the runtime module hook can't resolve
* the `@apm-js-collab/tracing-hooks` bare specifier under isolated installs (pnpm). This variant
* points the hook at the package location that `withSentryConfig` resolved at build time.
*
* @experimental May change or be removed in any release.
*/
export function experimentalUseDiagnosticsChannelInjection(): void {
const tracingHooksDir =
process.env._sentryOrchestrionTracingHooksDir || globalWithInjectedValues._sentryOrchestrionTracingHooksDir;
nodeExperimentalUseDiagnosticsChannelInjection(tracingHooksDir ? { tracingHooksDir } : undefined);
}

// Call at module level so `next build` prerender workers still register the runner without `init`
prepareSafeIdGeneratorContext();

Expand DownExpand Up@@ -138,6 +162,17 @@ export function init(options: NodeOptions): NodeClient | undefined {
customDefaultIntegrations.push(distDirRewriteFramesIntegration({ distDirName }));
}

// The build wired the orchestrion loader but the runtime opt-in is missing → no DB spans.
const useDiagnosticsChannelInjection =
process.env._sentryUseDiagnosticsChannelInjection || globalWithInjectedValues._sentryUseDiagnosticsChannelInjection;
if (DEBUG_BUILD && useDiagnosticsChannelInjection && !isDiagnosticsChannelInjectionEnabled()) {
debug.warn(
'[@sentry/nextjs] `useDiagnosticsChannelInjection` is enabled in `withSentryConfig`, but ' +
'`Sentry.experimentalUseDiagnosticsChannelInjection()` was not called before `Sentry.init()`. ' +
'Server DB spans will not be recorded.',
);
Comment thread
chargome marked this conversation as resolved.
}

// Detect if running on OpenNext/Cloudflare and get runtime config
const cloudflareConfig = getCloudflareRuntimeConfig();

Expand Down
73 changes: 73 additions & 0 deletions packages/nextjs/test/config/diagnosticsChannelInjection.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
import { describe, expect, it } from 'vitest';
import {
BUNDLE_SAFE_INSTRUMENTED_PACKAGES,
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';

describe('filterInstrumentedExternals', () => {
it('removes the given packages, keeps the rest', () => {
expect(
filterInstrumentedExternals(['express', 'pg', 'pg-pool', 'ioredis', 'mongodb'], ['pg', 'pg-pool', 'ioredis']),
).toEqual(['express', 'mongodb']);
});

it('is a no-op with an empty bundle list', () => {
expect(filterInstrumentedExternals(['express', 'pg'], [])).toEqual(['express', 'pg']);
});
});

describe('getServerExternalPackagesPatch (diagnostics-channel injection)', () => {
it('keeps everything external except the bundle-safe allowlist, and adds the runtime machinery', () => {
const patch = getServerExternalPackagesPatch({}, 16, true);
const externals = patch.serverExternalPackages ?? [];

// Only the verified bundle-safe packages leave the external list (→ build-time loader).
for (const name of BUNDLE_SAFE_INSTRUMENTED_PACKAGES) {
expect(externals).not.toContain(name);
}
// Other instrumented packages stay external (→ runtime module hook).
expect(externals).toContain('mysql');
expect(externals).toContain('pg');
expect(externals).toContain('pg-pool');
// The orchestrion machinery must be external for the runtime hook to work.
expect(externals).toContain('@apm-js-collab/tracing-hooks');
expect(externals).toContain('@apm-js-collab/code-transformer');
});

it('respects user-provided externals even for bundle-safe packages', () => {
const patch = getServerExternalPackagesPatch({ serverExternalPackages: ['ioredis'] }, 16, true);
expect(patch.serverExternalPackages).toContain('ioredis');
});

it('is unchanged with the flag off', () => {
const patch = getServerExternalPackagesPatch({}, 16, false);
const externals = patch.serverExternalPackages ?? [];
expect(externals).toContain('ioredis');
expect(externals).toContain('mysql');
expect(externals).not.toContain('@apm-js-collab/tracing-hooks');
});
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feat lacks integration or E2E

Medium Severity

This feature PR adds unit and webpack config tests but no integration or E2E test in the diff, though the description references separate e2e verification. Review guidelines expect at least one integration or E2E test for feat changes.

Fix in CursorFix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit a3a3595. Configure here.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handled in #22080


describe('setUpBuildTimeVariables (diagnostics-channel injection)', () => {
it('injects the flag marker and the tracing-hooks location', () => {
const nextConfig: NextConfigObject = {};
setUpBuildTimeVariables(nextConfig, { _experimental: { useDiagnosticsChannelInjection: true } }, undefined);

expect(nextConfig.env).toMatchObject({
_sentryUseDiagnosticsChannelInjection: 'true',
// The runtime module hook joins subpaths onto this, so it must be an absolute directory.
_sentryOrchestrionTracingHooksDir: expect.stringMatching(/@apm-js-collab[/+]tracing-hooks/),
});
});

it('injects neither with the flag off', () => {
const nextConfig: NextConfigObject = {};
setUpBuildTimeVariables(nextConfig, {}, undefined);

expect(nextConfig.env).not.toHaveProperty('_sentryUseDiagnosticsChannelInjection');
expect(nextConfig.env).not.toHaveProperty('_sentryOrchestrionTracingHooksDir');
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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 numberDiff line numberDiff line change
Expand Up@@ -29,6 +29,8 @@ const NODE_EXPORTS_IGNORE = [
// factories for that same Node-runtime-only opt-in, so it isn't surfaced
// through the framework / serverless SDKs either.
'diagnosticsChannelInjectionIntegrations',
// Companion to the above two, same reasoning (Next.js re-exports it via `export * from '@sentry/node'`)
'isDiagnosticsChannelInjectionEnabled',
// Internal helper only needed within integrations (e.g. bunRuntimeMetricsIntegration)
'_INTERNAL_normalizeCollectionInterval',
];
Expand Down
1 change: 1 addition & 0 deletions packages/nextjs/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,6 +85,7 @@
"@sentry/node": "10.65.0",
"@sentry/opentelemetry": "10.65.0",
"@sentry/react": "10.65.0",
"@sentry/server-utils": "10.65.0",
"@sentry/vercel-edge": "10.65.0",
"@sentry/webpack-plugin": "^5.3.0",
"rollup": "^4.60.3",
Expand Down
22 changes: 22 additions & 0 deletions packages/nextjs/src/config/diagnosticsChannelInjection.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
/**
* Instrumented packages verified (via e2e) to bundle correctly, removed from Sentry's own
* `serverExternalPackages` defaults so the build-time loader can transform them. Everything else
* stays external and is instrumented by the runtime module hook instead. Deliberately an
* allowlist — bundling can break packages outright (e.g. `mysql` 2.x under Turbopack).
*/
export const BUNDLE_SAFE_INSTRUMENTED_PACKAGES = ['ioredis'];

/**
* The orchestrion runtime machinery must stay external — its parser breaks when bundled, which
* silently disables the runtime module hook.
*/
export const ORCHESTRION_RUNTIME_EXTERNAL_PACKAGES = [
'@apm-js-collab/tracing-hooks',
'@apm-js-collab/code-transformer',
];

/** Remove the given packages from a `serverExternalPackages` list. */
export function filterInstrumentedExternals(externals: string[], packagesToBundle: string[]): string[] {
const set = new Set(packagesToBundle);
return externals.filter(name => !set.has(name));
}
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
import { debug } from '@sentry/core';
import * as path from 'path';
import { getOrchestrionLoaderPath, getSentryInstrumentations } from '@sentry/server-utils/orchestrion/webpack';
import type { VercelCronsConfig } from '../../common/types';
import type { RouteManifest } from '../manifest/types';
import type { NextConfigObject, SentryBuildOptions, TurbopackMatcherWithRule, TurbopackOptions } from '../types';
import type {
JSONValue,
NextConfigObject,
SentryBuildOptions,
TurbopackMatcherWithRule,
TurbopackOptions,
} from '../types';
import { supportsNativeDebugIds, supportsTurbopackRuleCondition } from '../util';
import { generateValueInjectionRules } from './generateValueInjectionRules';

Expand DownExpand Up@@ -106,9 +113,42 @@ export function constructTurbopackConfig({
});
}

newConfig.rules = maybeAddOrchestrionRule(newConfig.rules, userSentryOptions, nextJsVersion);

return newConfig;
}

/**
* Adds the orchestrion code-transform loader rule when diagnostics-channel injection is enabled.
*/
function maybeAddOrchestrionRule(
rules: TurbopackOptions['rules'],
userSentryOptions: SentryBuildOptions | undefined,
nextJsVersion: string | undefined,
): TurbopackOptions['rules'] {
if (
!userSentryOptions?._experimental?.useDiagnosticsChannelInjection ||
!nextJsVersion ||
!supportsTurbopackRuleCondition(nextJsVersion)
) {
return rules;
}

return safelyAddTurbopackRule(rules, {
matcher: '*.{js,mjs,cjs}',
rule: {
condition: 'node',
loaders: [
{
loader: getOrchestrionLoaderPath(),
// `instrumentations` is JSON-serializable
options: { instrumentations: getSentryInstrumentations() as unknown as JSONValue[] },
},
],
},
});
}

/**
* Safely add a Turbopack rule to the existing rules.
*
Expand Down
15 changes: 14 additions & 1 deletion packages/nextjs/src/config/types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ type NextRewrite = {
destination: string;
};

interface WebpackPluginInstance {
export interface WebpackPluginInstance {
[index: string]: unknown;
apply: (compiler: unknown) => void;
}
Expand DownExpand Up@@ -765,6 +765,19 @@ 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. You must still call `Sentry.experimentalUseDiagnosticsChannelInjection()`
* in your server config to record spans.
*
* 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
7 changes: 7 additions & 0 deletions packages/nextjs/src/config/webpack.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,7 +20,9 @@ import type {
WebpackConfigObject,
WebpackConfigObjectWithModuleRules,
WebpackEntryProperty,
WebpackPluginInstance,
} from './types';
import { sentryOrchestrionWebpackPlugin } from '@sentry/server-utils/orchestrion/webpack';
import { getNextjsVersion, getPackageModules } from './util';
import type { VercelCronsConfigResult } from './withSentryConfig/getFinalConfigObjectUtils';

Expand DownExpand Up@@ -429,6 +431,11 @@ export function constructWebpackConfigFunction({
}),
);

// Orchestrion code-transform loader — Node server runtime only, never the edge compilation
Comment thread
andreiborza marked this conversation as resolved.
if (runtime === 'server' && userSentryOptions._experimental?.useDiagnosticsChannelInjection) {
newConfig.plugins.push(sentryOrchestrionWebpackPlugin() as WebpackPluginInstance);
}
Comment thread
chargome marked this conversation as resolved.

return newConfig;
};
}
Expand Down
9 changes: 9 additions & 0 deletions packages/nextjs/src/config/withSentryConfig/buildTime.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import * as childProcess from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import { getTracingHooksDirectory } from '@sentry/server-utils/orchestrion/webpack';
import type { NextConfigObject, SentryBuildOptions } from '../types';

/**
Expand DownExpand Up@@ -50,6 +51,14 @@ 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';
// Resolved here (where the SDK is a real on-disk package) and inlined, because the runtime
// module hook can't resolve the bare specifier from a bundled server chunk under pnpm.
buildTimeVariables._sentryOrchestrionTracingHooksDir = getTracingHooksDirectory();
}

if (basePath) {
buildTimeVariables._sentryBasePath = basePath;
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,9 +85,11 @@ export function getFinalConfigObject(

maybeEnableTurbopackSourcemaps(incomingUserNextConfigObject, userSentryOptions, bundlerInfo);

const useDiagnosticsChannelInjection = userSentryOptions._experimental?.useDiagnosticsChannelInjection ?? false;

return {
...incomingUserNextConfigObject,
...getServerExternalPackagesPatch(incomingUserNextConfigObject, nextMajor),
...getServerExternalPackagesPatch(incomingUserNextConfigObject, nextMajor, useDiagnosticsChannelInjection),
...getWebpackPatch({
incomingUserNextConfigObject,
userSentryOptions,
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
import {
BUNDLE_SAFE_INSTRUMENTED_PACKAGES,
filterInstrumentedExternals,
ORCHESTRION_RUNTIME_EXTERNAL_PACKAGES,
} from '../diagnosticsChannelInjection';
import { handleRunAfterProductionCompile } from '../handleRunAfterProductionCompile';
import type { RouteManifest } from '../manifest/types';
import { constructTurbopackConfig } from '../turbopack';
Expand DownExpand Up@@ -227,23 +232,32 @@ export function maybeEnableTurbopackSourcemaps(
export function getServerExternalPackagesPatch(
incomingUserNextConfigObject: NextConfigObject,
nextMajor: number | undefined,
useDiagnosticsChannelInjection = false,
): Partial<NextConfigObject> {
// Diagnostics-channel injection: 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
? filterInstrumentedExternals(DEFAULT_SERVER_EXTERNAL_PACKAGES, BUNDLE_SAFE_INSTRUMENTED_PACKAGES)
: DEFAULT_SERVER_EXTERNAL_PACKAGES;
return [
...(userProvided || []),
...defaults,
...(useDiagnosticsChannelInjection ? ORCHESTRION_RUNTIME_EXTERNAL_PACKAGES : []),
];
};

if (nextMajor && nextMajor >= 15) {
return {
serverExternalPackages: [
...(incomingUserNextConfigObject.serverExternalPackages || []),
...DEFAULT_SERVER_EXTERNAL_PACKAGES,
],
};
return { serverExternalPackages: mergeExternals(incomingUserNextConfigObject.serverExternalPackages) };
}

return {
experimental: {
...incomingUserNextConfigObject.experimental,
serverComponentsExternalPackages: [
...(incomingUserNextConfigObject.experimental?.serverComponentsExternalPackages || []),
...DEFAULT_SERVER_EXTERNAL_PACKAGES,
],
serverComponentsExternalPackages: mergeExternals(
incomingUserNextConfigObject.experimental?.serverComponentsExternalPackages,
),
},
};
}
Expand Down
37 changes: 36 additions & 1 deletion packages/nextjs/src/server/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,13 @@ import {
SEMANTIC_ATTRIBUTE_SENTRY_OP,
} from '@sentry/core';
import type { NodeClient, NodeOptions } from '@sentry/node';
import { getDefaultIntegrations, httpIntegration, init as nodeInit } from '@sentry/node';
import {
experimentalUseDiagnosticsChannelInjection as nodeExperimentalUseDiagnosticsChannelInjection,
getDefaultIntegrations,
httpIntegration,
init as nodeInit,
isDiagnosticsChannelInjectionEnabled,
} from '@sentry/node';
import { DEBUG_BUILD } from '../common/debug-build';
import { devErrorSymbolicationEventProcessor } from '../common/devErrorSymbolicationEventProcessor';
import { getVercelEnv } from '../common/getVercelEnv';
Expand DownExpand Up@@ -41,8 +47,26 @@ export { startSpan, startSpanManual, startInactiveSpan } from '../common/utils/n
const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
_sentryRewriteFramesDistDir?: string;
_sentryRelease?: string;
_sentryUseDiagnosticsChannelInjection?: string;
_sentryOrchestrionTracingHooksDir?: string;
};

/**
* EXPERIMENTAL: Next.js-aware variant of `Sentry.experimentalUseDiagnosticsChannelInjection()`
* from `@sentry/node` (see its docs for behavior and caveats).
*
* Next.js bundles the SDK into the server build, from where the runtime module hook can't resolve
* the `@apm-js-collab/tracing-hooks` bare specifier under isolated installs (pnpm). This variant
* points the hook at the package location that `withSentryConfig` resolved at build time.
*
* @experimental May change or be removed in any release.
*/
export function experimentalUseDiagnosticsChannelInjection(): void {
const tracingHooksDir =
process.env._sentryOrchestrionTracingHooksDir || globalWithInjectedValues._sentryOrchestrionTracingHooksDir;
nodeExperimentalUseDiagnosticsChannelInjection(tracingHooksDir ? { tracingHooksDir } : undefined);
}

// Call at module level so `next build` prerender workers still register the runner without `init`
prepareSafeIdGeneratorContext();

Expand DownExpand Up@@ -138,6 +162,17 @@ export function init(options: NodeOptions): NodeClient | undefined {
customDefaultIntegrations.push(distDirRewriteFramesIntegration({ distDirName }));
}

// The build wired the orchestrion loader but the runtime opt-in is missing → no DB spans.
const useDiagnosticsChannelInjection =
process.env._sentryUseDiagnosticsChannelInjection || globalWithInjectedValues._sentryUseDiagnosticsChannelInjection;
if (DEBUG_BUILD && useDiagnosticsChannelInjection && !isDiagnosticsChannelInjectionEnabled()) {
debug.warn(
'[@sentry/nextjs] `useDiagnosticsChannelInjection` is enabled in `withSentryConfig`, but ' +
'`Sentry.experimentalUseDiagnosticsChannelInjection()` was not called before `Sentry.init()`. ' +
'Server DB spans will not be recorded.',
);
Comment thread
chargome marked this conversation as resolved.
}

// Detect if running on OpenNext/Cloudflare and get runtime config
const cloudflareConfig = getCloudflareRuntimeConfig();

Expand Down
73 changes: 73 additions & 0 deletions packages/nextjs/test/config/diagnosticsChannelInjection.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
import { describe, expect, it } from 'vitest';
import {
BUNDLE_SAFE_INSTRUMENTED_PACKAGES,
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';

describe('filterInstrumentedExternals', () => {
it('removes the given packages, keeps the rest', () => {
expect(
filterInstrumentedExternals(['express', 'pg', 'pg-pool', 'ioredis', 'mongodb'], ['pg', 'pg-pool', 'ioredis']),
).toEqual(['express', 'mongodb']);
});

it('is a no-op with an empty bundle list', () => {
expect(filterInstrumentedExternals(['express', 'pg'], [])).toEqual(['express', 'pg']);
});
});

describe('getServerExternalPackagesPatch (diagnostics-channel injection)', () => {
it('keeps everything external except the bundle-safe allowlist, and adds the runtime machinery', () => {
const patch = getServerExternalPackagesPatch({}, 16, true);
const externals = patch.serverExternalPackages ?? [];

// Only the verified bundle-safe packages leave the external list (→ build-time loader).
for (const name of BUNDLE_SAFE_INSTRUMENTED_PACKAGES) {
expect(externals).not.toContain(name);
}
// Other instrumented packages stay external (→ runtime module hook).
expect(externals).toContain('mysql');
expect(externals).toContain('pg');
expect(externals).toContain('pg-pool');
// The orchestrion machinery must be external for the runtime hook to work.
expect(externals).toContain('@apm-js-collab/tracing-hooks');
expect(externals).toContain('@apm-js-collab/code-transformer');
});

it('respects user-provided externals even for bundle-safe packages', () => {
const patch = getServerExternalPackagesPatch({ serverExternalPackages: ['ioredis'] }, 16, true);
expect(patch.serverExternalPackages).toContain('ioredis');
});

it('is unchanged with the flag off', () => {
const patch = getServerExternalPackagesPatch({}, 16, false);
const externals = patch.serverExternalPackages ?? [];
expect(externals).toContain('ioredis');
expect(externals).toContain('mysql');
expect(externals).not.toContain('@apm-js-collab/tracing-hooks');
});
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feat lacks integration or E2E

Medium Severity

This feature PR adds unit and webpack config tests but no integration or E2E test in the diff, though the description references separate e2e verification. Review guidelines expect at least one integration or E2E test for feat changes.

Fix in CursorFix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit a3a3595. Configure here.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handled in #22080


describe('setUpBuildTimeVariables (diagnostics-channel injection)', () => {
it('injects the flag marker and the tracing-hooks location', () => {
const nextConfig: NextConfigObject = {};
setUpBuildTimeVariables(nextConfig, { _experimental: { useDiagnosticsChannelInjection: true } }, undefined);

expect(nextConfig.env).toMatchObject({
_sentryUseDiagnosticsChannelInjection: 'true',
// The runtime module hook joins subpaths onto this, so it must be an absolute directory.
_sentryOrchestrionTracingHooksDir: expect.stringMatching(/@apm-js-collab[/+]tracing-hooks/),
});
});

it('injects neither with the flag off', () => {
const nextConfig: NextConfigObject = {};
setUpBuildTimeVariables(nextConfig, {}, undefined);

expect(nextConfig.env).not.toHaveProperty('_sentryUseDiagnosticsChannelInjection');
expect(nextConfig.env).not.toHaveProperty('_sentryOrchestrionTracingHooksDir');
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
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 numberDiff line numberDiff line change
Expand Up@@ -29,6 +29,8 @@ const NODE_EXPORTS_IGNORE = [
// factories for that same Node-runtime-only opt-in, so it isn't surfaced
// through the framework / serverless SDKs either.
'diagnosticsChannelInjectionIntegrations',
// Companion to the above two, same reasoning (Next.js re-exports it via `export * from '@sentry/node'`)
'isDiagnosticsChannelInjectionEnabled',
// Internal helper only needed within integrations (e.g. bunRuntimeMetricsIntegration)
'_INTERNAL_normalizeCollectionInterval',
];
Expand Down
1 change: 1 addition & 0 deletions packages/nextjs/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,6 +85,7 @@
"@sentry/node": "10.65.0",
"@sentry/opentelemetry": "10.65.0",
"@sentry/react": "10.65.0",
"@sentry/server-utils": "10.65.0",
"@sentry/vercel-edge": "10.65.0",
"@sentry/webpack-plugin": "^5.3.0",
"rollup": "^4.60.3",
Expand Down
22 changes: 22 additions & 0 deletions packages/nextjs/src/config/diagnosticsChannelInjection.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
/**
* Instrumented packages verified (via e2e) to bundle correctly, removed from Sentry's own
* `serverExternalPackages` defaults so the build-time loader can transform them. Everything else
* stays external and is instrumented by the runtime module hook instead. Deliberately an
* allowlist — bundling can break packages outright (e.g. `mysql` 2.x under Turbopack).
*/
export const BUNDLE_SAFE_INSTRUMENTED_PACKAGES = ['ioredis'];

/**
* The orchestrion runtime machinery must stay external — its parser breaks when bundled, which
* silently disables the runtime module hook.
*/
export const ORCHESTRION_RUNTIME_EXTERNAL_PACKAGES = [
'@apm-js-collab/tracing-hooks',
'@apm-js-collab/code-transformer',
];

/** Remove the given packages from a `serverExternalPackages` list. */
export function filterInstrumentedExternals(externals: string[], packagesToBundle: string[]): string[] {
const set = new Set(packagesToBundle);
return externals.filter(name => !set.has(name));
}
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
import { debug } from '@sentry/core';
import * as path from 'path';
import { getOrchestrionLoaderPath, getSentryInstrumentations } from '@sentry/server-utils/orchestrion/webpack';
import type { VercelCronsConfig } from '../../common/types';
import type { RouteManifest } from '../manifest/types';
import type { NextConfigObject, SentryBuildOptions, TurbopackMatcherWithRule, TurbopackOptions } from '../types';
import type {
JSONValue,
NextConfigObject,
SentryBuildOptions,
TurbopackMatcherWithRule,
TurbopackOptions,
} from '../types';
import { supportsNativeDebugIds, supportsTurbopackRuleCondition } from '../util';
import { generateValueInjectionRules } from './generateValueInjectionRules';

Expand DownExpand Up@@ -106,9 +113,42 @@ export function constructTurbopackConfig({
});
}

newConfig.rules = maybeAddOrchestrionRule(newConfig.rules, userSentryOptions, nextJsVersion);

return newConfig;
}

/**
* Adds the orchestrion code-transform loader rule when diagnostics-channel injection is enabled.
*/
function maybeAddOrchestrionRule(
rules: TurbopackOptions['rules'],
userSentryOptions: SentryBuildOptions | undefined,
nextJsVersion: string | undefined,
): TurbopackOptions['rules'] {
if (
!userSentryOptions?._experimental?.useDiagnosticsChannelInjection ||
!nextJsVersion ||
!supportsTurbopackRuleCondition(nextJsVersion)
) {
return rules;
}

return safelyAddTurbopackRule(rules, {
matcher: '*.{js,mjs,cjs}',
rule: {
condition: 'node',
loaders: [
{
loader: getOrchestrionLoaderPath(),
// `instrumentations` is JSON-serializable
options: { instrumentations: getSentryInstrumentations() as unknown as JSONValue[] },
},
],
},
});
}

/**
* Safely add a Turbopack rule to the existing rules.
*
Expand Down
15 changes: 14 additions & 1 deletion packages/nextjs/src/config/types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ type NextRewrite = {
destination: string;
};

interface WebpackPluginInstance {
export interface WebpackPluginInstance {
[index: string]: unknown;
apply: (compiler: unknown) => void;
}
Expand DownExpand Up@@ -765,6 +765,19 @@ 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. You must still call `Sentry.experimentalUseDiagnosticsChannelInjection()`
* in your server config to record spans.
*
* 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
7 changes: 7 additions & 0 deletions packages/nextjs/src/config/webpack.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,7 +20,9 @@ import type {
WebpackConfigObject,
WebpackConfigObjectWithModuleRules,
WebpackEntryProperty,
WebpackPluginInstance,
} from './types';
import { sentryOrchestrionWebpackPlugin } from '@sentry/server-utils/orchestrion/webpack';
import { getNextjsVersion, getPackageModules } from './util';
import type { VercelCronsConfigResult } from './withSentryConfig/getFinalConfigObjectUtils';

Expand DownExpand Up@@ -429,6 +431,11 @@ export function constructWebpackConfigFunction({
}),
);

// Orchestrion code-transform loader — Node server runtime only, never the edge compilation
Comment thread
andreiborza marked this conversation as resolved.
if (runtime === 'server' && userSentryOptions._experimental?.useDiagnosticsChannelInjection) {
newConfig.plugins.push(sentryOrchestrionWebpackPlugin() as WebpackPluginInstance);
}
Comment thread
chargome marked this conversation as resolved.

return newConfig;
};
}
Expand Down
9 changes: 9 additions & 0 deletions packages/nextjs/src/config/withSentryConfig/buildTime.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import * as childProcess from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import { getTracingHooksDirectory } from '@sentry/server-utils/orchestrion/webpack';
import type { NextConfigObject, SentryBuildOptions } from '../types';

/**
Expand DownExpand Up@@ -50,6 +51,14 @@ 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';
// Resolved here (where the SDK is a real on-disk package) and inlined, because the runtime
// module hook can't resolve the bare specifier from a bundled server chunk under pnpm.
buildTimeVariables._sentryOrchestrionTracingHooksDir = getTracingHooksDirectory();
}

if (basePath) {
buildTimeVariables._sentryBasePath = basePath;
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,9 +85,11 @@ export function getFinalConfigObject(

maybeEnableTurbopackSourcemaps(incomingUserNextConfigObject, userSentryOptions, bundlerInfo);

const useDiagnosticsChannelInjection = userSentryOptions._experimental?.useDiagnosticsChannelInjection ?? false;

return {
...incomingUserNextConfigObject,
...getServerExternalPackagesPatch(incomingUserNextConfigObject, nextMajor),
...getServerExternalPackagesPatch(incomingUserNextConfigObject, nextMajor, useDiagnosticsChannelInjection),
...getWebpackPatch({
incomingUserNextConfigObject,
userSentryOptions,
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
import {
BUNDLE_SAFE_INSTRUMENTED_PACKAGES,
filterInstrumentedExternals,
ORCHESTRION_RUNTIME_EXTERNAL_PACKAGES,
} from '../diagnosticsChannelInjection';
import { handleRunAfterProductionCompile } from '../handleRunAfterProductionCompile';
import type { RouteManifest } from '../manifest/types';
import { constructTurbopackConfig } from '../turbopack';
Expand DownExpand Up@@ -227,23 +232,32 @@ export function maybeEnableTurbopackSourcemaps(
export function getServerExternalPackagesPatch(
incomingUserNextConfigObject: NextConfigObject,
nextMajor: number | undefined,
useDiagnosticsChannelInjection = false,
): Partial<NextConfigObject> {
// Diagnostics-channel injection: 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
? filterInstrumentedExternals(DEFAULT_SERVER_EXTERNAL_PACKAGES, BUNDLE_SAFE_INSTRUMENTED_PACKAGES)
: DEFAULT_SERVER_EXTERNAL_PACKAGES;
return [
...(userProvided || []),
...defaults,
...(useDiagnosticsChannelInjection ? ORCHESTRION_RUNTIME_EXTERNAL_PACKAGES : []),
];
};

if (nextMajor && nextMajor >= 15) {
return {
serverExternalPackages: [
...(incomingUserNextConfigObject.serverExternalPackages || []),
...DEFAULT_SERVER_EXTERNAL_PACKAGES,
],
};
return { serverExternalPackages: mergeExternals(incomingUserNextConfigObject.serverExternalPackages) };
}

return {
experimental: {
...incomingUserNextConfigObject.experimental,
serverComponentsExternalPackages: [
...(incomingUserNextConfigObject.experimental?.serverComponentsExternalPackages || []),
...DEFAULT_SERVER_EXTERNAL_PACKAGES,
],
serverComponentsExternalPackages: mergeExternals(
incomingUserNextConfigObject.experimental?.serverComponentsExternalPackages,
),
},
};
}
Expand Down
37 changes: 36 additions & 1 deletion packages/nextjs/src/server/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,13 @@ import {
SEMANTIC_ATTRIBUTE_SENTRY_OP,
} from '@sentry/core';
import type { NodeClient, NodeOptions } from '@sentry/node';
import { getDefaultIntegrations, httpIntegration, init as nodeInit } from '@sentry/node';
import {
experimentalUseDiagnosticsChannelInjection as nodeExperimentalUseDiagnosticsChannelInjection,
getDefaultIntegrations,
httpIntegration,
init as nodeInit,
isDiagnosticsChannelInjectionEnabled,
} from '@sentry/node';
import { DEBUG_BUILD } from '../common/debug-build';
import { devErrorSymbolicationEventProcessor } from '../common/devErrorSymbolicationEventProcessor';
import { getVercelEnv } from '../common/getVercelEnv';
Expand DownExpand Up@@ -41,8 +47,26 @@ export { startSpan, startSpanManual, startInactiveSpan } from '../common/utils/n
const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
_sentryRewriteFramesDistDir?: string;
_sentryRelease?: string;
_sentryUseDiagnosticsChannelInjection?: string;
_sentryOrchestrionTracingHooksDir?: string;
};

/**
* EXPERIMENTAL: Next.js-aware variant of `Sentry.experimentalUseDiagnosticsChannelInjection()`
* from `@sentry/node` (see its docs for behavior and caveats).
*
* Next.js bundles the SDK into the server build, from where the runtime module hook can't resolve
* the `@apm-js-collab/tracing-hooks` bare specifier under isolated installs (pnpm). This variant
* points the hook at the package location that `withSentryConfig` resolved at build time.
*
* @experimental May change or be removed in any release.
*/
export function experimentalUseDiagnosticsChannelInjection(): void {
const tracingHooksDir =
process.env._sentryOrchestrionTracingHooksDir || globalWithInjectedValues._sentryOrchestrionTracingHooksDir;
nodeExperimentalUseDiagnosticsChannelInjection(tracingHooksDir ? { tracingHooksDir } : undefined);
}

// Call at module level so `next build` prerender workers still register the runner without `init`
prepareSafeIdGeneratorContext();

Expand DownExpand Up@@ -138,6 +162,17 @@ export function init(options: NodeOptions): NodeClient | undefined {
customDefaultIntegrations.push(distDirRewriteFramesIntegration({ distDirName }));
}

// The build wired the orchestrion loader but the runtime opt-in is missing → no DB spans.
const useDiagnosticsChannelInjection =
process.env._sentryUseDiagnosticsChannelInjection || globalWithInjectedValues._sentryUseDiagnosticsChannelInjection;
if (DEBUG_BUILD && useDiagnosticsChannelInjection && !isDiagnosticsChannelInjectionEnabled()) {
debug.warn(
'[@sentry/nextjs] `useDiagnosticsChannelInjection` is enabled in `withSentryConfig`, but ' +
'`Sentry.experimentalUseDiagnosticsChannelInjection()` was not called before `Sentry.init()`. ' +
'Server DB spans will not be recorded.',
);
Comment thread
chargome marked this conversation as resolved.
}

// Detect if running on OpenNext/Cloudflare and get runtime config
const cloudflareConfig = getCloudflareRuntimeConfig();

Expand Down
73 changes: 73 additions & 0 deletions packages/nextjs/test/config/diagnosticsChannelInjection.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
import { describe, expect, it } from 'vitest';
import {
BUNDLE_SAFE_INSTRUMENTED_PACKAGES,
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';

describe('filterInstrumentedExternals', () => {
it('removes the given packages, keeps the rest', () => {
expect(
filterInstrumentedExternals(['express', 'pg', 'pg-pool', 'ioredis', 'mongodb'], ['pg', 'pg-pool', 'ioredis']),
).toEqual(['express', 'mongodb']);
});

it('is a no-op with an empty bundle list', () => {
expect(filterInstrumentedExternals(['express', 'pg'], [])).toEqual(['express', 'pg']);
});
});

describe('getServerExternalPackagesPatch (diagnostics-channel injection)', () => {
it('keeps everything external except the bundle-safe allowlist, and adds the runtime machinery', () => {
const patch = getServerExternalPackagesPatch({}, 16, true);
const externals = patch.serverExternalPackages ?? [];

// Only the verified bundle-safe packages leave the external list (→ build-time loader).
for (const name of BUNDLE_SAFE_INSTRUMENTED_PACKAGES) {
expect(externals).not.toContain(name);
}
// Other instrumented packages stay external (→ runtime module hook).
expect(externals).toContain('mysql');
expect(externals).toContain('pg');
expect(externals).toContain('pg-pool');
// The orchestrion machinery must be external for the runtime hook to work.
expect(externals).toContain('@apm-js-collab/tracing-hooks');
expect(externals).toContain('@apm-js-collab/code-transformer');
});

it('respects user-provided externals even for bundle-safe packages', () => {
const patch = getServerExternalPackagesPatch({ serverExternalPackages: ['ioredis'] }, 16, true);
expect(patch.serverExternalPackages).toContain('ioredis');
});

it('is unchanged with the flag off', () => {
const patch = getServerExternalPackagesPatch({}, 16, false);
const externals = patch.serverExternalPackages ?? [];
expect(externals).toContain('ioredis');
expect(externals).toContain('mysql');
expect(externals).not.toContain('@apm-js-collab/tracing-hooks');
});
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feat lacks integration or E2E

Medium Severity

This feature PR adds unit and webpack config tests but no integration or E2E test in the diff, though the description references separate e2e verification. Review guidelines expect at least one integration or E2E test for feat changes.

Fix in CursorFix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit a3a3595. Configure here.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handled in #22080


describe('setUpBuildTimeVariables (diagnostics-channel injection)', () => {
it('injects the flag marker and the tracing-hooks location', () => {
const nextConfig: NextConfigObject = {};
setUpBuildTimeVariables(nextConfig, { _experimental: { useDiagnosticsChannelInjection: true } }, undefined);

expect(nextConfig.env).toMatchObject({
_sentryUseDiagnosticsChannelInjection: 'true',
// The runtime module hook joins subpaths onto this, so it must be an absolute directory.
_sentryOrchestrionTracingHooksDir: expect.stringMatching(/@apm-js-collab[/+]tracing-hooks/),
});
});

it('injects neither with the flag off', () => {
const nextConfig: NextConfigObject = {};
setUpBuildTimeVariables(nextConfig, {}, undefined);

expect(nextConfig.env).not.toHaveProperty('_sentryUseDiagnosticsChannelInjection');
expect(nextConfig.env).not.toHaveProperty('_sentryOrchestrionTracingHooksDir');
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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 numberDiff line numberDiff line change
Expand Up@@ -29,6 +29,8 @@ const NODE_EXPORTS_IGNORE = [
// factories for that same Node-runtime-only opt-in, so it isn't surfaced
// through the framework / serverless SDKs either.
'diagnosticsChannelInjectionIntegrations',
// Companion to the above two, same reasoning (Next.js re-exports it via `export * from '@sentry/node'`)
'isDiagnosticsChannelInjectionEnabled',
// Internal helper only needed within integrations (e.g. bunRuntimeMetricsIntegration)
'_INTERNAL_normalizeCollectionInterval',
];
Expand Down
1 change: 1 addition & 0 deletions packages/nextjs/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,6 +85,7 @@
"@sentry/node": "10.65.0",
"@sentry/opentelemetry": "10.65.0",
"@sentry/react": "10.65.0",
"@sentry/server-utils": "10.65.0",
"@sentry/vercel-edge": "10.65.0",
"@sentry/webpack-plugin": "^5.3.0",
"rollup": "^4.60.3",
Expand Down
22 changes: 22 additions & 0 deletions packages/nextjs/src/config/diagnosticsChannelInjection.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
/**
* Instrumented packages verified (via e2e) to bundle correctly, removed from Sentry's own
* `serverExternalPackages` defaults so the build-time loader can transform them. Everything else
* stays external and is instrumented by the runtime module hook instead. Deliberately an
* allowlist — bundling can break packages outright (e.g. `mysql` 2.x under Turbopack).
*/
export const BUNDLE_SAFE_INSTRUMENTED_PACKAGES = ['ioredis'];

/**
* The orchestrion runtime machinery must stay external — its parser breaks when bundled, which
* silently disables the runtime module hook.
*/
export const ORCHESTRION_RUNTIME_EXTERNAL_PACKAGES = [
'@apm-js-collab/tracing-hooks',
'@apm-js-collab/code-transformer',
];

/** Remove the given packages from a `serverExternalPackages` list. */
export function filterInstrumentedExternals(externals: string[], packagesToBundle: string[]): string[] {
const set = new Set(packagesToBundle);
return externals.filter(name => !set.has(name));
}
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
import { debug } from '@sentry/core';
import * as path from 'path';
import { getOrchestrionLoaderPath, getSentryInstrumentations } from '@sentry/server-utils/orchestrion/webpack';
import type { VercelCronsConfig } from '../../common/types';
import type { RouteManifest } from '../manifest/types';
import type { NextConfigObject, SentryBuildOptions, TurbopackMatcherWithRule, TurbopackOptions } from '../types';
import type {
JSONValue,
NextConfigObject,
SentryBuildOptions,
TurbopackMatcherWithRule,
TurbopackOptions,
} from '../types';
import { supportsNativeDebugIds, supportsTurbopackRuleCondition } from '../util';
import { generateValueInjectionRules } from './generateValueInjectionRules';

Expand DownExpand Up@@ -106,9 +113,42 @@ export function constructTurbopackConfig({
});
}

newConfig.rules = maybeAddOrchestrionRule(newConfig.rules, userSentryOptions, nextJsVersion);

return newConfig;
}

/**
* Adds the orchestrion code-transform loader rule when diagnostics-channel injection is enabled.
*/
function maybeAddOrchestrionRule(
rules: TurbopackOptions['rules'],
userSentryOptions: SentryBuildOptions | undefined,
nextJsVersion: string | undefined,
): TurbopackOptions['rules'] {
if (
!userSentryOptions?._experimental?.useDiagnosticsChannelInjection ||
!nextJsVersion ||
!supportsTurbopackRuleCondition(nextJsVersion)
) {
return rules;
}

return safelyAddTurbopackRule(rules, {
matcher: '*.{js,mjs,cjs}',
rule: {
condition: 'node',
loaders: [
{
loader: getOrchestrionLoaderPath(),
// `instrumentations` is JSON-serializable
options: { instrumentations: getSentryInstrumentations() as unknown as JSONValue[] },
},
],
},
});
}

/**
* Safely add a Turbopack rule to the existing rules.
*
Expand Down
15 changes: 14 additions & 1 deletion packages/nextjs/src/config/types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ type NextRewrite = {
destination: string;
};

interface WebpackPluginInstance {
export interface WebpackPluginInstance {
[index: string]: unknown;
apply: (compiler: unknown) => void;
}
Expand DownExpand Up@@ -765,6 +765,19 @@ 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. You must still call `Sentry.experimentalUseDiagnosticsChannelInjection()`
* in your server config to record spans.
*
* 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
7 changes: 7 additions & 0 deletions packages/nextjs/src/config/webpack.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,7 +20,9 @@ import type {
WebpackConfigObject,
WebpackConfigObjectWithModuleRules,
WebpackEntryProperty,
WebpackPluginInstance,
} from './types';
import { sentryOrchestrionWebpackPlugin } from '@sentry/server-utils/orchestrion/webpack';
import { getNextjsVersion, getPackageModules } from './util';
import type { VercelCronsConfigResult } from './withSentryConfig/getFinalConfigObjectUtils';

Expand DownExpand Up@@ -429,6 +431,11 @@ export function constructWebpackConfigFunction({
}),
);

// Orchestrion code-transform loader — Node server runtime only, never the edge compilation
Comment thread
andreiborza marked this conversation as resolved.
if (runtime === 'server' && userSentryOptions._experimental?.useDiagnosticsChannelInjection) {
newConfig.plugins.push(sentryOrchestrionWebpackPlugin() as WebpackPluginInstance);
}
Comment thread
chargome marked this conversation as resolved.

return newConfig;
};
}
Expand Down
9 changes: 9 additions & 0 deletions packages/nextjs/src/config/withSentryConfig/buildTime.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import * as childProcess from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import { getTracingHooksDirectory } from '@sentry/server-utils/orchestrion/webpack';
import type { NextConfigObject, SentryBuildOptions } from '../types';

/**
Expand DownExpand Up@@ -50,6 +51,14 @@ 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';
// Resolved here (where the SDK is a real on-disk package) and inlined, because the runtime
// module hook can't resolve the bare specifier from a bundled server chunk under pnpm.
buildTimeVariables._sentryOrchestrionTracingHooksDir = getTracingHooksDirectory();
}

if (basePath) {
buildTimeVariables._sentryBasePath = basePath;
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,9 +85,11 @@ export function getFinalConfigObject(

maybeEnableTurbopackSourcemaps(incomingUserNextConfigObject, userSentryOptions, bundlerInfo);

const useDiagnosticsChannelInjection = userSentryOptions._experimental?.useDiagnosticsChannelInjection ?? false;

return {
...incomingUserNextConfigObject,
...getServerExternalPackagesPatch(incomingUserNextConfigObject, nextMajor),
...getServerExternalPackagesPatch(incomingUserNextConfigObject, nextMajor, useDiagnosticsChannelInjection),
...getWebpackPatch({
incomingUserNextConfigObject,
userSentryOptions,
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
import {
BUNDLE_SAFE_INSTRUMENTED_PACKAGES,
filterInstrumentedExternals,
ORCHESTRION_RUNTIME_EXTERNAL_PACKAGES,
} from '../diagnosticsChannelInjection';
import { handleRunAfterProductionCompile } from '../handleRunAfterProductionCompile';
import type { RouteManifest } from '../manifest/types';
import { constructTurbopackConfig } from '../turbopack';
Expand DownExpand Up@@ -227,23 +232,32 @@ export function maybeEnableTurbopackSourcemaps(
export function getServerExternalPackagesPatch(
incomingUserNextConfigObject: NextConfigObject,
nextMajor: number | undefined,
useDiagnosticsChannelInjection = false,
): Partial<NextConfigObject> {
// Diagnostics-channel injection: 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
? filterInstrumentedExternals(DEFAULT_SERVER_EXTERNAL_PACKAGES, BUNDLE_SAFE_INSTRUMENTED_PACKAGES)
: DEFAULT_SERVER_EXTERNAL_PACKAGES;
return [
...(userProvided || []),
...defaults,
...(useDiagnosticsChannelInjection ? ORCHESTRION_RUNTIME_EXTERNAL_PACKAGES : []),
];
};

if (nextMajor && nextMajor >= 15) {
return {
serverExternalPackages: [
...(incomingUserNextConfigObject.serverExternalPackages || []),
...DEFAULT_SERVER_EXTERNAL_PACKAGES,
],
};
return { serverExternalPackages: mergeExternals(incomingUserNextConfigObject.serverExternalPackages) };
}

return {
experimental: {
...incomingUserNextConfigObject.experimental,
serverComponentsExternalPackages: [
...(incomingUserNextConfigObject.experimental?.serverComponentsExternalPackages || []),
...DEFAULT_SERVER_EXTERNAL_PACKAGES,
],
serverComponentsExternalPackages: mergeExternals(
incomingUserNextConfigObject.experimental?.serverComponentsExternalPackages,
),
},
};
}
Expand Down
37 changes: 36 additions & 1 deletion packages/nextjs/src/server/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,13 @@ import {
SEMANTIC_ATTRIBUTE_SENTRY_OP,
} from '@sentry/core';
import type { NodeClient, NodeOptions } from '@sentry/node';
import { getDefaultIntegrations, httpIntegration, init as nodeInit } from '@sentry/node';
import {
experimentalUseDiagnosticsChannelInjection as nodeExperimentalUseDiagnosticsChannelInjection,
getDefaultIntegrations,
httpIntegration,
init as nodeInit,
isDiagnosticsChannelInjectionEnabled,
} from '@sentry/node';
import { DEBUG_BUILD } from '../common/debug-build';
import { devErrorSymbolicationEventProcessor } from '../common/devErrorSymbolicationEventProcessor';
import { getVercelEnv } from '../common/getVercelEnv';
Expand DownExpand Up@@ -41,8 +47,26 @@ export { startSpan, startSpanManual, startInactiveSpan } from '../common/utils/n
const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
_sentryRewriteFramesDistDir?: string;
_sentryRelease?: string;
_sentryUseDiagnosticsChannelInjection?: string;
_sentryOrchestrionTracingHooksDir?: string;
};

/**
* EXPERIMENTAL: Next.js-aware variant of `Sentry.experimentalUseDiagnosticsChannelInjection()`
* from `@sentry/node` (see its docs for behavior and caveats).
*
* Next.js bundles the SDK into the server build, from where the runtime module hook can't resolve
* the `@apm-js-collab/tracing-hooks` bare specifier under isolated installs (pnpm). This variant
* points the hook at the package location that `withSentryConfig` resolved at build time.
*
* @experimental May change or be removed in any release.
*/
export function experimentalUseDiagnosticsChannelInjection(): void {
const tracingHooksDir =
process.env._sentryOrchestrionTracingHooksDir || globalWithInjectedValues._sentryOrchestrionTracingHooksDir;
nodeExperimentalUseDiagnosticsChannelInjection(tracingHooksDir ? { tracingHooksDir } : undefined);
}

// Call at module level so `next build` prerender workers still register the runner without `init`
prepareSafeIdGeneratorContext();

Expand DownExpand Up@@ -138,6 +162,17 @@ export function init(options: NodeOptions): NodeClient | undefined {
customDefaultIntegrations.push(distDirRewriteFramesIntegration({ distDirName }));
}

// The build wired the orchestrion loader but the runtime opt-in is missing → no DB spans.
const useDiagnosticsChannelInjection =
process.env._sentryUseDiagnosticsChannelInjection || globalWithInjectedValues._sentryUseDiagnosticsChannelInjection;
if (DEBUG_BUILD && useDiagnosticsChannelInjection && !isDiagnosticsChannelInjectionEnabled()) {
debug.warn(
'[@sentry/nextjs] `useDiagnosticsChannelInjection` is enabled in `withSentryConfig`, but ' +
'`Sentry.experimentalUseDiagnosticsChannelInjection()` was not called before `Sentry.init()`. ' +
'Server DB spans will not be recorded.',
);
Comment thread
chargome marked this conversation as resolved.
}

// Detect if running on OpenNext/Cloudflare and get runtime config
const cloudflareConfig = getCloudflareRuntimeConfig();

Expand Down
73 changes: 73 additions & 0 deletions packages/nextjs/test/config/diagnosticsChannelInjection.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
import { describe, expect, it } from 'vitest';
import {
BUNDLE_SAFE_INSTRUMENTED_PACKAGES,
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';

describe('filterInstrumentedExternals', () => {
it('removes the given packages, keeps the rest', () => {
expect(
filterInstrumentedExternals(['express', 'pg', 'pg-pool', 'ioredis', 'mongodb'], ['pg', 'pg-pool', 'ioredis']),
).toEqual(['express', 'mongodb']);
});

it('is a no-op with an empty bundle list', () => {
expect(filterInstrumentedExternals(['express', 'pg'], [])).toEqual(['express', 'pg']);
});
});

describe('getServerExternalPackagesPatch (diagnostics-channel injection)', () => {
it('keeps everything external except the bundle-safe allowlist, and adds the runtime machinery', () => {
const patch = getServerExternalPackagesPatch({}, 16, true);
const externals = patch.serverExternalPackages ?? [];

// Only the verified bundle-safe packages leave the external list (→ build-time loader).
for (const name of BUNDLE_SAFE_INSTRUMENTED_PACKAGES) {
expect(externals).not.toContain(name);
}
// Other instrumented packages stay external (→ runtime module hook).
expect(externals).toContain('mysql');
expect(externals).toContain('pg');
expect(externals).toContain('pg-pool');
// The orchestrion machinery must be external for the runtime hook to work.
expect(externals).toContain('@apm-js-collab/tracing-hooks');
expect(externals).toContain('@apm-js-collab/code-transformer');
});

it('respects user-provided externals even for bundle-safe packages', () => {
const patch = getServerExternalPackagesPatch({ serverExternalPackages: ['ioredis'] }, 16, true);
expect(patch.serverExternalPackages).toContain('ioredis');
});

it('is unchanged with the flag off', () => {
const patch = getServerExternalPackagesPatch({}, 16, false);
const externals = patch.serverExternalPackages ?? [];
expect(externals).toContain('ioredis');
expect(externals).toContain('mysql');
expect(externals).not.toContain('@apm-js-collab/tracing-hooks');
});
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feat lacks integration or E2E

Medium Severity

This feature PR adds unit and webpack config tests but no integration or E2E test in the diff, though the description references separate e2e verification. Review guidelines expect at least one integration or E2E test for feat changes.

Fix in CursorFix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit a3a3595. Configure here.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handled in #22080


describe('setUpBuildTimeVariables (diagnostics-channel injection)', () => {
it('injects the flag marker and the tracing-hooks location', () => {
const nextConfig: NextConfigObject = {};
setUpBuildTimeVariables(nextConfig, { _experimental: { useDiagnosticsChannelInjection: true } }, undefined);

expect(nextConfig.env).toMatchObject({
_sentryUseDiagnosticsChannelInjection: 'true',
// The runtime module hook joins subpaths onto this, so it must be an absolute directory.
_sentryOrchestrionTracingHooksDir: expect.stringMatching(/@apm-js-collab[/+]tracing-hooks/),
});
});

it('injects neither with the flag off', () => {
const nextConfig: NextConfigObject = {};
setUpBuildTimeVariables(nextConfig, {}, undefined);

expect(nextConfig.env).not.toHaveProperty('_sentryUseDiagnosticsChannelInjection');
expect(nextConfig.env).not.toHaveProperty('_sentryOrchestrionTracingHooksDir');
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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 numberDiff line numberDiff line change
Expand Up@@ -29,6 +29,8 @@ const NODE_EXPORTS_IGNORE = [
// factories for that same Node-runtime-only opt-in, so it isn't surfaced
// through the framework / serverless SDKs either.
'diagnosticsChannelInjectionIntegrations',
// Companion to the above two, same reasoning (Next.js re-exports it via `export * from '@sentry/node'`)
'isDiagnosticsChannelInjectionEnabled',
// Internal helper only needed within integrations (e.g. bunRuntimeMetricsIntegration)
'_INTERNAL_normalizeCollectionInterval',
];
Expand Down
1 change: 1 addition & 0 deletions packages/nextjs/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,6 +85,7 @@
"@sentry/node": "10.65.0",
"@sentry/opentelemetry": "10.65.0",
"@sentry/react": "10.65.0",
"@sentry/server-utils": "10.65.0",
"@sentry/vercel-edge": "10.65.0",
"@sentry/webpack-plugin": "^5.3.0",
"rollup": "^4.60.3",
Expand Down
22 changes: 22 additions & 0 deletions packages/nextjs/src/config/diagnosticsChannelInjection.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
/**
* Instrumented packages verified (via e2e) to bundle correctly, removed from Sentry's own
* `serverExternalPackages` defaults so the build-time loader can transform them. Everything else
* stays external and is instrumented by the runtime module hook instead. Deliberately an
* allowlist — bundling can break packages outright (e.g. `mysql` 2.x under Turbopack).
*/
export const BUNDLE_SAFE_INSTRUMENTED_PACKAGES = ['ioredis'];

/**
* The orchestrion runtime machinery must stay external — its parser breaks when bundled, which
* silently disables the runtime module hook.
*/
export const ORCHESTRION_RUNTIME_EXTERNAL_PACKAGES = [
'@apm-js-collab/tracing-hooks',
'@apm-js-collab/code-transformer',
];

/** Remove the given packages from a `serverExternalPackages` list. */
export function filterInstrumentedExternals(externals: string[], packagesToBundle: string[]): string[] {
const set = new Set(packagesToBundle);
return externals.filter(name => !set.has(name));
}
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
import { debug } from '@sentry/core';
import * as path from 'path';
import { getOrchestrionLoaderPath, getSentryInstrumentations } from '@sentry/server-utils/orchestrion/webpack';
import type { VercelCronsConfig } from '../../common/types';
import type { RouteManifest } from '../manifest/types';
import type { NextConfigObject, SentryBuildOptions, TurbopackMatcherWithRule, TurbopackOptions } from '../types';
import type {
JSONValue,
NextConfigObject,
SentryBuildOptions,
TurbopackMatcherWithRule,
TurbopackOptions,
} from '../types';
import { supportsNativeDebugIds, supportsTurbopackRuleCondition } from '../util';
import { generateValueInjectionRules } from './generateValueInjectionRules';

Expand DownExpand Up@@ -106,9 +113,42 @@ export function constructTurbopackConfig({
});
}

newConfig.rules = maybeAddOrchestrionRule(newConfig.rules, userSentryOptions, nextJsVersion);

return newConfig;
}

/**
* Adds the orchestrion code-transform loader rule when diagnostics-channel injection is enabled.
*/
function maybeAddOrchestrionRule(
rules: TurbopackOptions['rules'],
userSentryOptions: SentryBuildOptions | undefined,
nextJsVersion: string | undefined,
): TurbopackOptions['rules'] {
if (
!userSentryOptions?._experimental?.useDiagnosticsChannelInjection ||
!nextJsVersion ||
!supportsTurbopackRuleCondition(nextJsVersion)
) {
return rules;
}

return safelyAddTurbopackRule(rules, {
matcher: '*.{js,mjs,cjs}',
rule: {
condition: 'node',
loaders: [
{
loader: getOrchestrionLoaderPath(),
// `instrumentations` is JSON-serializable
options: { instrumentations: getSentryInstrumentations() as unknown as JSONValue[] },
},
],
},
});
}

/**
* Safely add a Turbopack rule to the existing rules.
*
Expand Down
15 changes: 14 additions & 1 deletion packages/nextjs/src/config/types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ type NextRewrite = {
destination: string;
};

interface WebpackPluginInstance {
export interface WebpackPluginInstance {
[index: string]: unknown;
apply: (compiler: unknown) => void;
}
Expand DownExpand Up@@ -765,6 +765,19 @@ 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. You must still call `Sentry.experimentalUseDiagnosticsChannelInjection()`
* in your server config to record spans.
*
* 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
7 changes: 7 additions & 0 deletions packages/nextjs/src/config/webpack.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,7 +20,9 @@ import type {
WebpackConfigObject,
WebpackConfigObjectWithModuleRules,
WebpackEntryProperty,
WebpackPluginInstance,
} from './types';
import { sentryOrchestrionWebpackPlugin } from '@sentry/server-utils/orchestrion/webpack';
import { getNextjsVersion, getPackageModules } from './util';
import type { VercelCronsConfigResult } from './withSentryConfig/getFinalConfigObjectUtils';

Expand DownExpand Up@@ -429,6 +431,11 @@ export function constructWebpackConfigFunction({
}),
);

// Orchestrion code-transform loader — Node server runtime only, never the edge compilation
Comment thread
andreiborza marked this conversation as resolved.
if (runtime === 'server' && userSentryOptions._experimental?.useDiagnosticsChannelInjection) {
newConfig.plugins.push(sentryOrchestrionWebpackPlugin() as WebpackPluginInstance);
}
Comment thread
chargome marked this conversation as resolved.

return newConfig;
};
}
Expand Down
9 changes: 9 additions & 0 deletions packages/nextjs/src/config/withSentryConfig/buildTime.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import * as childProcess from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import { getTracingHooksDirectory } from '@sentry/server-utils/orchestrion/webpack';
import type { NextConfigObject, SentryBuildOptions } from '../types';

/**
Expand DownExpand Up@@ -50,6 +51,14 @@ 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';
// Resolved here (where the SDK is a real on-disk package) and inlined, because the runtime
// module hook can't resolve the bare specifier from a bundled server chunk under pnpm.
buildTimeVariables._sentryOrchestrionTracingHooksDir = getTracingHooksDirectory();
}

if (basePath) {
buildTimeVariables._sentryBasePath = basePath;
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,9 +85,11 @@ export function getFinalConfigObject(

maybeEnableTurbopackSourcemaps(incomingUserNextConfigObject, userSentryOptions, bundlerInfo);

const useDiagnosticsChannelInjection = userSentryOptions._experimental?.useDiagnosticsChannelInjection ?? false;

return {
...incomingUserNextConfigObject,
...getServerExternalPackagesPatch(incomingUserNextConfigObject, nextMajor),
...getServerExternalPackagesPatch(incomingUserNextConfigObject, nextMajor, useDiagnosticsChannelInjection),
...getWebpackPatch({
incomingUserNextConfigObject,
userSentryOptions,
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
import {
BUNDLE_SAFE_INSTRUMENTED_PACKAGES,
filterInstrumentedExternals,
ORCHESTRION_RUNTIME_EXTERNAL_PACKAGES,
} from '../diagnosticsChannelInjection';
import { handleRunAfterProductionCompile } from '../handleRunAfterProductionCompile';
import type { RouteManifest } from '../manifest/types';
import { constructTurbopackConfig } from '../turbopack';
Expand DownExpand Up@@ -227,23 +232,32 @@ export function maybeEnableTurbopackSourcemaps(
export function getServerExternalPackagesPatch(
incomingUserNextConfigObject: NextConfigObject,
nextMajor: number | undefined,
useDiagnosticsChannelInjection = false,
): Partial<NextConfigObject> {
// Diagnostics-channel injection: 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
? filterInstrumentedExternals(DEFAULT_SERVER_EXTERNAL_PACKAGES, BUNDLE_SAFE_INSTRUMENTED_PACKAGES)
: DEFAULT_SERVER_EXTERNAL_PACKAGES;
return [
...(userProvided || []),
...defaults,
...(useDiagnosticsChannelInjection ? ORCHESTRION_RUNTIME_EXTERNAL_PACKAGES : []),
];
};

if (nextMajor && nextMajor >= 15) {
return {
serverExternalPackages: [
...(incomingUserNextConfigObject.serverExternalPackages || []),
...DEFAULT_SERVER_EXTERNAL_PACKAGES,
],
};
return { serverExternalPackages: mergeExternals(incomingUserNextConfigObject.serverExternalPackages) };
}

return {
experimental: {
...incomingUserNextConfigObject.experimental,
serverComponentsExternalPackages: [
...(incomingUserNextConfigObject.experimental?.serverComponentsExternalPackages || []),
...DEFAULT_SERVER_EXTERNAL_PACKAGES,
],
serverComponentsExternalPackages: mergeExternals(
incomingUserNextConfigObject.experimental?.serverComponentsExternalPackages,
),
},
};
}
Expand Down
37 changes: 36 additions & 1 deletion packages/nextjs/src/server/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,13 @@ import {
SEMANTIC_ATTRIBUTE_SENTRY_OP,
} from '@sentry/core';
import type { NodeClient, NodeOptions } from '@sentry/node';
import { getDefaultIntegrations, httpIntegration, init as nodeInit } from '@sentry/node';
import {
experimentalUseDiagnosticsChannelInjection as nodeExperimentalUseDiagnosticsChannelInjection,
getDefaultIntegrations,
httpIntegration,
init as nodeInit,
isDiagnosticsChannelInjectionEnabled,
} from '@sentry/node';
import { DEBUG_BUILD } from '../common/debug-build';
import { devErrorSymbolicationEventProcessor } from '../common/devErrorSymbolicationEventProcessor';
import { getVercelEnv } from '../common/getVercelEnv';
Expand DownExpand Up@@ -41,8 +47,26 @@ export { startSpan, startSpanManual, startInactiveSpan } from '../common/utils/n
const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
_sentryRewriteFramesDistDir?: string;
_sentryRelease?: string;
_sentryUseDiagnosticsChannelInjection?: string;
_sentryOrchestrionTracingHooksDir?: string;
};

/**
* EXPERIMENTAL: Next.js-aware variant of `Sentry.experimentalUseDiagnosticsChannelInjection()`
* from `@sentry/node` (see its docs for behavior and caveats).
*
* Next.js bundles the SDK into the server build, from where the runtime module hook can't resolve
* the `@apm-js-collab/tracing-hooks` bare specifier under isolated installs (pnpm). This variant
* points the hook at the package location that `withSentryConfig` resolved at build time.
*
* @experimental May change or be removed in any release.
*/
export function experimentalUseDiagnosticsChannelInjection(): void {
const tracingHooksDir =
process.env._sentryOrchestrionTracingHooksDir || globalWithInjectedValues._sentryOrchestrionTracingHooksDir;
nodeExperimentalUseDiagnosticsChannelInjection(tracingHooksDir ? { tracingHooksDir } : undefined);
}

// Call at module level so `next build` prerender workers still register the runner without `init`
prepareSafeIdGeneratorContext();

Expand DownExpand Up@@ -138,6 +162,17 @@ export function init(options: NodeOptions): NodeClient | undefined {
customDefaultIntegrations.push(distDirRewriteFramesIntegration({ distDirName }));
}

// The build wired the orchestrion loader but the runtime opt-in is missing → no DB spans.
const useDiagnosticsChannelInjection =
process.env._sentryUseDiagnosticsChannelInjection || globalWithInjectedValues._sentryUseDiagnosticsChannelInjection;
if (DEBUG_BUILD && useDiagnosticsChannelInjection && !isDiagnosticsChannelInjectionEnabled()) {
debug.warn(
'[@sentry/nextjs] `useDiagnosticsChannelInjection` is enabled in `withSentryConfig`, but ' +
'`Sentry.experimentalUseDiagnosticsChannelInjection()` was not called before `Sentry.init()`. ' +
'Server DB spans will not be recorded.',
);
Comment thread
chargome marked this conversation as resolved.
}

// Detect if running on OpenNext/Cloudflare and get runtime config
const cloudflareConfig = getCloudflareRuntimeConfig();

Expand Down
73 changes: 73 additions & 0 deletions packages/nextjs/test/config/diagnosticsChannelInjection.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
import { describe, expect, it } from 'vitest';
import {
BUNDLE_SAFE_INSTRUMENTED_PACKAGES,
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';

describe('filterInstrumentedExternals', () => {
it('removes the given packages, keeps the rest', () => {
expect(
filterInstrumentedExternals(['express', 'pg', 'pg-pool', 'ioredis', 'mongodb'], ['pg', 'pg-pool', 'ioredis']),
).toEqual(['express', 'mongodb']);
});

it('is a no-op with an empty bundle list', () => {
expect(filterInstrumentedExternals(['express', 'pg'], [])).toEqual(['express', 'pg']);
});
});

describe('getServerExternalPackagesPatch (diagnostics-channel injection)', () => {
it('keeps everything external except the bundle-safe allowlist, and adds the runtime machinery', () => {
const patch = getServerExternalPackagesPatch({}, 16, true);
const externals = patch.serverExternalPackages ?? [];

// Only the verified bundle-safe packages leave the external list (→ build-time loader).
for (const name of BUNDLE_SAFE_INSTRUMENTED_PACKAGES) {
expect(externals).not.toContain(name);
}
// Other instrumented packages stay external (→ runtime module hook).
expect(externals).toContain('mysql');
expect(externals).toContain('pg');
expect(externals).toContain('pg-pool');
// The orchestrion machinery must be external for the runtime hook to work.
expect(externals).toContain('@apm-js-collab/tracing-hooks');
expect(externals).toContain('@apm-js-collab/code-transformer');
});

it('respects user-provided externals even for bundle-safe packages', () => {
const patch = getServerExternalPackagesPatch({ serverExternalPackages: ['ioredis'] }, 16, true);
expect(patch.serverExternalPackages).toContain('ioredis');
});

it('is unchanged with the flag off', () => {
const patch = getServerExternalPackagesPatch({}, 16, false);
const externals = patch.serverExternalPackages ?? [];
expect(externals).toContain('ioredis');
expect(externals).toContain('mysql');
expect(externals).not.toContain('@apm-js-collab/tracing-hooks');
});
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feat lacks integration or E2E

Medium Severity

This feature PR adds unit and webpack config tests but no integration or E2E test in the diff, though the description references separate e2e verification. Review guidelines expect at least one integration or E2E test for feat changes.

Fix in CursorFix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit a3a3595. Configure here.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handled in #22080


describe('setUpBuildTimeVariables (diagnostics-channel injection)', () => {
it('injects the flag marker and the tracing-hooks location', () => {
const nextConfig: NextConfigObject = {};
setUpBuildTimeVariables(nextConfig, { _experimental: { useDiagnosticsChannelInjection: true } }, undefined);

expect(nextConfig.env).toMatchObject({
_sentryUseDiagnosticsChannelInjection: 'true',
// The runtime module hook joins subpaths onto this, so it must be an absolute directory.
_sentryOrchestrionTracingHooksDir: expect.stringMatching(/@apm-js-collab[/+]tracing-hooks/),
});
});

it('injects neither with the flag off', () => {
const nextConfig: NextConfigObject = {};
setUpBuildTimeVariables(nextConfig, {}, undefined);

expect(nextConfig.env).not.toHaveProperty('_sentryUseDiagnosticsChannelInjection');
expect(nextConfig.env).not.toHaveProperty('_sentryOrchestrionTracingHooksDir');
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
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 numberDiff line numberDiff line change
Expand Up@@ -29,6 +29,8 @@ const NODE_EXPORTS_IGNORE = [
// factories for that same Node-runtime-only opt-in, so it isn't surfaced
// through the framework / serverless SDKs either.
'diagnosticsChannelInjectionIntegrations',
// Companion to the above two, same reasoning (Next.js re-exports it via `export * from '@sentry/node'`)
'isDiagnosticsChannelInjectionEnabled',
// Internal helper only needed within integrations (e.g. bunRuntimeMetricsIntegration)
'_INTERNAL_normalizeCollectionInterval',
];
Expand Down
1 change: 1 addition & 0 deletions packages/nextjs/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,6 +85,7 @@
"@sentry/node": "10.65.0",
"@sentry/opentelemetry": "10.65.0",
"@sentry/react": "10.65.0",
"@sentry/server-utils": "10.65.0",
"@sentry/vercel-edge": "10.65.0",
"@sentry/webpack-plugin": "^5.3.0",
"rollup": "^4.60.3",
Expand Down
22 changes: 22 additions & 0 deletions packages/nextjs/src/config/diagnosticsChannelInjection.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
/**
* Instrumented packages verified (via e2e) to bundle correctly, removed from Sentry's own
* `serverExternalPackages` defaults so the build-time loader can transform them. Everything else
* stays external and is instrumented by the runtime module hook instead. Deliberately an
* allowlist — bundling can break packages outright (e.g. `mysql` 2.x under Turbopack).
*/
export const BUNDLE_SAFE_INSTRUMENTED_PACKAGES = ['ioredis'];

/**
* The orchestrion runtime machinery must stay external — its parser breaks when bundled, which
* silently disables the runtime module hook.
*/
export const ORCHESTRION_RUNTIME_EXTERNAL_PACKAGES = [
'@apm-js-collab/tracing-hooks',
'@apm-js-collab/code-transformer',
];

/** Remove the given packages from a `serverExternalPackages` list. */
export function filterInstrumentedExternals(externals: string[], packagesToBundle: string[]): string[] {
const set = new Set(packagesToBundle);
return externals.filter(name => !set.has(name));
}
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
import { debug } from '@sentry/core';
import * as path from 'path';
import { getOrchestrionLoaderPath, getSentryInstrumentations } from '@sentry/server-utils/orchestrion/webpack';
import type { VercelCronsConfig } from '../../common/types';
import type { RouteManifest } from '../manifest/types';
import type { NextConfigObject, SentryBuildOptions, TurbopackMatcherWithRule, TurbopackOptions } from '../types';
import type {
JSONValue,
NextConfigObject,
SentryBuildOptions,
TurbopackMatcherWithRule,
TurbopackOptions,
} from '../types';
import { supportsNativeDebugIds, supportsTurbopackRuleCondition } from '../util';
import { generateValueInjectionRules } from './generateValueInjectionRules';

Expand DownExpand Up@@ -106,9 +113,42 @@ export function constructTurbopackConfig({
});
}

newConfig.rules = maybeAddOrchestrionRule(newConfig.rules, userSentryOptions, nextJsVersion);

return newConfig;
}

/**
* Adds the orchestrion code-transform loader rule when diagnostics-channel injection is enabled.
*/
function maybeAddOrchestrionRule(
rules: TurbopackOptions['rules'],
userSentryOptions: SentryBuildOptions | undefined,
nextJsVersion: string | undefined,
): TurbopackOptions['rules'] {
if (
!userSentryOptions?._experimental?.useDiagnosticsChannelInjection ||
!nextJsVersion ||
!supportsTurbopackRuleCondition(nextJsVersion)
) {
return rules;
}

return safelyAddTurbopackRule(rules, {
matcher: '*.{js,mjs,cjs}',
rule: {
condition: 'node',
loaders: [
{
loader: getOrchestrionLoaderPath(),
// `instrumentations` is JSON-serializable
options: { instrumentations: getSentryInstrumentations() as unknown as JSONValue[] },
},
],
},
});
}

/**
* Safely add a Turbopack rule to the existing rules.
*
Expand Down
15 changes: 14 additions & 1 deletion packages/nextjs/src/config/types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ type NextRewrite = {
destination: string;
};

interface WebpackPluginInstance {
export interface WebpackPluginInstance {
[index: string]: unknown;
apply: (compiler: unknown) => void;
}
Expand DownExpand Up@@ -765,6 +765,19 @@ 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. You must still call `Sentry.experimentalUseDiagnosticsChannelInjection()`
* in your server config to record spans.
*
* 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
7 changes: 7 additions & 0 deletions packages/nextjs/src/config/webpack.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,7 +20,9 @@ import type {
WebpackConfigObject,
WebpackConfigObjectWithModuleRules,
WebpackEntryProperty,
WebpackPluginInstance,
} from './types';
import { sentryOrchestrionWebpackPlugin } from '@sentry/server-utils/orchestrion/webpack';
import { getNextjsVersion, getPackageModules } from './util';
import type { VercelCronsConfigResult } from './withSentryConfig/getFinalConfigObjectUtils';

Expand DownExpand Up@@ -429,6 +431,11 @@ export function constructWebpackConfigFunction({
}),
);

// Orchestrion code-transform loader — Node server runtime only, never the edge compilation
Comment thread
andreiborza marked this conversation as resolved.
if (runtime === 'server' && userSentryOptions._experimental?.useDiagnosticsChannelInjection) {
newConfig.plugins.push(sentryOrchestrionWebpackPlugin() as WebpackPluginInstance);
}
Comment thread
chargome marked this conversation as resolved.

return newConfig;
};
}
Expand Down
9 changes: 9 additions & 0 deletions packages/nextjs/src/config/withSentryConfig/buildTime.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import * as childProcess from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import { getTracingHooksDirectory } from '@sentry/server-utils/orchestrion/webpack';
import type { NextConfigObject, SentryBuildOptions } from '../types';

/**
Expand DownExpand Up@@ -50,6 +51,14 @@ 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';
// Resolved here (where the SDK is a real on-disk package) and inlined, because the runtime
// module hook can't resolve the bare specifier from a bundled server chunk under pnpm.
buildTimeVariables._sentryOrchestrionTracingHooksDir = getTracingHooksDirectory();
}

if (basePath) {
buildTimeVariables._sentryBasePath = basePath;
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,9 +85,11 @@ export function getFinalConfigObject(

maybeEnableTurbopackSourcemaps(incomingUserNextConfigObject, userSentryOptions, bundlerInfo);

const useDiagnosticsChannelInjection = userSentryOptions._experimental?.useDiagnosticsChannelInjection ?? false;

return {
...incomingUserNextConfigObject,
...getServerExternalPackagesPatch(incomingUserNextConfigObject, nextMajor),
...getServerExternalPackagesPatch(incomingUserNextConfigObject, nextMajor, useDiagnosticsChannelInjection),
...getWebpackPatch({
incomingUserNextConfigObject,
userSentryOptions,
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
import {
BUNDLE_SAFE_INSTRUMENTED_PACKAGES,
filterInstrumentedExternals,
ORCHESTRION_RUNTIME_EXTERNAL_PACKAGES,
} from '../diagnosticsChannelInjection';
import { handleRunAfterProductionCompile } from '../handleRunAfterProductionCompile';
import type { RouteManifest } from '../manifest/types';
import { constructTurbopackConfig } from '../turbopack';
Expand DownExpand Up@@ -227,23 +232,32 @@ export function maybeEnableTurbopackSourcemaps(
export function getServerExternalPackagesPatch(
incomingUserNextConfigObject: NextConfigObject,
nextMajor: number | undefined,
useDiagnosticsChannelInjection = false,
): Partial<NextConfigObject> {
// Diagnostics-channel injection: 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
? filterInstrumentedExternals(DEFAULT_SERVER_EXTERNAL_PACKAGES, BUNDLE_SAFE_INSTRUMENTED_PACKAGES)
: DEFAULT_SERVER_EXTERNAL_PACKAGES;
return [
...(userProvided || []),
...defaults,
...(useDiagnosticsChannelInjection ? ORCHESTRION_RUNTIME_EXTERNAL_PACKAGES : []),
];
};

if (nextMajor && nextMajor >= 15) {
return {
serverExternalPackages: [
...(incomingUserNextConfigObject.serverExternalPackages || []),
...DEFAULT_SERVER_EXTERNAL_PACKAGES,
],
};
return { serverExternalPackages: mergeExternals(incomingUserNextConfigObject.serverExternalPackages) };
}

return {
experimental: {
...incomingUserNextConfigObject.experimental,
serverComponentsExternalPackages: [
...(incomingUserNextConfigObject.experimental?.serverComponentsExternalPackages || []),
...DEFAULT_SERVER_EXTERNAL_PACKAGES,
],
serverComponentsExternalPackages: mergeExternals(
incomingUserNextConfigObject.experimental?.serverComponentsExternalPackages,
),
},
};
}
Expand Down
37 changes: 36 additions & 1 deletion packages/nextjs/src/server/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,13 @@ import {
SEMANTIC_ATTRIBUTE_SENTRY_OP,
} from '@sentry/core';
import type { NodeClient, NodeOptions } from '@sentry/node';
import { getDefaultIntegrations, httpIntegration, init as nodeInit } from '@sentry/node';
import {
experimentalUseDiagnosticsChannelInjection as nodeExperimentalUseDiagnosticsChannelInjection,
getDefaultIntegrations,
httpIntegration,
init as nodeInit,
isDiagnosticsChannelInjectionEnabled,
} from '@sentry/node';
import { DEBUG_BUILD } from '../common/debug-build';
import { devErrorSymbolicationEventProcessor } from '../common/devErrorSymbolicationEventProcessor';
import { getVercelEnv } from '../common/getVercelEnv';
Expand DownExpand Up@@ -41,8 +47,26 @@ export { startSpan, startSpanManual, startInactiveSpan } from '../common/utils/n
const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
_sentryRewriteFramesDistDir?: string;
_sentryRelease?: string;
_sentryUseDiagnosticsChannelInjection?: string;
_sentryOrchestrionTracingHooksDir?: string;
};

/**
* EXPERIMENTAL: Next.js-aware variant of `Sentry.experimentalUseDiagnosticsChannelInjection()`
* from `@sentry/node` (see its docs for behavior and caveats).
*
* Next.js bundles the SDK into the server build, from where the runtime module hook can't resolve
* the `@apm-js-collab/tracing-hooks` bare specifier under isolated installs (pnpm). This variant
* points the hook at the package location that `withSentryConfig` resolved at build time.
*
* @experimental May change or be removed in any release.
*/
export function experimentalUseDiagnosticsChannelInjection(): void {
const tracingHooksDir =
process.env._sentryOrchestrionTracingHooksDir || globalWithInjectedValues._sentryOrchestrionTracingHooksDir;
nodeExperimentalUseDiagnosticsChannelInjection(tracingHooksDir ? { tracingHooksDir } : undefined);
}

// Call at module level so `next build` prerender workers still register the runner without `init`
prepareSafeIdGeneratorContext();

Expand DownExpand Up@@ -138,6 +162,17 @@ export function init(options: NodeOptions): NodeClient | undefined {
customDefaultIntegrations.push(distDirRewriteFramesIntegration({ distDirName }));
}

// The build wired the orchestrion loader but the runtime opt-in is missing → no DB spans.
const useDiagnosticsChannelInjection =
process.env._sentryUseDiagnosticsChannelInjection || globalWithInjectedValues._sentryUseDiagnosticsChannelInjection;
if (DEBUG_BUILD && useDiagnosticsChannelInjection && !isDiagnosticsChannelInjectionEnabled()) {
debug.warn(
'[@sentry/nextjs] `useDiagnosticsChannelInjection` is enabled in `withSentryConfig`, but ' +
'`Sentry.experimentalUseDiagnosticsChannelInjection()` was not called before `Sentry.init()`. ' +
'Server DB spans will not be recorded.',
);
Comment thread
chargome marked this conversation as resolved.
}

// Detect if running on OpenNext/Cloudflare and get runtime config
const cloudflareConfig = getCloudflareRuntimeConfig();

Expand Down
73 changes: 73 additions & 0 deletions packages/nextjs/test/config/diagnosticsChannelInjection.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
import { describe, expect, it } from 'vitest';
import {
BUNDLE_SAFE_INSTRUMENTED_PACKAGES,
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';

describe('filterInstrumentedExternals', () => {
it('removes the given packages, keeps the rest', () => {
expect(
filterInstrumentedExternals(['express', 'pg', 'pg-pool', 'ioredis', 'mongodb'], ['pg', 'pg-pool', 'ioredis']),
).toEqual(['express', 'mongodb']);
});

it('is a no-op with an empty bundle list', () => {
expect(filterInstrumentedExternals(['express', 'pg'], [])).toEqual(['express', 'pg']);
});
});

describe('getServerExternalPackagesPatch (diagnostics-channel injection)', () => {
it('keeps everything external except the bundle-safe allowlist, and adds the runtime machinery', () => {
const patch = getServerExternalPackagesPatch({}, 16, true);
const externals = patch.serverExternalPackages ?? [];

// Only the verified bundle-safe packages leave the external list (→ build-time loader).
for (const name of BUNDLE_SAFE_INSTRUMENTED_PACKAGES) {
expect(externals).not.toContain(name);
}
// Other instrumented packages stay external (→ runtime module hook).
expect(externals).toContain('mysql');
expect(externals).toContain('pg');
expect(externals).toContain('pg-pool');
// The orchestrion machinery must be external for the runtime hook to work.
expect(externals).toContain('@apm-js-collab/tracing-hooks');
expect(externals).toContain('@apm-js-collab/code-transformer');
});

it('respects user-provided externals even for bundle-safe packages', () => {
const patch = getServerExternalPackagesPatch({ serverExternalPackages: ['ioredis'] }, 16, true);
expect(patch.serverExternalPackages).toContain('ioredis');
});

it('is unchanged with the flag off', () => {
const patch = getServerExternalPackagesPatch({}, 16, false);
const externals = patch.serverExternalPackages ?? [];
expect(externals).toContain('ioredis');
expect(externals).toContain('mysql');
expect(externals).not.toContain('@apm-js-collab/tracing-hooks');
});
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feat lacks integration or E2E

Medium Severity

This feature PR adds unit and webpack config tests but no integration or E2E test in the diff, though the description references separate e2e verification. Review guidelines expect at least one integration or E2E test for feat changes.

Fix in CursorFix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit a3a3595. Configure here.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handled in #22080


describe('setUpBuildTimeVariables (diagnostics-channel injection)', () => {
it('injects the flag marker and the tracing-hooks location', () => {
const nextConfig: NextConfigObject = {};
setUpBuildTimeVariables(nextConfig, { _experimental: { useDiagnosticsChannelInjection: true } }, undefined);

expect(nextConfig.env).toMatchObject({
_sentryUseDiagnosticsChannelInjection: 'true',
// The runtime module hook joins subpaths onto this, so it must be an absolute directory.
_sentryOrchestrionTracingHooksDir: expect.stringMatching(/@apm-js-collab[/+]tracing-hooks/),
});
});

it('injects neither with the flag off', () => {
const nextConfig: NextConfigObject = {};
setUpBuildTimeVariables(nextConfig, {}, undefined);

expect(nextConfig.env).not.toHaveProperty('_sentryUseDiagnosticsChannelInjection');
expect(nextConfig.env).not.toHaveProperty('_sentryOrchestrionTracingHooksDir');
});
});
Loading
Loading