Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 1.8k
feat(nextjs): Add opt-in for orchestrion instrumentation#22043
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
e908fcb49fe3ad8e64adb608f5935ae940df068a03dcf5b0ba3a35955820b35027bac67982e110a6439d48a8e7aFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff 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 number | Diff line number | Diff line change |
|---|---|---|
| @@ -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'; | ||
| @@ -429,6 +431,11 @@ export function constructWebpackConfigFunction({ | ||
| }), | ||
| ); | ||
| // Orchestrion code-transform loader — Node server runtime only, never the edge compilation | ||
| if (runtime === 'server' && userSentryOptions._experimental?.useDiagnosticsChannelInjection) { | ||
| newConfig.plugins.push(sentryOrchestrionWebpackPlugin() as WebpackPluginInstance); | ||
| } | ||
chargome marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| return newConfig; | ||
| }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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'; | ||
| @@ -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(); | ||
| @@ -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.', | ||
| ); | ||
chargome marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| } | ||
| // Detect if running on OpenNext/Cloudflare and get runtime config | ||
| const cloudflareConfig = getCloudflareRuntimeConfig(); | ||
| Original file line number | Diff line number | Diff 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'); | ||
| }); | ||
| }); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Feat lacks integration or E2EMedium 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 Triggered by project rule: PR Review Guidelines for Cursor Bot Reviewed by Cursor Bugbot for commit a3a3595. Configure here. MemberAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe 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'); | ||
| }); | ||
| }); | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.


Uh oh!
There was an error while loading. Please reload this page.