Skip to content

Commit 43866e7

Browse files
committed
feat(node): Only setup orchestrion channel listeners when needed
1 parent 2cbacb5 commit 43866e7

7 files changed

Lines changed: 103 additions & 10 deletions

File tree

packages/bun/src/plugin.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ import {
4444
} from '@sentry/server-utils/orchestrion/config';
4545

4646
const BUNDLER_MARKER_BANNER =
47-
';(globalThis.__SENTRY_ORCHESTRION__=(globalThis.__SENTRY_ORCHESTRION__||{})).bundler=true;';
47+
';(globalThis.__SENTRY_ORCHESTRION__=(globalThis.__SENTRY_ORCHESTRION__||{})).bundler=[];';
4848

4949
// Minimal shape of Bun's `PluginBuilder` that we touch. Typed locally instead
5050
// of depending on `bun-types`, which would pull Bun's globals.
@@ -57,7 +57,7 @@ interface BunPluginBuilder {
5757
* with the central `SENTRY_INSTRUMENTATIONS`. The plugin injects
5858
* `diagnostics_channel.tracingChannel` calls into the instrumented libraries as
5959
* `bun build` bundles them, and injects a banner that sets
60-
* `globalThis.__SENTRY_ORCHESTRION__.bundler = true` when the bundle boots
60+
* `globalThis.__SENTRY_ORCHESTRION__.bundler = []` when the bundle boots
6161
*
6262
* Pass the result to `Bun.build({ plugins: [...] })`.
6363
*

packages/core/src/client.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -927,6 +927,16 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
927927
*/
928928
public on(hook: 'stopUIProfiler', callback: () => void): () => void;
929929

930+
/**
931+
* A hook that is called when the orchestrion runtime hook injects diagnostics
932+
* channels into a module as it is loaded.
933+
*
934+
* The callback receives the name of the instrumented module.
935+
*
936+
* @returns {() => void} A function that, when executed, removes the registered callback.
937+
*/
938+
public on(hook: 'orchestrion.module-runtime-injected', callback: (moduleName: string) => void): () => void;
939+
930940
/**
931941
* Register a hook on this client.
932942
*/
@@ -1188,6 +1198,12 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
11881198
*/
11891199
public emit(hook: 'stopUIProfiler'): void;
11901200

1201+
/**
1202+
* Emit a hook event when the orchestrion runtime hook injects diagnostics
1203+
* channels into a module as it is loaded.
1204+
*/
1205+
public emit(hook: 'orchestrion.module-runtime-injected', moduleName: string): void;
1206+
11911207
/**
11921208
* Emit a hook that was previously registered via `on()`.
11931209
*/

packages/server-utils/src/integrations/tracing-channel/express/index.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,35 @@ import type { IntegrationFn } from '@sentry/core';
33
import { defineIntegration, waitForTracingChannelBinding } from '@sentry/core';
44
import type { ExpressIntegrationOptions } from './types';
55
import { instrumentExpress } from './instrumentation';
6+
import { expressModuleNames } from '../../../orchestrion/config/express';
7+
import { instrumentOrchestrion, instrumentOrchestrionLazy } from '../../../orchestrion/instrumentation';
68

79
// NOTE: this uses the same name as the OTel integration by design.
810
// When enabled, the OTel 'Express' integration is omitted from the default set.
911
const INTEGRATION_NAME = 'Express' as const;
1012

1113
const _expressChannelIntegration = ((options: ExpressIntegrationOptions = {}) => {
14+
// `tracingChannel` is unavailable before Node 18.19 so do nothing in that case.
15+
if (!diagnosticsChannel.tracingChannel) {
16+
return {
17+
name: INTEGRATION_NAME,
18+
};
19+
}
20+
21+
const instrumentFn = () => {
22+
instrumentExpress(options, diagnosticsChannel.tracingChannel);
23+
};
24+
1225
return {
1326
name: INTEGRATION_NAME,
1427
setupOnce() {
15-
// `tracingChannel` is unavailable before Node 18.19 so do nothing in that case.
16-
if (!diagnosticsChannel.tracingChannel) {
17-
return;
18-
}
19-
2028
waitForTracingChannelBinding(() => {
21-
instrumentExpress(options, diagnosticsChannel.tracingChannel);
29+
instrumentOrchestrion(expressModuleNames, instrumentFn);
2230
});
2331
},
32+
setup(client) {
33+
instrumentOrchestrionLazy(client, expressModuleNames, instrumentFn);
34+
},
2435
};
2536
}) satisfies IntegrationFn;
2637

packages/server-utils/src/orchestrion/config/express.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { InstrumentationConfig } from '@apm-js-collab/code-transformer';
2+
import { uniq } from '@sentry/core';
23

34
export const expressConfig = [
45
// Express funnels every middleware/route handler through a single method on
@@ -54,7 +55,9 @@ export const expressConfig = [
5455
module: { name: 'router', versionRange: '>=2.0.0 <3', filePath: 'index.js' },
5556
functionQuery: { expressionName: 'use', kind: 'Sync' },
5657
},
57-
] satisfies InstrumentationConfig[];
58+
] as const satisfies InstrumentationConfig[];
59+
60+
export const expressModuleNames = uniq(expressConfig.map(config => config.module.name));
5861

