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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ describe('orchestrion mysql instrumentation (Bun)', () => {
// with the expected SQL
expect(line).toContain('statement=SELECT 1 AS solution');
// injected banner ran at bundle boot
expect(line).toContain('"bundler":true');
expect(line).toContain('"bundler":[]');
} finally {
if (outfile) {
rmSync(dirname(outfile), { recursive: true, force: true });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ describe('orchestrion pg instrumentation (Bun)', () => {
// with the expected SQL
expect(line).toContain('statement=SELECT 1 AS solution');
// injected banner ran at bundle boot
expect(line).toContain('"bundler":true');
expect(line).toContain('"bundler":[]');
} finally {
if (outfile) {
rmSync(dirname(outfile), { recursive: true, force: true });
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,5 @@
import codeTransformerRollup from '@apm-js-collab/code-transformer-bundler-plugins/rollup';
import { INSTRUMENTED_MODULE_NAMES, SENTRY_INSTRUMENTATIONS } from '@sentry/server-utils/orchestrion/config';

// Tells the SDK the orchestrion bundler transform ran, so `detectOrchestrionSetup()`
// no-ops the runtime diagnostics-channel hook. Injected via `codeTransformerRollup`'s
// `injectDiagnostics` option (sourcemap-safe) instead of a hand-rolled plugin.
const orchestrionBundlerMarker = [
'globalThis.__SENTRY_ORCHESTRION__ = (globalThis.__SENTRY_ORCHESTRION__ || {});',
'globalThis.__SENTRY_ORCHESTRION__.bundler = true;',
'',
].join('\n');
import { INSTRUMENTED_MODULE_NAMES } from '@sentry/server-utils/orchestrion/config';
import { sentryOrchestrionPlugin } from '@sentry/server-utils/orchestrion/rollup';

// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
Expand Down Expand Up @@ -40,13 +31,11 @@ export default defineNuxtConfig({
inline: [...INSTRUMENTED_MODULE_NAMES, 'standard-as-callback'],
},
rollupConfig: {
plugins: [
codeTransformerRollup({
instrumentations: SENTRY_INSTRUMENTATIONS,
injectDiagnostics: () => orchestrionBundlerMarker,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}) as any,
],
// The Sentry rollup plugin runs the orchestrion code transform and injects a boot marker listing
// the modules it actually transformed (`__SENTRY_ORCHESTRION__.bundler = [...]`). The SDK's
// per-module channel gating reads that list, so it must be the real set, not an empty array.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
plugins: [sentryOrchestrionPlugin() as any],
},
},
});
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import * as Sentry from '@sentry/node';

async function run() {
// Yield a microtick so the DC subscriber (deferred via Promise.resolve().then)
// is registered before ioredis creates its native TracingChannels on import.
await Promise.resolve();

const { default: Redis } = await import('ioredis-5');
const redisClient = new Redis({ host: '127.0.0.1', port: 6382, lazyConnect: true });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@ const CONNECT_CONFIG = {
};

async function run() {
// Yield a microtick so the DC subscriber (deferred via Promise.resolve().then)
// is registered before mysql2 publishes on its native TracingChannels.
await Promise.resolve();

// Gate on the DB actually accepting a connection before opening the span (see `waitForConnection`).
// MySQL keeps finalizing for a short window after the healthcheck passes and drops early handshakes,
// so this retries a real connect. It runs outside an active span, so the connect stays uninstrumented.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import * as Sentry from '@sentry/node';

async function run() {
// Yield a microtick so the DC subscriber (deferred via Promise.resolve().then)
// is registered before node-redis eagerly creates its native TracingChannels on require().
await Promise.resolve();

const { createClient } = await import('redis-5-tracing');
const redisClient = await createClient({ socket: { host: '127.0.0.1', port: 6381 } }).connect();

Expand Down
4 changes: 2 additions & 2 deletions packages/bun/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ import {
} from '@sentry/server-utils/orchestrion/config';

const BUNDLER_MARKER_BANNER =
';(globalThis.__SENTRY_ORCHESTRION__=(globalThis.__SENTRY_ORCHESTRION__||{})).bundler=true;';
';(globalThis.__SENTRY_ORCHESTRION__=(globalThis.__SENTRY_ORCHESTRION__||{})).bundler=[];';

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

cc @isaacs do we actually need this? Can we properly inject this with the correct modules somehow?

Comment thread
cursor[bot] marked this conversation as resolved.

// Minimal shape of Bun's `PluginBuilder` that we touch. Typed locally instead
// of depending on `bun-types`, which would pull Bun's globals.
Expand All @@ -57,7 +57,7 @@ interface BunPluginBuilder {
* with the central `SENTRY_INSTRUMENTATIONS`. The plugin injects
* `diagnostics_channel.tracingChannel` calls into the instrumented libraries as
* `bun build` bundles them, and injects a banner that sets
* `globalThis.__SENTRY_ORCHESTRION__.bundler = true` when the bundle boots
* `globalThis.__SENTRY_ORCHESTRION__.bundler = []` when the bundle boots
*
* Pass the result to `Bun.build({ plugins: [...] })`.
*
Expand Down
16 changes: 16 additions & 0 deletions packages/core/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -927,6 +927,16 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
*/
public on(hook: 'stopUIProfiler', callback: () => void): () => void;

/**
* A hook that is called when the orchestrion runtime hook injects diagnostics
* channels into a module as it is loaded.
*
* The callback receives the name of the instrumented module.
*
* @returns {() => void} A function that, when executed, removes the registered callback.
*/
public on(hook: 'orchestrion.module-runtime-injected', callback: (moduleName: string) => void): () => void;

/**
* Register a hook on this client.
*/
Expand Down Expand Up @@ -1188,6 +1198,12 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
*/
public emit(hook: 'stopUIProfiler'): void;

/**
* Emit a hook event when the orchestrion runtime hook injects diagnostics
* channels into a module as it is loaded.
*/
public emit(hook: 'orchestrion.module-runtime-injected', moduleName: string): void;

/**
* Emit a hook that was previously registered via `on()`.
*/
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/utils/worldwide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ export type InternalGlobal = {
/** Empty array signifies bundler plugin ran */
bundler?: string[];
};
/**
* Bridge set by the SDK during `init` so that build-time (bundler) injected modules can announce
* themselves the moment they load. Bundlers that transform modules but provide no runtime boot hook
* (e.g. Turbopack, which only takes loaders) prepend a prologue to each instrumented module that
* calls this with the module name; the bridge emits `orchestrion.module-runtime-injected` so channel
* subscribers get wired up before the module publishes. Mirrors the runtime module hook's own event.
*/
__SENTRY_ORCHESTRION_ON_INJECT__?: (moduleName: string) => void;
} & Carrier;

/** Get's the global object for the current JavaScript runtime */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
import { debug } from '@sentry/core';
import * as path from 'path';
import { getOrchestrionLoaderPath, getSentryInstrumentations } from '@sentry/server-utils/orchestrion/webpack';
import { getSentryOrchestrionLoaderPath } from '@sentry/server-utils/orchestrion/webpack';
import type { VercelCronsConfig } from '../../common/types';
import type { RouteManifest } from '../manifest/types';
import type {
JSONValue,
NextConfigObject,
SentryBuildOptions,
TurbopackMatcherWithRule,
TurbopackOptions,
} from '../types';
import type { NextConfigObject, SentryBuildOptions, TurbopackMatcherWithRule, TurbopackOptions } from '../types';
import { supportsNativeDebugIds, supportsTurbopackRuleCondition } from '../util';
import { generateValueInjectionRules } from './generateValueInjectionRules';

Expand Down Expand Up @@ -58,6 +52,10 @@ export function constructTurbopackConfig({
nextJsVersion,
tunnelPath,
vercelCronsConfig,
// Mark the bundler injection path as active at boot (before `Sentry.init()`), so
// `isOrchestrionInjected()` is reliable even without the runtime hook. Individual modules add
// themselves to this list as they load; see the orchestrion loader's injected prologue.
injectOrchestrionBundlerMarker: !!userSentryOptions?._experimental?.useDiagnosticsChannelInjection,
});

for (const { matcher, rule } of valueInjectionRules) {
Expand Down Expand Up @@ -138,13 +136,9 @@ function maybeAddOrchestrionRule(
matcher: '*.{js,mjs,cjs}',
rule: {
condition: 'node',
loaders: [
{
loader: getOrchestrionLoaderPath(),
// `instrumentations` is JSON-serializable
options: { instrumentations: getSentryInstrumentations() as unknown as JSONValue[] },
},
],
// The Sentry loader reads the instrumentation config itself and appends a self-registration
// prologue to each transformed module, so it needs no `options` — use the string shortcut.
loaders: [getSentryOrchestrionLoaderPath()],
},
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ export function generateValueInjectionRules({
nextJsVersion,
tunnelPath,
vercelCronsConfig,
injectOrchestrionBundlerMarker,
}: {
routeManifest?: RouteManifest;
nextJsVersion?: string;
tunnelPath?: string;
vercelCronsConfig?: VercelCronsConfig;
injectOrchestrionBundlerMarker?: boolean;
}): TurbopackMatcherWithRule[] {
const rules: TurbopackMatcherWithRule[] = [];
const isomorphicValues: Record<string, JSONValue> = {};
Expand Down Expand Up @@ -45,6 +47,15 @@ export function generateValueInjectionRules({
// Use process.cwd() to get the project directory at build time
serverValues.__SENTRY_SERVER_MODULES__ = getPackageModules(process.cwd());

// Mark the bundler (build-time) orchestrion injection as active at boot. This runs at the top of the
// server `instrumentation` file — before `Sentry.init()` and before the runtime hook — so
// `isOrchestrionInjected()` is reliable for bundler-only setups too. Turbopack has no plugin/boot
// hook to emit the full transformed-module list the way the webpack plugin does, so we seed an empty
// array here; each transformed module then appends itself as it loads (see the orchestrion loader).
if (injectOrchestrionBundlerMarker) {
serverValues.__SENTRY_ORCHESTRION__ = { bundler: [] };
}

if (Object.keys(isomorphicValues).length > 0) {
clientValues = { ...clientValues, ...isomorphicValues };
serverValues = { ...serverValues, ...isomorphicValues };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1574,4 +1574,30 @@ describe('safelyAddTurbopackRule', () => {
});
});
});

describe('orchestrion diagnostics-channel injection', () => {
it('does not add the orchestrion loader rule by default', () => {
const result = constructTurbopackConfig({
userNextConfig: {},
userSentryOptions: {},
nextJsVersion: '16.0.0',
});

expect(result.rules?.['*.{js,mjs,cjs}']).toBeUndefined();
});

it('adds the Sentry orchestrion loader rule (node server only) when injection is enabled', () => {
const result = constructTurbopackConfig({
userNextConfig: {},
userSentryOptions: { _experimental: { useDiagnosticsChannelInjection: true } },
nextJsVersion: '16.0.0',
});

const rule = result.rules?.['*.{js,mjs,cjs}'] as { condition?: unknown; loaders?: unknown[] } | undefined;
expect(rule?.condition).toBe('node');
// A single loader referenced by path (string shortcut, no options).
expect(rule?.loaders).toHaveLength(1);
expect(typeof rule?.loaders?.[0]).toBe('string');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,26 @@ describe('generateValueInjectionRules', () => {
});
});

describe('orchestrion bundler marker', () => {
it('does not inject the orchestrion marker by default', () => {
const result = generateValueInjectionRules({});

const serverRule = result.find(rule => rule.matcher === '**/instrumentation.*');
const values = (serverRule?.rule as { loaders: [{ options: { values: Record<string, unknown> } }] }).loaders[0]
.options.values;
expect(values).not.toHaveProperty('__SENTRY_ORCHESTRION__');
});

it('seeds an empty bundler array at boot when injectOrchestrionBundlerMarker is set', () => {
const result = generateValueInjectionRules({ injectOrchestrionBundlerMarker: true });

const serverRule = result.find(rule => rule.matcher === '**/instrumentation.*');
const values = (serverRule?.rule as { loaders: [{ options: { values: Record<string, unknown> } }] }).loaders[0]
.options.values;
expect(values.__SENTRY_ORCHESTRION__).toEqual({ bundler: [] });
});
});

describe('with nextJsVersion only', () => {
it('should generate client and server rules when nextJsVersion is provided', () => {
const result = generateValueInjectionRules({
Expand Down
19 changes: 12 additions & 7 deletions packages/node/src/integrations/tracing/dataloader/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,24 @@ const INTEGRATION_NAME = 'Dataloader' as const;
export const instrumentDataloader = generateInstrumentOnce(INTEGRATION_NAME, () => new DataloaderInstrumentation());

const _dataloaderIntegration = (() => {
// Decide in setup/setupOnce, not in the factory: the runtime channel injection runs inside `Sentry.init()`,
// after the integrations array has already been built, so `isOrchestrionInjected()` is only
// reliable by `setup`. When the diagnostics channels are injected (runtime hook or bundler
// plugin), subscribe to them (the channel integration needs the client to register its
// injection listener); otherwise fall back to the vendored OTel instrumentation.

return {
name: INTEGRATION_NAME,
setupOnce() {
// Decide here, not in the factory: the runtime channel injection runs inside `Sentry.init()`,
// after the integrations array has already been built, so `isOrchestrionInjected()` is only
// reliable by `setupOnce`. When the diagnostics channels are injected (runtime hook or bundler
// plugin), subscribe to them; otherwise fall back to the vendored OTel instrumentation.
if (isOrchestrionInjected()) {
dataloaderChannelIntegration().setupOnce?.();
} else {
if (!isOrchestrionInjected()) {
instrumentDataloader();
}
},
setup(client) {
if (isOrchestrionInjected()) {
dataloaderChannelIntegration().setup?.(client);
}
},
};
}) satisfies IntegrationFn;

Expand Down
18 changes: 12 additions & 6 deletions packages/node/src/integrations/tracing/knex/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,24 @@ const INTEGRATION_NAME = 'Knex' as const;
export const instrumentKnex = generateInstrumentOnce(INTEGRATION_NAME, () => new KnexInstrumentation());

const _knexIntegration = (() => {
// Decide in setup/setupOnce, not in the factory: the runtime channel injection runs inside `Sentry.init()`,
// after the integrations array has already been built, so `isOrchestrionInjected()` is only
// reliable by `setup`. When the diagnostics channels are injected (runtime hook or bundler
// plugin), subscribe to them (the channel integration needs the client to register its
// injection listener); otherwise fall back to the vendored OTel instrumentation.

return {
name: INTEGRATION_NAME,
setupOnce() {
// Prefer the diagnostics-channel subscriber when orchestrion injected its channels; otherwise
// fall back to the vendored OTel instrumentation. `isOrchestrionInjected()` is only reliable by
// `setupOnce` (the runtime injection runs during `Sentry.init()`, after integrations are built).
if (isOrchestrionInjected()) {
knexChannelIntegration().setupOnce?.();
} else {
if (!isOrchestrionInjected()) {
instrumentKnex();
}
},
setup(client) {
if (isOrchestrionInjected()) {
knexChannelIntegration().setup?.(client);
}
},
};
}) satisfies IntegrationFn;

Expand Down
11 changes: 11 additions & 0 deletions packages/server-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@
"import": "./build/esm/orchestrion/bundler/esbuild.js",
"require": "./build/cjs/orchestrion/bundler/esbuild.js"
},
"./orchestrion/turbopack-loader": {
"types": "./build/types/orchestrion/bundler/turbopack-loader.d.ts",
"import": "./build/esm/orchestrion/bundler/turbopack-loader.js",
"require": "./build/cjs/orchestrion/bundler/turbopack-loader.js"
},
"./orchestrion/import-hook": {
"import": "./build/orchestrion/import-hook.mjs"
}
Expand Down Expand Up @@ -86,6 +91,9 @@
],
"orchestrion/esbuild": [
"build/types-ts3.8/orchestrion/bundler/esbuild.d.ts"
],
"orchestrion/turbopack-loader": [
"build/types-ts3.8/orchestrion/bundler/turbopack-loader.d.ts"
]
},
"*": {
Expand All @@ -109,6 +117,9 @@
],
"orchestrion/esbuild": [
"build/types/orchestrion/bundler/esbuild.d.ts"
],
"orchestrion/turbopack-loader": [
"build/types/orchestrion/bundler/turbopack-loader.d.ts"
]
}
},
Expand Down
3 changes: 3 additions & 0 deletions packages/server-utils/rollup.npm.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ export default [
'src/orchestrion/bundler/rollup.ts',
'src/orchestrion/bundler/webpack.ts',
'src/orchestrion/bundler/esbuild.ts',
// Referenced by absolute path (never imported), so it must be its own entrypoint to land in
// build/. Turbopack `require`s it as a loader; see `getSentryOrchestrionLoaderPath`.
'src/orchestrion/bundler/turbopack-loader.ts',
],
packageSpecificConfig: {
output: {
Expand Down
Loading
Loading