Skip to content

Commit 93dba1a

Browse files
committed
fix(server-utils): Ensure all orchestrion instrumentation lazy registers
1 parent 3e8c1ea commit 93dba1a

65 files changed

Lines changed: 1009 additions & 1074 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/node/src/integrations/tracing/dataloader/index.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,24 @@ const INTEGRATION_NAME = 'Dataloader' as const;
99
export const instrumentDataloader = generateInstrumentOnce(INTEGRATION_NAME, () => new DataloaderInstrumentation());
1010

1111
const _dataloaderIntegration = (() => {
12+
// Decide in setup/setupOnce, not in the factory: the runtime channel injection runs inside `Sentry.init()`,
13+
// after the integrations array has already been built, so `isOrchestrionInjected()` is only
14+
// reliable by `setup`. When the diagnostics channels are injected (runtime hook or bundler
15+
// plugin), subscribe to them (the channel integration needs the client to register its
16+
// injection listener); otherwise fall back to the vendored OTel instrumentation.
17+
1218
return {
1319
name: INTEGRATION_NAME,
1420
setupOnce() {
15-
// Decide here, not in the factory: the runtime channel injection runs inside `Sentry.init()`,
16-
// after the integrations array has already been built, so `isOrchestrionInjected()` is only
17-
// reliable by `setupOnce`. When the diagnostics channels are injected (runtime hook or bundler
18-
// plugin), subscribe to them; otherwise fall back to the vendored OTel instrumentation.
19-
if (isOrchestrionInjected()) {
20-
dataloaderChannelIntegration().setupOnce?.();
21-
} else {
21+
if (!isOrchestrionInjected()) {
2222
instrumentDataloader();
2323
}
2424
},
25+
setup(client) {
26+
if (isOrchestrionInjected()) {
27+
dataloaderChannelIntegration().setup?.(client);
28+
}
29+
},
2530
};
2631
}) satisfies IntegrationFn;
2732

packages/node/src/integrations/tracing/knex/index.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,24 @@ const INTEGRATION_NAME = 'Knex' as const;
99
export const instrumentKnex = generateInstrumentOnce(INTEGRATION_NAME, () => new KnexInstrumentation());
1010

1111
const _knexIntegration = (() => {
12+
// Decide in setup/setupOnce, not in the factory: the runtime channel injection runs inside `Sentry.init()`,
13+
// after the integrations array has already been built, so `isOrchestrionInjected()` is only
14+
// reliable by `setup`. When the diagnostics channels are injected (runtime hook or bundler
15+
// plugin), subscribe to them (the channel integration needs the client to register its
16+
// injection listener); otherwise fall back to the vendored OTel instrumentation.
17+
1218
return {
1319
name: INTEGRATION_NAME,
1420
setupOnce() {
15-
// Prefer the diagnostics-channel subscriber when orchestrion injected its channels; otherwise
16-
// fall back to the vendored OTel instrumentation. `isOrchestrionInjected()` is only reliable by
17-
// `setupOnce` (the runtime injection runs during `Sentry.init()`, after integrations are built).
18-
if (isOrchestrionInjected()) {
19-
knexChannelIntegration().setupOnce?.();
20-
} else {
21+
if (!isOrchestrionInjected()) {
2122
instrumentKnex();
2223
}
2324
},
25+
setup(client) {
26+
if (isOrchestrionInjected()) {
27+
knexChannelIntegration().setup?.(client);
28+
}
29+
},
2430
};
2531
}) satisfies IntegrationFn;
2632

packages/server-utils/src/integrations/tracing-channel/amqplib.ts

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,13 @@ import * as diagnosticsChannel from 'node:diagnostics_channel';
33
import type { IntegrationFn, Span, SpanAttributes } from '@sentry/core';
44
import {
55
continueTrace,
6-
debug,
76
defineIntegration,
87
getTraceData,
98
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
109
SPAN_KIND,
1110
SPAN_STATUS_ERROR,
1211
startInactiveSpan,
1312
timestampInSeconds,
14-
waitForTracingChannelBinding,
1513
} from '@sentry/core';
1614
// eslint-disable-next-line typescript/no-deprecated -- NET_PEER_* emitted alongside SERVER_* for backwards compatibility (TODO(v11): remove)
1715
import {
@@ -27,9 +25,10 @@ import {
2725
SERVER_PORT,
2826
URL_FULL,
2927
} from '@sentry/conventions/attributes';
30-
import { DEBUG_BUILD } from '../../debug-build';
3128
import { CHANNELS } from '../../orchestrion/channels';
3229
import { bindTracingChannelToSpan } from '../../tracing-channel';
30+
import { invokeOrchestrionInstrumentation } from '../../orchestrion/instrumentation';
31+
import { amqplibModuleNames } from '../../orchestrion/config/amqplib';
3332

3433
// NOTE: this uses the same name as the OTel integration by design.
3534
// When enabled, the OTel 'Amqplib' integration is omitted from the default set.
@@ -156,32 +155,20 @@ interface AmqpConnectContext {
156155

157156
const NOOP = (): void => {};
158157

159-
// Guards against subscribing to the amqplib channels more than once in a process. Core dedupes
160-
// `setupOnce` by integration *name*, which is not enough here: the Deno SDK wraps this integration
161-
// under a different name (`DenoAmqplib`) via `extendIntegration`, so adding both would otherwise run
162-
// the subscribe logic twice and emit duplicate spans for every operation.
163-
let subscribed = false;
158+
function instrumentAmqplib(): void {
159+
subscribeConnect();
160+
subscribePublish();
161+
subscribeConfirmPublish();
162+
subscribeConsume();
163+
subscribeDispatch();
164+
subscribeSettle();
165+
}
164166

165167
const _amqplibChannelIntegration = (() => {
166168
return {
167169
name: INTEGRATION_NAME,
168-
setupOnce() {
169-
// `tracingChannel` is unavailable before Node 18.19 so do nothing in that case.
170-
if (!diagnosticsChannel.tracingChannel || subscribed) {
171-
return;
172-
}
173-
subscribed = true;
174-
175-
DEBUG_BUILD && debug.log('[orchestrion:amqplib] subscribing to amqplib tracing channels');
176-
177-
waitForTracingChannelBinding(() => {
178-
subscribeConnect();
179-
subscribePublish();
180-
subscribeConfirmPublish();
181-
subscribeConsume();
182-
subscribeDispatch();
183-
subscribeSettle();
184-
});
170+
setup(client) {
171+
invokeOrchestrionInstrumentation(client, amqplibModuleNames, instrumentAmqplib, []);
185172
},
186173
};
187174
}) satisfies IntegrationFn;

packages/server-utils/src/integrations/tracing-channel/anthropic.ts

Lines changed: 22 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {
44
_INTERNAL_shouldSkipAiProviderWrapping,
55
addAnthropicRequestAttributes,
66
addAnthropicResponseAttributes,
7-
debug,
87
defineIntegration,
98
extractAnthropicRequestAttributes,
109
GEN_AI_REQUEST_MODEL_ATTRIBUTE,
@@ -14,11 +13,11 @@ import {
1413
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
1514
shouldEnableTruncation,
1615
startInactiveSpan,
17-
waitForTracingChannelBinding,
1816
} from '@sentry/core';
19-
import { DEBUG_BUILD } from '../../debug-build';
2017
import { CHANNELS } from '../../orchestrion/channels';
2118
import { bindTracingChannelToSpan } from '../../tracing-channel';
19+
import { invokeOrchestrionInstrumentation } from '../../orchestrion/instrumentation';
20+
import { anthropicAiModuleNames } from '../../orchestrion/config/anthropic-ai';
2221

2322
// Same name as the OTel integration by design: when enabled, the OTel 'Anthropic_AI'
2423
// integration is dropped from the default set (see the Node opt-in loader).
@@ -47,39 +46,30 @@ interface AnthropicChannelContext {
4746
result?: unknown;
4847
}
4948

50-
let subscribed = false;
49+
function instrumentAnthropic(options: AnthropicAiOptions): void {
50+
for (const { channel, operation, methodPath, stream } of INSTRUMENTED_CHANNELS) {
51+
bindTracingChannelToSpan(
52+
diagnosticsChannel.tracingChannel<AnthropicChannelContext>(channel),
53+
data => createGenAiSpan(data, operation, methodPath, options),
54+
{
55+
beforeSpanEnd: (span, data) => {
56+
addAnthropicResponseAttributes(
57+
span,
58+
data.result as AnthropicAiResponse,
59+
resolveAIRecordingOptions(options).recordOutputs,
60+
);
61+
},
62+
deferSpanEnd: ({ span, data }) => wrapStreamResult(span, data, stream, options),
63+
},
64+
);
65+
}
66+
}
5167

5268
const _anthropicChannelIntegration = ((options: AnthropicAiOptions = {}) => {
5369
return {
5470
name: INTEGRATION_NAME,
55-
setupOnce() {
56-
// tracingChannel is unavailable before Node 18.19 and prevent double-subscribe
57-
if (!diagnosticsChannel.tracingChannel || subscribed) {
58-
return;
59-
}
60-
subscribed = true;
61-
62-
// `bindTracingChannelToSpan` needs the async-context binding that `initOpenTelemetry()` registers
63-
// after `setupOnce` runs, so wait for it before subscribing.
64-
waitForTracingChannelBinding(() => {
65-
for (const { channel, operation, methodPath, stream } of INSTRUMENTED_CHANNELS) {
66-
DEBUG_BUILD && debug.log(`[orchestrion:anthropic] subscribing to channel "${channel}"`);
67-
bindTracingChannelToSpan(
68-
diagnosticsChannel.tracingChannel<AnthropicChannelContext>(channel),
69-
data => createGenAiSpan(data, operation, methodPath, options),
70-
{
71-
beforeSpanEnd: (span, data) => {
72-
addAnthropicResponseAttributes(
73-
span,
74-
data.result as AnthropicAiResponse,
75-
resolveAIRecordingOptions(options).recordOutputs,
76-
);
77-
},
78-
deferSpanEnd: ({ span, data }) => wrapStreamResult(span, data, stream, options),
79-
},
80-
);
81-
}
82-
});
71+
setup(client) {
72+
invokeOrchestrionInstrumentation(client, anthropicAiModuleNames, instrumentAnthropic, [options]);
8373
},
8474
};
8575
}) satisfies IntegrationFn;

packages/server-utils/src/integrations/tracing-channel/dataloader.ts

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,19 @@
11
import * as diagnosticsChannel from 'node:diagnostics_channel';
22
import type { IntegrationFn, Span, StartSpanOptions } from '@sentry/core';
33
import {
4-
debug,
54
defineIntegration,
65
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
76
SPAN_KIND,
87
startInactiveSpan,
98
startSpan,
10-
waitForTracingChannelBinding,
119
} from '@sentry/core';
12-
import { DEBUG_BUILD } from '../../debug-build';
1310
import type { ChannelName } from '../../orchestrion/channels';
1411
import { CHANNELS } from '../../orchestrion/channels';
1512
import type { TracingChannelPayloadWithSpan } from '../../tracing-channel';
1613
import { bindTracingChannelToSpan } from '../../tracing-channel';
14+
import { invokeOrchestrionInstrumentation } from '../../orchestrion/instrumentation';
15+
import { dataloaderModuleNames } from '../../orchestrion/config/dataloader';
1716

18-
// NOTE: this uses the same name as the OTel integration by design.
19-
// When enabled, the OTel 'Dataloader' integration is omitted from the default set.
2017
const INTEGRATION_NAME = 'Dataloader' as const;
2118

2219
const MODULE_NAME = 'dataloader';
@@ -78,25 +75,20 @@ function makeSpanOptions(loader: DataLoaderInstance | undefined, operation: Oper
7875
};
7976
}
8077

78+
function instrumentDataloader(): void {
79+
subscribeConstruct();
80+
subscribeLoad();
81+
subscribeSimpleOperation(CHANNELS.DATALOADER_LOAD_MANY, 'loadMany');
82+
subscribeSimpleOperation(CHANNELS.DATALOADER_PRIME, 'prime');
83+
subscribeSimpleOperation(CHANNELS.DATALOADER_CLEAR, 'clear');
84+
subscribeSimpleOperation(CHANNELS.DATALOADER_CLEAR_ALL, 'clearAll');
85+
}
86+
8187
const _dataloaderChannelIntegration = (() => {
8288
return {
8389
name: INTEGRATION_NAME,
84-
setupOnce() {
85-
// `tracingChannel` is unavailable before Node 18.19 so do nothing in that case.
86-
if (!diagnosticsChannel.tracingChannel) {
87-
return;
88-
}
89-
90-
DEBUG_BUILD && debug.log('[orchestrion:dataloader] subscribing to dataloader tracing channels');
91-
92-
waitForTracingChannelBinding(() => {
93-
subscribeConstruct();
94-
subscribeLoad();
95-
subscribeSimpleOperation(CHANNELS.DATALOADER_LOAD_MANY, 'loadMany');
96-
subscribeSimpleOperation(CHANNELS.DATALOADER_PRIME, 'prime');
97-
subscribeSimpleOperation(CHANNELS.DATALOADER_CLEAR, 'clear');
98-
subscribeSimpleOperation(CHANNELS.DATALOADER_CLEAR_ALL, 'clearAll');
99-
});
90+
setup(client) {
91+
invokeOrchestrionInstrumentation(client, dataloaderModuleNames, instrumentDataloader, []);
10092
},
10193
};
10294
}) satisfies IntegrationFn;

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

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import * as diagnosticsChannel from 'node:diagnostics_channel';
21
import type { IntegrationFn } from '@sentry/core';
3-
import { defineIntegration, waitForTracingChannelBinding } from '@sentry/core';
2+
import { defineIntegration } from '@sentry/core';
43
import type { ExpressIntegrationOptions } from './types';
54
import { instrumentExpress } from './instrumentation';
5+
import { expressModuleNames } from '../../../orchestrion/config/express';
6+
import { invokeOrchestrionInstrumentation } from '../../../orchestrion/instrumentation';
67

78
// NOTE: this uses the same name as the OTel integration by design.
89
// When enabled, the OTel 'Express' integration is omitted from the default set.
@@ -11,15 +12,8 @@ const INTEGRATION_NAME = 'Express' as const;
1112
const _expressChannelIntegration = ((options: ExpressIntegrationOptions = {}) => {
1213
return {
1314
name: INTEGRATION_NAME,
14-
setupOnce() {
15-
// `tracingChannel` is unavailable before Node 18.19 so do nothing in that case.
16-
if (!diagnosticsChannel.tracingChannel) {
17-
return;
18-
}
19-
20-
waitForTracingChannelBinding(() => {
21-
instrumentExpress(options, diagnosticsChannel.tracingChannel);
22-
});
15+
setup(client) {
16+
invokeOrchestrionInstrumentation(client, expressModuleNames, instrumentExpress, [options]);
2317
},
2418
};
2519
}) satisfies IntegrationFn;

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

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type * as diagnosticsChannel from 'node:diagnostics_channel';
1+
import * as diagnosticsChannel from 'node:diagnostics_channel';
22
import { HTTP_ROUTE } from '@sentry/conventions/attributes';
33
import type { Span } from '@sentry/core';
44
import {
@@ -45,16 +45,8 @@ const ATTR_EXPRESS_TYPE = 'express.type';
4545

4646
const NOOP = (): void => {};
4747

48-
let _isInstrumented = false;
49-
50-
export function instrumentExpress(
51-
options: ExpressIntegrationOptions,
52-
tracingChannel: typeof diagnosticsChannel.tracingChannel,
53-
): void {
54-
if (_isInstrumented) {
55-
return;
56-
}
57-
_isInstrumented = true;
48+
export function instrumentExpress(options: ExpressIntegrationOptions): void {
49+
const tracingChannel = diagnosticsChannel.tracingChannel;
5850

5951
// Record each layer's registered path *pattern* as it is registered, so the
6052
// matched route can be reconstructed with its parameters intact at request

packages/server-utils/src/integrations/tracing-channel/generic-pool.ts

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
import * as diagnosticsChannel from 'node:diagnostics_channel';
22
import type { IntegrationFn } from '@sentry/core';
3-
import {
4-
defineIntegration,
5-
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
6-
startInactiveSpan,
7-
waitForTracingChannelBinding,
8-
} from '@sentry/core';
3+
import { defineIntegration, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/core';
94
import { CHANNELS } from '../../orchestrion/channels';
105
import { bindTracingChannelToSpan } from '../../tracing-channel';
6+
import { invokeOrchestrionInstrumentation } from '../../orchestrion/instrumentation';
7+
import { genericPoolModuleNames } from '../../orchestrion/config/generic-pool';
118

129
// Same name as the OTel integration by design — when enabled, the OTel
1310
// 'GenericPool' integration is omitted from the default set.
@@ -20,13 +17,8 @@ interface GenericPoolAcquireContext {
2017
const _genericPoolChannelIntegration = (() => {
2118
return {
2219
name: INTEGRATION_NAME,
23-
setupOnce() {
24-
// `tracingChannel` is unavailable before Node 18.19 so do nothing in that case.
25-
if (!diagnosticsChannel.tracingChannel) {
26-
return;
27-
}
28-
29-
waitForTracingChannelBinding(() => instrumentGenericPool());
20+
setup(client) {
21+
invokeOrchestrionInstrumentation(client, genericPoolModuleNames, instrumentGenericPool, []);
3022
},
3123
};
3224
}) satisfies IntegrationFn;

0 commit comments

Comments
 (0)