5962
export const expressChannels = {
6063
// Express v4 runs each layer's handler through `Layer.prototype.handle_request`

packages/server-utils/src/orchestrion/detect.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,11 @@ export function detectOrchestrionSetup(): void {
5050
: '[Sentry] Bundler plugin did not run',
5151
);
5252
}
53+
54+
/**
55+
* Get a list of all module names (e.g. express, @nestjs/core, ...) that have been injected by orchestrion, either at runtime or via a bundler plugin.
56+
*/
57+
export function getOrchestrionInjectedModules(): string[] {
58+
const { runtime, bundler } = GLOBAL_OBJ.__SENTRY_ORCHESTRION__ ?? {};
59+
return [...(runtime ?? []), ...(bundler ?? [])];
60+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import type { Client } from '@sentry/core';
2+
import { addNonEnumerableProperty } from '@sentry/core';
3+
import { getOrchestrionInjectedModules } from './detect';
4+
5+
const INSTRUMENTATION_FN_SYMBOL = Symbol.for('InstrumentationFn');
6+
type InstrumentationFn = (() => void) & { [INSTRUMENTATION_FN_SYMBOL]?: boolean };
7+
8+
/**
9+
* Instrument the provided callback to run when one of the provided module names has been orchestrion-injected already.
10+
* Make sure to pass the same callback instance to this as is passed to `instrumentOrchestrionLazy`.
11+
*/
12+
export function instrumentOrchestrion(moduleNames: string[], callback: InstrumentationFn) {
13+
if (hasBeenInjected(callback)) {
14+
return;
15+
}
16+
17+
const modules = getOrchestrionInjectedModules();
18+
if (moduleNames.some(name => modules.includes(name))) {
19+
callback();
20+
markAsInjected(callback);
21+
}
22+
}
23+
24+
/**
25+
* Run the provided instrumentation callback when one of the provided module names has been orchestrion-injected at runtime.
26+
* This listens to the `orchestrion.module-runtime-injected` event and runs the callback when the event is emitted.
27+
* Make sure to pass the same callback instance to this as is passed to `instrumentOrchestrion`.
28+
*/
29+
export function instrumentOrchestrionLazy(client: Client, moduleNames: string[], callback: InstrumentationFn) {
30+
if (hasBeenInjected(callback)) {
31+
return;
32+
}
33+
34+
const cleanup = client.on('orchestrion.module-runtime-injected', (moduleName: string) => {
35+
if (moduleNames.includes(moduleName)) {
36+
if (hasBeenInjected(callback)) {
37+
cleanup();
38+
return;
39+
}
40+
callback();
41+
markAsInjected(callback);
42+
cleanup();
43+
}
44+
});
45+
}
46+
47+
function hasBeenInjected(callback: InstrumentationFn) {
48+
return callback[INSTRUMENTATION_FN_SYMBOL] ?? false;
49+
}
50+
51+
function markAsInjected(callback: InstrumentationFn) {
52+
addNonEnumerableProperty(callback, INSTRUMENTATION_FN_SYMBOL, true);
53+
}

packages/server-utils/src/orchestrion/runtime/register.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { debug, GLOBAL_OBJ } from '@sentry/core';
1+
import { debug, getClient, GLOBAL_OBJ } from '@sentry/core';
22
import { createRequire } from 'node:module';
33
import * as Module from 'node:module';
44
import { pathToFileURL } from 'node:url';
@@ -115,6 +115,8 @@ export function registerDiagnosticsChannelInjection(options?: RegisterDiagnostic
115115
GLOBAL_OBJ.__SENTRY_ORCHESTRION__ = GLOBAL_OBJ.__SENTRY_ORCHESTRION__ || {};
116116
GLOBAL_OBJ.__SENTRY_ORCHESTRION__.runtime = GLOBAL_OBJ.__SENTRY_ORCHESTRION__.runtime || [];
117117
GLOBAL_OBJ.__SENTRY_ORCHESTRION__.runtime.push(event.moduleName);
118+
119+
getClient()?.emit('orchestrion.module-runtime-injected', event.moduleName);
118120
});
119121

120122
initialize({ instrumentations: SENTRY_INSTRUMENTATIONS });

0 commit comments

Comments
 (0)