Skip to content

Commit 682391e

Browse files
committed
feat(nest): wire up orchestrion instrumentation
Add `'Nest'` to the set of integrations that are implemented using Orchestrion, and which override a prior OTel based integration. The integration swap is moved into the `_init` method in the Node SDK, because the NestJS SDK (and other framework SDKs) will pass in its own defaultIntegrations array, which would bypass the old swap location. Now the swap is uniform for every framework SDK based on Node init, and respects `defaultIntegrations: false`. A new unit test is added that proves the opt-out leaves the defaults untouched, and opt-in replaces the named OTel integration with channel integrations.
1 parent d314ba3 commit 682391e

3 files changed

Lines changed: 114 additions & 27 deletions

File tree

packages/node/src/sdk/index.ts

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -30,32 +30,14 @@ export function getDefaultIntegrationsWithoutPerformance(): Integration[] {
3030

3131
/** Get the default integrations for the Node SDK. */
3232
export function getDefaultIntegrations(options: Options): Integration[] {
33-
const integrations: Integration[] = [
33+
return [
3434
...getDefaultIntegrationsWithoutPerformance(),
3535
// We only add performance integrations if tracing is enabled
3636
// Note that this means that without tracing enabled, e.g. `expressIntegration()` will not be added
3737
// This means that generally request isolation will work (because that is done by httpIntegration)
3838
// But `transactionName` will not be set automatically
3939
...(hasSpansEnabled(options) ? getAutoPerformanceIntegrations() : []),
4040
];
41-
42-
// When the app opted into diagnostics-channel injection (via
43-
// `experimentalUseDiagnosticsChannelInjection()`) AND span recording is
44-
// enabled, swap the channel-based integrations in place of OTel equivalents
45-
// so the two don't both instrument the same library.
46-
//
47-
// Every channel-based integration we ship today is a 1:1 replacement for an
48-
// OTel performance/tracing integration and produces nothing but spans (those
49-
// only come from `getAutoPerformanceIntegrations()` above), so it's gated on
50-
// span recording.
51-
if (isDiagnosticsChannelInjectionEnabled() && hasSpansEnabled(options)) {
52-
const diagnosticsChannelInjection = resolveDiagnosticsChannelInjection();
53-
if (diagnosticsChannelInjection) {
54-
const replaced = new Set(diagnosticsChannelInjection.replacedOtelIntegrationNames);
55-
return [...integrations.filter(i => !replaced.has(i.name)), ...diagnosticsChannelInjection.integrations];
56-
}
57-
}
58-
return integrations;
5941
}
6042

6143
/**
@@ -77,8 +59,7 @@ function _init(
7759
// EXPERIMENTAL: diagnostics-channel injection, opted into via
7860
// `experimentalUseDiagnosticsChannelInjection()`. Gated on span recording to
7961
// match the OTel integrations it replaces. With tracing off there are no
80-
// channel subscribers, so injecting is pointless work. `resolve...()` is
81-
// memoized, so `getDefaultIntegrations()` (below) sees the same instance.
62+
// channel subscribers, so injecting is pointless work.
8263
const diagnosticsChannelInjection =
8364
isDiagnosticsChannelInjectionEnabled() && hasSpansEnabled(options)
8465
? resolveDiagnosticsChannelInjection()
@@ -90,10 +71,26 @@ function _init(
9071
diagnosticsChannelInjection.register();
9172
}
9273

74+
// Only use Node SDK defaults if none provided.
75+
let defaultIntegrations = options.defaultIntegrations ?? getDefaultIntegrationsImpl(options);
76+
77+
// When opted into diagnostics-channel injection, swap the channel-based
78+
// integrations in place of their OTel equivalents so the two don't both
79+
// instrument the same library. Done here (rather than in
80+
// `getDefaultIntegrations`) so it also covers framework SDKs (e.g.
81+
// `@sentry/nestjs`) that pass their own `defaultIntegrations` array, and it
82+
// respects `defaultIntegrations: false` (not an array -> left untouched).
83+
if (diagnosticsChannelInjection && Array.isArray(defaultIntegrations)) {
84+
const replaced = new Set(diagnosticsChannelInjection.replacedOtelIntegrationNames);
85+
defaultIntegrations = [
86+
...defaultIntegrations.filter(integration => !replaced.has(integration.name)),
87+
...diagnosticsChannelInjection.integrations,
88+
];
89+
}
90+
9391
const client = initNodeCore({
9492
...options,
95-
// Only use Node SDK defaults if none provided
96-
defaultIntegrations: options.defaultIntegrations ?? getDefaultIntegrationsImpl(options),
93+
defaultIntegrations,
9794
});
9895

9996
// Add Node SDK specific OpenTelemetry setup
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import type { Integration } from '@sentry/core';
2+
import { debug } from '@sentry/core';
3+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4+
import { init } from '../../src/sdk';
5+
import { setDiagnosticsChannelInjectionLoader } from '../../src/sdk/diagnosticsChannelInjection';
6+
import { cleanupOtel, resetGlobals } from '../helpers/mockSdkInit';
7+
8+
// eslint-disable-next-line no-var
9+
declare var global: any;
10+
11+
const PUBLIC_DSN = 'https://username@domain/123';
12+
13+
function mockIntegration(name: string): Integration {
14+
return { name, setupOnce: vi.fn() };
15+
}
16+
17+
// These tests run in definition order: the first runs before any loader is set
18+
// (opt-out), the second sets it (opt-in). The module-level loader state is
19+
// isolated per test file by vitest, so it doesn't leak elsewhere.
20+
describe('diagnostics-channel injection integration swap', () => {
21+
beforeEach(() => {
22+
global.__SENTRY__ = {};
23+
vi.spyOn(debug, 'enable').mockImplementation(() => undefined);
24+
});
25+
26+
afterEach(() => {
27+
cleanupOtel();
28+
resetGlobals();
29+
vi.clearAllMocks();
30+
});
31+
32+
it('does not swap integrations when not opted in', () => {
33+
// Distinct names from the opt-in test below: `@sentry/core` only runs
34+
// `setupOnce` once per integration name per process, so reusing names across
35+
// tests would suppress later calls.
36+
const otelNest = mockIntegration('OptOutNest');
37+
const http = mockIntegration('OptOutHttp');
38+
39+
init({
40+
dsn: PUBLIC_DSN,
41+
tracesSampleRate: 1,
42+
skipOpenTelemetrySetup: true,
43+
defaultIntegrations: [otelNest, http],
44+
});
45+
46+
// No opt-in -> the supplied defaults are set up untouched.
47+
expect(otelNest.setupOnce).toHaveBeenCalledTimes(1);
48+
expect(http.setupOnce).toHaveBeenCalledTimes(1);
49+
});
50+
51+
it('replaces the named OTel integrations with the channel integrations, even when defaultIntegrations are supplied by a framework SDK', () => {
52+
const channelMysql = mockIntegration('Mysql');
53+
const channelNest = mockIntegration('Nest');
54+
const register = vi.fn();
55+
const detect = vi.fn();
56+
setDiagnosticsChannelInjectionLoader(() => ({
57+
integrations: [channelMysql, channelNest],
58+
replacedOtelIntegrationNames: ['Mysql', 'Nest'],
59+
register,
60+
detect,
61+
}));
62+
63+
// Mimics `@sentry/nestjs`, which prepends its OTel `Nest` integration to
64+
// its own `defaultIntegrations` array (so node's `getDefaultIntegrations`
65+
// swap never sees it; swap must happen in `init`).
66+
const otelNest = mockIntegration('Nest');
67+
const http = mockIntegration('Http');
68+
69+
init({
70+
dsn: PUBLIC_DSN,
71+
tracesSampleRate: 1,
72+
skipOpenTelemetrySetup: true,
73+
defaultIntegrations: [otelNest, http],
74+
});
75+
76+
// OTel 'Nest' filtered out, never set up.
77+
expect(otelNest.setupOnce).not.toHaveBeenCalled();
78+
// Channel replacements set up instead.
79+
expect(channelNest.setupOnce).toHaveBeenCalledTimes(1);
80+
expect(channelMysql.setupOnce).toHaveBeenCalledTimes(1);
81+
// Unrelated default preserved.
82+
expect(http.setupOnce).toHaveBeenCalledTimes(1);
83+
// Hooks installed and detection ran once.
84+
expect(register).toHaveBeenCalledTimes(1);
85+
expect(detect).toHaveBeenCalledTimes(1);
86+
});
87+
});
Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
import { lruMemoizerChannelIntegration } from '../integrations/tracing-channel/lru-memoizer';
22
import { mysqlChannelIntegration } from '../integrations/tracing-channel/mysql';
3+
import { nestjsChannelIntegration } from '../integrations/tracing-channel/nestjs';
34
import { postgresChannelIntegration } from '../integrations/tracing-channel/postgres';
45

56
export { detectOrchestrionSetup, isOrchestrionInjected } from './detect';
6-
export { lruMemoizerChannelIntegration, mysqlChannelIntegration, postgresChannelIntegration };
7-
// Not part of `channelIntegrations` below: `Nest` isn't a `@sentry/node` default integration (it's added
8-
// by the standalone `@sentry/nestjs` SDK), so it's not swapped via the generic default-integration path.
9-
export { nestjsChannelIntegration } from '../integrations/tracing-channel/nestjs';
7+
export { lruMemoizerChannelIntegration, mysqlChannelIntegration, nestjsChannelIntegration, postgresChannelIntegration };
108

119
/**
1210
* The canonical set of orchestrion diagnostics-channel integrations, keyed by their public
@@ -16,9 +14,14 @@ export { nestjsChannelIntegration } from '../integrations/tracing-channel/nestjs
1614
* opt-in helper (`experimentalUseDiagnosticsChannelInjection`) and its public
1715
* `diagnosticsChannelInjectionIntegrations()` map — picks it up automatically, so there's no separate
1816
* list to keep in sync.
17+
*
18+
* `Nest` is included even though it isn't a `@sentry/node` default integration: the swap runs in the Node
19+
* SDK's `_init` over the *final* `defaultIntegrations`, so it also replaces the OTel `Nest` that
20+
* `@sentry/nestjs` prepends to its own defaults.
1921
*/
2022
export const channelIntegrations = {
2123
postgresIntegration: postgresChannelIntegration,
2224
mysqlIntegration: mysqlChannelIntegration,
2325
lruMemoizerIntegration: lruMemoizerChannelIntegration,
26+
nestIntegration: nestjsChannelIntegration,
2427
} as const;

0 commit comments

Comments
 (0)