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
15 changes: 15 additions & 0 deletions packages/node/src/sdk/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import {
type AsyncLocalStorageLookup,
registerPrepareSpanScope,
type SentryTracerProvider,
setOpenTelemetryContextAsyncContextStrategy,
} from '@sentry/opentelemetry';
import { setAsyncLocalStorageAsyncContextStrategy } from '@sentry/server-utils';
import { isMainThread, threadId } from 'worker_threads';
import { DEBUG_BUILD } from '../debug-build';
import type { NodeClientOptions } from '../types';
Expand Down Expand Up @@ -82,6 +84,19 @@ export class NodeClient extends ServerRuntimeClient<NodeClientOptions> {
registerPrepareSpanScope(this);
}

/** @inheritDoc */
public init(): void {
// Must run before `super.init()`: channel-based integrations capture the strategy's
// AsyncLocalStorage via `getTracingChannelBinding()` during integration setup.
if (this.getOptions().enableOpenTelemetrySetup) {
this.asyncLocalStorageLookup = setOpenTelemetryContextAsyncContextStrategy();
} else {
this.asyncLocalStorageLookup = { asyncLocalStorage: setAsyncLocalStorageAsyncContextStrategy() };
}

super.init();
}

/** Get the OTEL tracer. */
public get tracer(): Tracer {
if (this._tracer) {
Expand Down
16 changes: 0 additions & 16 deletions packages/node/src/sdk/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ import {
requestDataIntegration,
stackParserFromStackParserOptions,
} from '@sentry/core';
import { setOpenTelemetryContextAsyncContextStrategy } from '@sentry/opentelemetry';
import { setAsyncLocalStorageAsyncContextStrategy } from '@sentry/server-utils';
import { isMainThread, parentPort } from 'node:worker_threads';
import { detectOrchestrionSetup } from '@sentry/server-utils/orchestrion';
import { registerDiagnosticsChannelInjection } from '@sentry/server-utils/orchestrion/register';
Expand Down Expand Up @@ -170,19 +168,6 @@ function _init(

const clientOptions = getClientOptions({ ...options, defaultIntegrations }, getDefaultIntegrationsImpl);

// When Sentry does not own an OpenTelemetry tracer provider, scope isolation runs on a pure
// AsyncLocalStorage strategy instead of the OpenTelemetry context strategy. Instrumentation still
// emits spans via core `startSpan`; there is just no OTel provider or propagator behind them.
let asyncLocalStorageLookup: ReturnType<typeof setOpenTelemetryContextAsyncContextStrategy> | undefined;
if (!clientOptions.enableOpenTelemetrySetup) {
// The ALS store already is the `{ scope, isolationScope }` object, so no `contextSymbol` is needed
// to reach it (unlike the OTel context strategy, where it is nested under the OTel context).
const asyncLocalStorage = setAsyncLocalStorageAsyncContextStrategy();
asyncLocalStorageLookup = { asyncLocalStorage };
} else {
asyncLocalStorageLookup = setOpenTelemetryContextAsyncContextStrategy();
}

const scope = getCurrentScope();
scope.update(clientOptions.initialScope);

Expand All @@ -199,7 +184,6 @@ function _init(
getCurrentScope().setClient(client);

client.init();
client.asyncLocalStorageLookup = asyncLocalStorageLookup;

/*! rollup-include-cjs-only */
debug.log(`SDK initialized from CommonJS`);
Expand Down
48 changes: 47 additions & 1 deletion packages/node/test/sdk/client.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { AsyncLocalStorage } from 'node:async_hooks';
import { ProxyTracer } from '@opentelemetry/api';
import type { Event, EventHint, Log } from '@sentry/core';
import { getMainCarrier, Scope, SDK_VERSION } from '@sentry/core';
import { getAsyncContextStrategy, getMainCarrier, Scope, SDK_VERSION } from '@sentry/core';
import type { SentryTracerProvider } from '@sentry/opentelemetry';
import { setOpenTelemetryContextAsyncContextStrategy } from '@sentry/opentelemetry';
import * as SentryOpentelemetry from '@sentry/opentelemetry';
import * as SentryServerUtils from '@sentry/server-utils';
import * as os from 'os';
import { afterEach, beforeEach, describe, expect, it, test, vi } from 'vitest';
import { NodeClient } from '../../src';
Expand All @@ -20,6 +23,49 @@ describe('NodeClient', () => {
cleanupOtel();
});

describe('init', () => {
beforeEach(() => {
// Undo the OTel strategy the outer `beforeEach` installs, so each test observes what `init()`
// does in a fresh process (the ALS installer would otherwise reuse the OTel manager's storage).
getMainCarrier().__SENTRY__ = undefined;
cleanupOtel();
});

it('installs the AsyncLocalStorage context strategy by default', () => {
const alsStrategySpy = vi.spyOn(SentryServerUtils, 'setAsyncLocalStorageAsyncContextStrategy');
const otelStrategySpy = vi.spyOn(SentryOpentelemetry, 'setOpenTelemetryContextAsyncContextStrategy');

const client = new NodeClient(getDefaultNodeClientOptions());

expect(alsStrategySpy).not.toHaveBeenCalled();
expect(otelStrategySpy).not.toHaveBeenCalled();

client.init();

expect(alsStrategySpy).toHaveBeenCalledTimes(1);
expect(otelStrategySpy).not.toHaveBeenCalled();
expect(client.asyncLocalStorageLookup?.asyncLocalStorage).toBeInstanceOf(AsyncLocalStorage);
expect(client.asyncLocalStorageLookup?.contextSymbol).toBeUndefined();
// The lookup points at the same ALS the installed strategy hands to channel-based integrations
expect(getAsyncContextStrategy(getMainCarrier()).getTracingChannelBinding?.()?.asyncLocalStorage).toBe(
client.asyncLocalStorageLookup?.asyncLocalStorage,
);
});

it('installs the OpenTelemetry context strategy with `enableOpenTelemetrySetup`', () => {
const alsStrategySpy = vi.spyOn(SentryServerUtils, 'setAsyncLocalStorageAsyncContextStrategy');
const otelStrategySpy = vi.spyOn(SentryOpentelemetry, 'setOpenTelemetryContextAsyncContextStrategy');

const client = new NodeClient(getDefaultNodeClientOptions({ enableOpenTelemetrySetup: true }));
client.init();

expect(otelStrategySpy).toHaveBeenCalledTimes(1);
expect(alsStrategySpy).not.toHaveBeenCalled();
expect(client.asyncLocalStorageLookup?.asyncLocalStorage).toBeInstanceOf(AsyncLocalStorage);
expect(client.asyncLocalStorageLookup?.contextSymbol).toBeDefined();
});
});

it('sets correct metadata', () => {
const options = getDefaultNodeClientOptions();
const client = new NodeClient(options);
Expand Down
Loading