Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions packages/node/src/sdk/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,18 @@ import {
type SentryTracerProvider,
setOpenTelemetryContextAsyncContextStrategy,
} from '@sentry/opentelemetry';
import { registerDiagnosticsChannelInjection } from '@sentry/server-runtime-injection/register';
import { setAsyncLocalStorageAsyncContextStrategy } from '@sentry/server-utils';
import { isMainThread, threadId } from 'worker_threads';
import { DEBUG_BUILD } from '../debug-build';
import type { NodeClientOptions } from '../types';

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

// Treeshakable guard to remove all code related to runtime diagnostics-channel injection. Set to
// `false` at build time by the Sentry bundler plugins' `bundleSizeOptimizations.excludeChannelInjection`.
declare const __SENTRY_CHANNEL_INJECTION__: boolean | undefined;

/** A client for using Sentry with Node & OpenTelemetry. */
export class NodeClient extends ServerRuntimeClient<NodeClientOptions> {
public traceProvider: SentryTracerProvider | undefined;
Expand Down Expand Up @@ -82,6 +87,21 @@ export class NodeClient extends ServerRuntimeClient<NodeClientOptions> {
// Same constructor anchoring as above: every client must continue incoming (remote) traces,
// also manually constructed ones that never run `initOtel`.
registerPrepareSpanScope(this);

// Install the channel-based (orchestrion diagnostics-channel) instrumentation hooks here, in the
// constructor, so that every client installs them — not only the one built by the Node SDK's
// `init()`. Downstream SDKs construct a client without going through that path, and would
// otherwise never install the hooks. Registration is idempotent (a global marker guards it), so
// a second construction is harmless. The channel integrations capture errors as well as spans,
// so this is independent of tracing. Opt out at runtime with `enableRuntimeChannelInjection:
// false`, or at build time via the bundler plugins'
// `bundleSizeOptimizations.excludeChannelInjection` (which tree-shakes this whole block away).
if (
(typeof __SENTRY_CHANNEL_INJECTION__ === 'undefined' || __SENTRY_CHANNEL_INJECTION__) &&
options.enableRuntimeChannelInjection !== false
) {
registerDiagnosticsChannelInjection();
}
}

/** @inheritDoc */
Expand Down
24 changes: 4 additions & 20 deletions packages/node/src/sdk/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import {
} from '@sentry/core';
import { isMainThread, parentPort } from 'node:worker_threads';
import { detectOrchestrionSetup, getErrorIntegrations, getTracingIntegrations } from '@sentry/server-utils';
import { registerDiagnosticsChannelInjection } from '@sentry/server-runtime-injection/register';
import { DEBUG_BUILD } from '../debug-build';
import { childProcessIntegration } from '../integrations/childProcess';
import { consoleIntegration } from '../integrations/console';
Expand All @@ -41,10 +40,6 @@ import { defaultStackParser, getSentryRelease } from './api';
import { NodeClient } from './client';
import { initOpenTelemetry } from './initOtel';

// Treeshakable guard to remove all code related to runtime diagnostics-channel injection. Set to
// `false` at build time by the Sentry bundler plugins' `bundleSizeOptimizations.excludeChannelInjection`.
declare const __SENTRY_CHANNEL_INJECTION__: boolean | undefined;

/**
* Get the base default integrations shared by all Node SDK default-integration sets.
*/
Expand Down Expand Up @@ -134,9 +129,10 @@ function _init(

applySdkMetadata(options, 'node');

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

// Install the channel-based (orchestrion diagnostics-channel) instrumentation hooks by default,
// independent of tracing — the channel integrations also capture errors, not just spans. Opt out at
// runtime with `enableRuntimeChannelInjection: false`, or at build time via the bundler plugins'
// `bundleSizeOptimizations.excludeChannelInjection` (which tree-shakes this whole block away).
// Install as early as possible, before the app imports its instrumented modules.
if (
(typeof __SENTRY_CHANNEL_INJECTION__ === 'undefined' || __SENTRY_CHANNEL_INJECTION__) &&
options.enableRuntimeChannelInjection !== false
) {
registerDiagnosticsChannelInjection();
}

// Only use Node SDK defaults if none provided.
const defaultIntegrations = options.defaultIntegrations ?? getDefaultIntegrationsImpl(optionsWithResolvedTracing);

Expand Down
43 changes: 43 additions & 0 deletions packages/node/test/sdk/diagnosticsChannelInjection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ vi.mock('@sentry/server-utils', async importOriginal => {
return { ...actual, detectOrchestrionSetup };
});

import { NodeClient } from '../../src/sdk/client';
import { init } from '../../src/sdk';
import { getDefaultNodeClientOptions } from '../helpers/getDefaultNodeClientOptions';
import { cleanupOtel, resetGlobals } from '../helpers/mockSdkInit';

// eslint-disable-next-line no-var
Expand Down Expand Up @@ -83,3 +85,44 @@ describe('diagnostics-channel injection', () => {
}
});
});

// The registration lives in the `NodeClient` constructor, so downstream SDKs that build a client
// directly (without going through the Node SDK's `init()`) still install the injection hooks.
describe('diagnostics-channel injection on direct client construction', () => {
beforeEach(() => {
global.__SENTRY__ = {};
vi.spyOn(debug, 'enable').mockImplementation(() => undefined);
});

afterEach(() => {
cleanupOtel();
resetGlobals();
vi.clearAllMocks();
});

it('registers the injection hooks when a NodeClient is constructed directly', () => {
new NodeClient(getDefaultNodeClientOptions({ enableOpenTelemetrySetup: false }));

expect(registerDiagnosticsChannelInjection).toHaveBeenCalledTimes(1);
});

it('does not register the injection hooks when `enableRuntimeChannelInjection` is false', () => {
new NodeClient(
getDefaultNodeClientOptions({ enableRuntimeChannelInjection: false, enableOpenTelemetrySetup: false }),
);

expect(registerDiagnosticsChannelInjection).not.toHaveBeenCalled();
});

it('does not register the injection hooks when the `__SENTRY_CHANNEL_INJECTION__` build flag is false', () => {
vi.stubGlobal('__SENTRY_CHANNEL_INJECTION__', false);

try {
new NodeClient(getDefaultNodeClientOptions({ enableOpenTelemetrySetup: false }));

expect(registerDiagnosticsChannelInjection).not.toHaveBeenCalled();
} finally {
vi.unstubAllGlobals();
}
});
});
Loading