From c26844d8e2c811c814cca9f635901f65aa117e32 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Thu, 20 Aug 2026 01:46:16 +0200 Subject: [PATCH 1/2] fix(node): Move Async context strategy setup to Client.init() --- packages/node/src/sdk/client.ts | 15 ++++++++ packages/node/src/sdk/index.ts | 16 --------- packages/node/test/sdk/client.test.ts | 49 ++++++++++++++++++++++++++- 3 files changed, 63 insertions(+), 17 deletions(-) diff --git a/packages/node/src/sdk/client.ts b/packages/node/src/sdk/client.ts index 1a101a7bff88..0f35938114bc 100644 --- a/packages/node/src/sdk/client.ts +++ b/packages/node/src/sdk/client.ts @@ -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'; @@ -82,6 +84,19 @@ export class NodeClient extends ServerRuntimeClient { 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) { diff --git a/packages/node/src/sdk/index.ts b/packages/node/src/sdk/index.ts index 9d0da79809cc..2bb381588581 100644 --- a/packages/node/src/sdk/index.ts +++ b/packages/node/src/sdk/index.ts @@ -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'; @@ -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 | 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); @@ -199,7 +184,6 @@ function _init( getCurrentScope().setClient(client); client.init(); - client.asyncLocalStorageLookup = asyncLocalStorageLookup; /*! rollup-include-cjs-only */ debug.log(`SDK initialized from CommonJS`); diff --git a/packages/node/test/sdk/client.test.ts b/packages/node/test/sdk/client.test.ts index 10301e5c87bd..445b97fd6abe 100644 --- a/packages/node/test/sdk/client.test.ts +++ b/packages/node/test/sdk/client.test.ts @@ -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'; @@ -20,6 +23,50 @@ 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); From 4f19dd8055c2c4ee49d48e7f43bcb947891f548c Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Thu, 20 Aug 2026 01:51:57 +0200 Subject: [PATCH 2/2] Fix formatting Co-Authored-By: Claude Fable 5 --- packages/node/test/sdk/client.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/node/test/sdk/client.test.ts b/packages/node/test/sdk/client.test.ts index 445b97fd6abe..1792dd6da0eb 100644 --- a/packages/node/test/sdk/client.test.ts +++ b/packages/node/test/sdk/client.test.ts @@ -64,7 +64,6 @@ describe('NodeClient', () => { expect(client.asyncLocalStorageLookup?.asyncLocalStorage).toBeInstanceOf(AsyncLocalStorage); expect(client.asyncLocalStorageLookup?.contextSymbol).toBeDefined(); }); - }); it('sets correct metadata', () => {