Skip to content

Commit 03cf182

Browse files
committed
fix(node): Register diagnostics-channel injection in NodeClient constructor
1 parent 4e2fa6b commit 03cf182

3 files changed

Lines changed: 67 additions & 20 deletions

File tree

packages/node/src/sdk/client.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,18 @@ import {
1717
type SentryTracerProvider,
1818
setOpenTelemetryContextAsyncContextStrategy,
1919
} from '@sentry/opentelemetry';
20+
import { registerDiagnosticsChannelInjection } from '@sentry/server-runtime-injection/register';
2021
import { setAsyncLocalStorageAsyncContextStrategy } from '@sentry/server-utils';
2122
import { isMainThread, threadId } from 'worker_threads';
2223
import { DEBUG_BUILD } from '../debug-build';
2324
import type { NodeClientOptions } from '../types';
2425

2526
const DEFAULT_CLIENT_REPORT_FLUSH_INTERVAL_MS = 60_000; // 60s was chosen arbitrarily
2627

28+
// Treeshakable guard to remove all code related to runtime diagnostics-channel injection. Set to
29+
// `false` at build time by the Sentry bundler plugins' `bundleSizeOptimizations.excludeChannelInjection`.
30+
declare const __SENTRY_CHANNEL_INJECTION__: boolean | undefined;
31+
2732
/** A client for using Sentry with Node & OpenTelemetry. */
2833
export class NodeClient extends ServerRuntimeClient<NodeClientOptions> {
2934
public traceProvider: SentryTracerProvider | undefined;
@@ -82,6 +87,21 @@ export class NodeClient extends ServerRuntimeClient<NodeClientOptions> {
8287
// Same constructor anchoring as above: every client must continue incoming (remote) traces,
8388
// also manually constructed ones that never run `initOtel`.
8489
registerPrepareSpanScope(this);
90+
91+
// Install the channel-based (orchestrion diagnostics-channel) instrumentation hooks here, in the
92+
// constructor, so that every client installs them — not only the one built by the Node SDK's
93+
// `init()`. Downstream SDKs construct a client without going through that path, and would
94+
// otherwise never install the hooks. Registration is idempotent (a global marker guards it), so
95+
// a second construction is harmless. The channel integrations capture errors as well as spans,
96+
// so this is independent of tracing. Opt out at runtime with `enableRuntimeChannelInjection:
97+
// false`, or at build time via the bundler plugins'
98+
// `bundleSizeOptimizations.excludeChannelInjection` (which tree-shakes this whole block away).
99+
if (
100+
(typeof __SENTRY_CHANNEL_INJECTION__ === 'undefined' || __SENTRY_CHANNEL_INJECTION__) &&
101+
options.enableRuntimeChannelInjection !== false
102+
) {
103+
registerDiagnosticsChannelInjection();
104+
}
85105
}
86106

87107
/** @inheritDoc */

packages/node/src/sdk/index.ts

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import {
1717
} from '@sentry/core';
1818
import { isMainThread, parentPort } from 'node:worker_threads';
1919
import { detectOrchestrionSetup, getErrorIntegrations, getTracingIntegrations } from '@sentry/server-utils';
20-
import { registerDiagnosticsChannelInjection } from '@sentry/server-runtime-injection/register';
2120
import { DEBUG_BUILD } from '../debug-build';
2221
import { childProcessIntegration } from '../integrations/childProcess';
2322
import { consoleIntegration } from '../integrations/console';
@@ -41,10 +40,6 @@ import { defaultStackParser, getSentryRelease } from './api';
4140
import { NodeClient } from './client';
4241
import { initOpenTelemetry } from './initOtel';
4342

44-
// Treeshakable guard to remove all code related to runtime diagnostics-channel injection. Set to
45-
// `false` at build time by the Sentry bundler plugins' `bundleSizeOptimizations.excludeChannelInjection`.
46-
declare const __SENTRY_CHANNEL_INJECTION__: boolean | undefined;
47-
4843
/**
4944
* Get the base default integrations shared by all Node SDK default-integration sets.
5045
*/
@@ -134,9 +129,10 @@ function _init(
134129

135130
applySdkMetadata(options, 'node');
136131

137-
// Enable debug logging before channel-injection registration below, so its failure modes (e.g. no
138-
// available Node hook API, dep-resolution errors) actually surface. `getClientOptions` resolves
139-
// `debug` the same way for the client; resolving it here as well keeps the two in agreement.
132+
// Enable debug logging before the client is created, so failure modes during setup (e.g. the
133+
// channel-injection registration finding no available Node hook API, or dep-resolution errors)
134+
// actually surface. `getClientOptions` resolves `debug` the same way for the client; resolving it
135+
// here as well keeps the two in agreement.
140136
if (envToBool(options.debug ?? process.env.SENTRY_DEBUG)) {
141137
if (DEBUG_BUILD) {
142138
debug.enable();
@@ -158,18 +154,6 @@ function _init(
158154
tracesSampleRate: getTracesSampleRate(options.tracesSampleRate),
159155
};
160156

161-
// Install the channel-based (orchestrion diagnostics-channel) instrumentation hooks by default,
162-
// independent of tracing — the channel integrations also capture errors, not just spans. Opt out at
163-
// runtime with `enableRuntimeChannelInjection: false`, or at build time via the bundler plugins'
164-
// `bundleSizeOptimizations.excludeChannelInjection` (which tree-shakes this whole block away).
165-
// Install as early as possible, before the app imports its instrumented modules.
166-
if (
167-
(typeof __SENTRY_CHANNEL_INJECTION__ === 'undefined' || __SENTRY_CHANNEL_INJECTION__) &&
168-
options.enableRuntimeChannelInjection !== false
169-
) {
170-
registerDiagnosticsChannelInjection();
171-
}
172-
173157
// Only use Node SDK defaults if none provided.
174158
const defaultIntegrations = options.defaultIntegrations ?? getDefaultIntegrationsImpl(optionsWithResolvedTracing);
175159

packages/node/test/sdk/diagnosticsChannelInjection.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ vi.mock('@sentry/server-utils', async importOriginal => {
1414
return { ...actual, detectOrchestrionSetup };
1515
});
1616

17+
import { NodeClient } from '../../src/sdk/client';
1718
import { init } from '../../src/sdk';
19+
import { getDefaultNodeClientOptions } from '../helpers/getDefaultNodeClientOptions';
1820
import { cleanupOtel, resetGlobals } from '../helpers/mockSdkInit';
1921

2022
// eslint-disable-next-line no-var
@@ -83,3 +85,44 @@ describe('diagnostics-channel injection', () => {
8385
}
8486
});
8587
});
88+
89+
// The registration lives in the `NodeClient` constructor, so downstream SDKs that build a client
90+
// directly (without going through the Node SDK's `init()`) still install the injection hooks.
91+
describe('diagnostics-channel injection on direct client construction', () => {
92+
beforeEach(() => {
93+
global.__SENTRY__ = {};
94+
vi.spyOn(debug, 'enable').mockImplementation(() => undefined);
95+
});
96+
97+
afterEach(() => {
98+
cleanupOtel();
99+
resetGlobals();
100+
vi.clearAllMocks();
101+
});
102+
103+
it('registers the injection hooks when a NodeClient is constructed directly', () => {
104+
new NodeClient(getDefaultNodeClientOptions({ enableOpenTelemetrySetup: false }));
105+
106+
expect(registerDiagnosticsChannelInjection).toHaveBeenCalledTimes(1);
107+
});
108+
109+
it('does not register the injection hooks when `enableRuntimeChannelInjection` is false', () => {
110+
new NodeClient(
111+
getDefaultNodeClientOptions({ enableRuntimeChannelInjection: false, enableOpenTelemetrySetup: false }),
112+
);
113+
114+
expect(registerDiagnosticsChannelInjection).not.toHaveBeenCalled();
115+
});
116+
117+
it('does not register the injection hooks when the `__SENTRY_CHANNEL_INJECTION__` build flag is false', () => {
118+
vi.stubGlobal('__SENTRY_CHANNEL_INJECTION__', false);
119+
120+
try {
121+
new NodeClient(getDefaultNodeClientOptions({ enableOpenTelemetrySetup: false }));
122+
123+
expect(registerDiagnosticsChannelInjection).not.toHaveBeenCalled();
124+
} finally {
125+
vi.unstubAllGlobals();
126+
}
127+
});
128+
});

0 commit comments

Comments
 (0)