Skip to content

Commit 783398f

Browse files
nicohrubecclaude
andcommitted
feat(node): Add diagnostics-channel instrumentation for lru-memoizer
Adds an experimental orchestrion (diagnostics-channel) integration for lru-memoizer, active only when `experimentalUseDiagnosticsChannelInjection()` is opted into. It rebinds the memoized callback to the caller's active span (the channel equivalent of the OTel `context.bind`) and creates no spans. Purely additive — the vendored OTel integration stays the default and is filtered out via `replacedOtelIntegrationNames` only when opted in. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 86b7148 commit 783398f

6 files changed

Lines changed: 86 additions & 4 deletions

File tree

dev-packages/node-integration-tests/suites/tracing/lru-memoizer/test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,12 @@ describe('lru-memoizer', () => {
9090
},
9191
};
9292

93-
await createTestRunner().withFlags(...flags).expect(expectation).expect(expectation).start().completed();
93+
await createTestRunner()
94+
.withFlags(...flags)
95+
.expect(expectation)
96+
.expect(expectation)
97+
.start()
98+
.completed();
9499
});
95100
});
96101
});

packages/node/src/sdk/experimentalUseDiagnosticsChannelInjection.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { mysqlChannelIntegration, detectOrchestrionSetup } from '@sentry/server-utils/orchestrion';
1+
import {
2+
mysqlChannelIntegration,
3+
lruMemoizerChannelIntegration,
4+
detectOrchestrionSetup,
5+
} from '@sentry/server-utils/orchestrion';
26
import { registerDiagnosticsChannelInjection } from '@sentry/server-utils/orchestrion/register';
37
import type { DiagnosticsChannelInjection } from './diagnosticsChannelInjection';
48
import { setDiagnosticsChannelInjectionLoader } from './diagnosticsChannelInjection';
@@ -38,8 +42,8 @@ import { setDiagnosticsChannelInjectionLoader } from './diagnosticsChannelInject
3842
export function experimentalUseDiagnosticsChannelInjection(): void {
3943
setDiagnosticsChannelInjectionLoader(
4044
(): DiagnosticsChannelInjection => ({
41-
integrations: [mysqlChannelIntegration()],
42-
replacedOtelIntegrationNames: ['Mysql'],
45+
integrations: [mysqlChannelIntegration(), lruMemoizerChannelIntegration()],
46+
replacedOtelIntegrationNames: ['Mysql', 'LruMemoizer'],
4347
register: registerDiagnosticsChannelInjection,
4448
detect: detectOrchestrionSetup,
4549
}),
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import * as diagnosticsChannel from 'node:diagnostics_channel';
2+
import type { IntegrationFn } from '@sentry/core';
3+
import { debug, defineIntegration, getActiveSpan, withActiveSpan } from '@sentry/core';
4+
import { DEBUG_BUILD } from '../../debug-build';
5+
import { CHANNELS } from '../../orchestrion/channels';
6+
7+
// Same name as the OTel integration by design — when enabled, the OTel
8+
// 'LruMemoizer' integration is omitted from the default set.
9+
const INTEGRATION_NAME = 'LruMemoizer';
10+
11+
interface LruMemoizerChannelContext {
12+
arguments: unknown[];
13+
}
14+
15+
const _lruMemoizerChannelIntegration = (() => {
16+
return {
17+
name: INTEGRATION_NAME,
18+
setupOnce() {
19+
// `tracingChannel` is unavailable before Node 18.19 — no-op instead of throwing (#21783).
20+
if (!diagnosticsChannel.tracingChannel) {
21+
return;
22+
}
23+
24+
DEBUG_BUILD && debug.log(`[orchestrion:lru-memoizer] subscribing to channel "${CHANNELS.LRU_MEMOIZER_LOAD}"`);
25+
const lruMemoizerCh = diagnosticsChannel.tracingChannel(CHANNELS.LRU_MEMOIZER_LOAD);
26+
27+
lruMemoizerCh.subscribe({
28+
// lru-memoizer queues the callback and fires it later via setImmediate, from a
29+
// different async context. Rebind it to the caller's active span (still correct
30+
// here, synchronously inside the memoized call) so nested spans parent correctly.
31+
// This is the channel equivalent of the OTel version's `context.bind(context.active(), cb)`.
32+
// orchestrion (kind: 'Callback') has already spliced its own wrapper into the last
33+
// arg by the time `start` fires, and only publishes `start` when that arg is a function.
34+
start(rawCtx) {
35+
const ctx = rawCtx as LruMemoizerChannelContext;
36+
const parentSpan = getActiveSpan();
37+
if (!parentSpan || ctx.arguments.length === 0) {
38+
return;
39+
}
40+
const cbIdx = ctx.arguments.length - 1;
41+
const orchestrionWrappedCb = ctx.arguments[cbIdx];
42+
if (typeof orchestrionWrappedCb !== 'function') {
43+
return;
44+
}
45+
const wrapped = orchestrionWrappedCb as (...a: unknown[]) => unknown;
46+
ctx.arguments[cbIdx] = function (this: unknown, ...args: unknown[]): unknown {
47+
return withActiveSpan(parentSpan, () => wrapped.apply(this, args));
48+
};
49+
},
50+
end() {},
51+
asyncStart() {},
52+
asyncEnd() {},
53+
error() {},
54+
});
55+
},
56+
};
57+
}) satisfies IntegrationFn;
58+
59+
/**
60+
* EXPERIMENTAL — orchestrion-driven lru-memoizer integration. Subscribes to
61+
* `orchestrion:lru-memoizer:load` (injected into `lru-memoizer/lib/async.js`'s
62+
* `memoizedFunction`). Creates no spans; only rebinds the memoized callback to the
63+
* caller's active span. Requires the orchestrion runtime hook or bundler plugin.
64+
*/
65+
export const lruMemoizerChannelIntegration = defineIntegration(_lruMemoizerChannelIntegration);

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
*/
1414
export const CHANNELS = {
1515
MYSQL_QUERY: 'orchestrion:mysql:query',
16+
LRU_MEMOIZER_LOAD: 'orchestrion:lru-memoizer:load',
1617
} as const;
1718

1819
export type ChannelName = (typeof CHANNELS)[keyof typeof CHANNELS];

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ export const SENTRY_INSTRUMENTATIONS: InstrumentationConfig[] = [
3232
// attach `'end'`/`'error'` listeners that finish the span.
3333
functionQuery: { expressionName: 'query', kind: 'Auto' },
3434
},
35+
{
36+
channelName: 'load',
37+
// `>=2.1.0` only: the named `function memoizedFunction()` the selector targets exists from 2.1.0
38+
module: { name: 'lru-memoizer', versionRange: '>=2.1.0 <4', filePath: 'lib/async.js' },
39+
functionQuery: { functionName: 'memoizedFunction', kind: 'Callback' },
40+
},
3541
];
3642

3743
/**
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export { detectOrchestrionSetup } from './detect';
22
export { mysqlChannelIntegration } from '../integrations/tracing-channel/mysql';
3+
export { lruMemoizerChannelIntegration } from '../integrations/tracing-channel/lru-memoizer';

0 commit comments

Comments
 (0)