diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1-custom-sampler/package.json b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1-custom-sampler/package.json index d1957655916b..6f379575019b 100644 --- a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1-custom-sampler/package.json +++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1-custom-sampler/package.json @@ -12,7 +12,6 @@ }, "dependencies": { "@opentelemetry/api": "^1.9.0", - "@opentelemetry/context-async-hooks": "^1.30.1", "@opentelemetry/core": "^1.30.1", "@opentelemetry/instrumentation": "^0.57.1", "@opentelemetry/instrumentation-http": "^0.57.1", diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1-sdk-node/package.json b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1-sdk-node/package.json index 69decb891620..b7d9b06647b3 100644 --- a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1-sdk-node/package.json +++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1-sdk-node/package.json @@ -12,7 +12,6 @@ }, "dependencies": { "@opentelemetry/api": "^1.9.0", - "@opentelemetry/context-async-hooks": "^1.30.1", "@opentelemetry/core": "^1.30.1", "@opentelemetry/instrumentation": "^0.57.2", "@opentelemetry/instrumentation-http": "^0.57.2", diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/package.json b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/package.json index abb49f748d96..28d17064a5ff 100644 --- a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/package.json +++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/package.json @@ -14,7 +14,6 @@ "@sentry/node-core": "latest || *", "@sentry/opentelemetry": "latest || *", "@opentelemetry/api": "^1.9.0", - "@opentelemetry/context-async-hooks": "^1.30.1", "@opentelemetry/core": "^1.30.1", "@opentelemetry/instrumentation": "^0.57.1", "@opentelemetry/instrumentation-http": "^0.57.1", diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2-custom-sampler/package.json b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2-custom-sampler/package.json index 974d0711acc8..f79c0894bfc9 100644 --- a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2-custom-sampler/package.json +++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2-custom-sampler/package.json @@ -12,7 +12,6 @@ }, "dependencies": { "@opentelemetry/api": "^1.9.0", - "@opentelemetry/context-async-hooks": "^2.6.0", "@opentelemetry/core": "^2.6.0", "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/instrumentation-http": "^0.214.0", diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2-sdk-node/package.json b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2-sdk-node/package.json index 00e1ab056be6..dd294c205b32 100644 --- a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2-sdk-node/package.json +++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2-sdk-node/package.json @@ -12,7 +12,6 @@ }, "dependencies": { "@opentelemetry/api": "^1.9.0", - "@opentelemetry/context-async-hooks": "^2.6.0", "@opentelemetry/core": "^2.6.0", "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/instrumentation-http": "^0.214.0", diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/package.json b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/package.json index 77b6006ee947..7c1ea4377070 100644 --- a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/package.json +++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/package.json @@ -14,7 +14,6 @@ "@sentry/node-core": "latest || *", "@sentry/opentelemetry": "latest || *", "@opentelemetry/api": "^1.9.0", - "@opentelemetry/context-async-hooks": "^2.6.0", "@opentelemetry/core": "^2.6.0", "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/instrumentation-http": "^0.214.0", diff --git a/dev-packages/node-core-integration-tests/package.json b/dev-packages/node-core-integration-tests/package.json index 038cb87dc03d..e46fe5825af8 100644 --- a/dev-packages/node-core-integration-tests/package.json +++ b/dev-packages/node-core-integration-tests/package.json @@ -27,7 +27,6 @@ "@nestjs/core": "^11", "@nestjs/platform-express": "^11", "@opentelemetry/api": "^1.9.1", - "@opentelemetry/context-async-hooks": "^2.6.1", "@opentelemetry/core": "^2.6.1", "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/instrumentation-http": "0.214.0", diff --git a/packages/node-core/README.md b/packages/node-core/README.md index a6245cbd9b0e..c3ccc6df1b3d 100644 --- a/packages/node-core/README.md +++ b/packages/node-core/README.md @@ -13,7 +13,6 @@ Unlike the `@sentry/node` SDK, this SDK comes with no OpenTelemetry auto-instrumentation out of the box. It requires the following OpenTelemetry dependencies and supports both v1 and v2 of OpenTelemetry: - `@opentelemetry/api` -- `@opentelemetry/context-async-hooks` - `@opentelemetry/core` - `@opentelemetry/instrumentation` - `@opentelemetry/resources` @@ -23,10 +22,10 @@ Unlike the `@sentry/node` SDK, this SDK comes with no OpenTelemetry auto-instrum ## Installation ```bash -npm install @sentry/node-core @sentry/opentelemetry @opentelemetry/api @opentelemetry/core @opentelemetry/context-async-hooks @opentelemetry/instrumentation @opentelemetry/resources @opentelemetry/sdk-trace-base @opentelemetry/semantic-conventions +npm install @sentry/node-core @sentry/opentelemetry @opentelemetry/api @opentelemetry/core @opentelemetry/instrumentation @opentelemetry/resources @opentelemetry/sdk-trace-base @opentelemetry/semantic-conventions # Or yarn -yarn add @sentry/node-core @sentry/opentelemetry @opentelemetry/api @opentelemetry/core @opentelemetry/context-async-hooks @opentelemetry/instrumentation @opentelemetry/resources @opentelemetry/sdk-trace-base @opentelemetry/semantic-conventions +yarn add @sentry/node-core @sentry/opentelemetry @opentelemetry/api @opentelemetry/core @opentelemetry/instrumentation @opentelemetry/resources @opentelemetry/sdk-trace-base @opentelemetry/semantic-conventions ``` ## Usage diff --git a/packages/node-core/package.json b/packages/node-core/package.json index 726897c30319..ae247548614e 100644 --- a/packages/node-core/package.json +++ b/packages/node-core/package.json @@ -78,7 +78,6 @@ }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", - "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.1.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/instrumentation": ">=0.57.1 <1", "@opentelemetry/resources": "^1.30.1 || ^2.1.0", @@ -90,9 +89,6 @@ "@opentelemetry/api": { "optional": true }, - "@opentelemetry/context-async-hooks": { - "optional": true - }, "@opentelemetry/core": { "optional": true }, @@ -119,7 +115,6 @@ }, "devDependencies": { "@opentelemetry/api": "^1.9.1", - "@opentelemetry/context-async-hooks": "^2.6.1", "@opentelemetry/core": "^2.6.1", "@opentelemetry/exporter-trace-otlp-http": "^0.214.0", "@opentelemetry/instrumentation": "^0.214.0", diff --git a/packages/node-core/src/otel/contextManager.ts b/packages/node-core/src/otel/contextManager.ts index 252508eb7c88..8a41e322cfad 100644 --- a/packages/node-core/src/otel/contextManager.ts +++ b/packages/node-core/src/otel/contextManager.ts @@ -1,11 +1,7 @@ -import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'; -import { wrapContextManagerClass } from '@sentry/opentelemetry'; +import { SentryAsyncLocalStorageContextManager } from '@sentry/opentelemetry'; /** - * This is a custom ContextManager for OpenTelemetry, which extends the default AsyncLocalStorageContextManager. + * This is a custom ContextManager for OpenTelemetry & Sentry. * It ensures that we create a new hub per context, so that the OTEL Context & the Sentry Scopes are always in sync. - * - * Note that we currently only support AsyncHooks with this, - * but since this should work for Node 14+ anyhow that should be good enough. */ -export const SentryContextManager = wrapContextManagerClass(AsyncLocalStorageContextManager); +export const SentryContextManager = SentryAsyncLocalStorageContextManager; diff --git a/packages/node/package.json b/packages/node/package.json index a348cb4affa7..7d1d17bfe4a8 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -66,7 +66,6 @@ }, "dependencies": { "@opentelemetry/api": "^1.9.1", - "@opentelemetry/context-async-hooks": "^2.6.1", "@opentelemetry/core": "^2.6.1", "@opentelemetry/instrumentation": "^0.214.0", "@opentelemetry/instrumentation-amqplib": "0.61.0", diff --git a/packages/opentelemetry/README.md b/packages/opentelemetry/README.md index 86a644f5c467..dd20135c268b 100644 --- a/packages/opentelemetry/README.md +++ b/packages/opentelemetry/README.md @@ -43,7 +43,7 @@ This is how you can use this in your app: 2. Call `setupEventContextTrace(client)` 3. Add `SentrySampler` as sampler 4. Add `SentrySpanProcessor` as span processor -5. Add a context manager wrapped via `wrapContextManagerClass` +5. Register the Sentry context manager (`SentryAsyncLocalStorageContextManager`, or `wrapContextManagerClass` for a custom base) 6. Add `SentryPropagator` as propagator 7. Setup OTEL-powered async context strategy for Sentry via `setOpenTelemetryContextAsyncContextStrategy()` @@ -52,14 +52,13 @@ For example, you could set this up as follows: ```js import * as Sentry from '@sentry/node'; import { + SentryAsyncLocalStorageContextManager, SentryPropagator, SentrySampler, SentrySpanProcessor, setupEventContextTrace, - wrapContextManagerClass, setOpenTelemetryContextAsyncContextStrategy, } from '@sentry/opentelemetry'; -import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'; import { context, propagation, trace } from '@opentelemetry/api'; function setupSentry() { @@ -75,12 +74,10 @@ function setupSentry() { }); provider.addSpanProcessor(new SentrySpanProcessor()); - const SentryContextManager = wrapContextManagerClass(AsyncLocalStorageContextManager); - // Initialize the provider trace.setGlobalTracerProvider(provider); propagation.setGlobalPropagator(new SentryPropagator()); - context.setGlobalContextManager(new SentryContextManager()); + context.setGlobalContextManager(new SentryAsyncLocalStorageContextManager()); setOpenTelemetryContextAsyncContextStrategy(); } diff --git a/packages/opentelemetry/package.json b/packages/opentelemetry/package.json index d63ec3ee2097..64b22768bf7a 100644 --- a/packages/opentelemetry/package.json +++ b/packages/opentelemetry/package.json @@ -43,14 +43,12 @@ }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", - "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.1.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", "@opentelemetry/semantic-conventions": "^1.39.0" }, "devDependencies": { "@opentelemetry/api": "^1.9.1", - "@opentelemetry/context-async-hooks": "^2.6.1", "@opentelemetry/core": "^2.6.1", "@opentelemetry/sdk-trace-base": "^2.6.1", "@opentelemetry/semantic-conventions": "^1.40.0" diff --git a/packages/opentelemetry/src/asyncLocalStorageContextManager.ts b/packages/opentelemetry/src/asyncLocalStorageContextManager.ts new file mode 100644 index 000000000000..e1a7db98e527 --- /dev/null +++ b/packages/opentelemetry/src/asyncLocalStorageContextManager.ts @@ -0,0 +1,214 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * NOTICE from the Sentry authors: + * This implementation follows the behavior of OpenTelemetry’s `@opentelemetry/context-async-hooks` + * package, combining logic that upstream splits across: + * - https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-context-async-hooks/src/AbstractAsyncHooksContextManager.ts + * - https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-context-async-hooks/src/AsyncLocalStorageContextManager.ts + * It is a single-class re-implementation for Sentry (not a verbatim copy of those files). + */ + +import type { Context, ContextManager } from '@opentelemetry/api'; +import { ROOT_CONTEXT } from '@opentelemetry/api'; +import { AsyncLocalStorage } from 'node:async_hooks'; +import { EventEmitter } from 'node:events'; +import { SENTRY_SCOPES_CONTEXT_KEY } from './constants'; +import type { AsyncLocalStorageLookup } from './contextManager'; +import { buildContextWithSentryScopes } from './utils/buildContextWithSentryScopes'; +import { setIsSetup } from './utils/setupCheck'; + +type ListenerFn = (...args: unknown[]) => unknown; + +/** + * Per-event map from user listeners to context-bound listeners. + */ +type PatchMap = Record>; + +const ADD_LISTENER_METHODS = ['addListener', 'on', 'once', 'prependListener', 'prependOnceListener'] as const; + +/** + * OpenTelemetry-compatible context manager using Node.js `AsyncLocalStorage`. + * Semantics match `@opentelemetry/context-async-hooks` (function `bind` + `EventEmitter` patching). + */ +export class SentryAsyncLocalStorageContextManager implements ContextManager { + protected readonly _asyncLocalStorage = new AsyncLocalStorage(); + + private readonly _kOtListeners = Symbol('OtListeners'); + private _wrapped = false; + + public constructor() { + setIsSetup('SentryContextManager'); + } + + public active(): Context { + return this._asyncLocalStorage.getStore() ?? ROOT_CONTEXT; + } + + public with ReturnType>( + context: Context, + fn: F, + thisArg?: ThisParameterType, + ...args: A + ): ReturnType { + const ctx2 = buildContextWithSentryScopes(context, this.active()); + const cb = thisArg == null ? fn : fn.bind(thisArg); + return this._asyncLocalStorage.run(ctx2, cb as never, ...args); + } + + public enable(): this { + return this; + } + + public disable(): this { + this._asyncLocalStorage.disable(); + return this; + } + + public bind(context: Context, target: T): T { + if (target instanceof EventEmitter) { + return this._bindEventEmitter(context, target); + } + if (typeof target === 'function') { + return this._bindFunction(context, target as unknown as ListenerFn) as T; + } + return target; + } + + /** + * Gets underlying AsyncLocalStorage and symbol to allow lookup of scope. + * This is Sentry-specific. + */ + public getAsyncLocalStorageLookup(): AsyncLocalStorageLookup { + return { + asyncLocalStorage: this._asyncLocalStorage, + contextSymbol: SENTRY_SCOPES_CONTEXT_KEY, + }; + } + + private _bindFunction(context: Context, target: ListenerFn): ListenerFn { + const managerWith = this.with.bind(this); + const contextWrapper = function (this: never, ...args: unknown[]) { + return managerWith(context, () => target.apply(this, args)); + }; + Object.defineProperty(contextWrapper, 'length', { + enumerable: false, + configurable: true, + writable: false, + value: target.length, + }); + return contextWrapper; + } + + private _bindEventEmitter(context: Context, ee: T): T { + if (this._getPatchMap(ee) !== undefined) { + return ee; + } + this._createPatchMap(ee); + + for (const methodName of ADD_LISTENER_METHODS) { + if (ee[methodName] === undefined) continue; + ee[methodName] = this._patchAddListener( + ee, + ee[methodName] as unknown as (...args: unknown[]) => unknown, + context, + ); + } + if (typeof ee.removeListener === 'function') { + // oxlint-disable-next-line @typescript-eslint/unbound-method -- patched like upstream OTel context manager + ee.removeListener = this._patchRemoveListener(ee, ee.removeListener as (...args: unknown[]) => unknown); + } + if (typeof ee.off === 'function') { + // oxlint-disable-next-line @typescript-eslint/unbound-method + ee.off = this._patchRemoveListener(ee, ee.off as (...args: unknown[]) => unknown); + } + if (typeof ee.removeAllListeners === 'function') { + ee.removeAllListeners = this._patchRemoveAllListeners( + ee, + // oxlint-disable-next-line @typescript-eslint/unbound-method + ee.removeAllListeners as (...args: unknown[]) => unknown, + ); + } + return ee; + } + + private _patchRemoveListener(ee: EventEmitter, original: (...args: unknown[]) => unknown) { + // oxlint-disable-next-line @typescript-eslint/no-this-alias + const contextManager = this; + return function (this: unknown, event: string, listener: ListenerFn) { + const events = contextManager._getPatchMap(ee)?.[event]; + if (events === undefined) { + return original.call(this, event, listener); + } + const patchedListener = events.get(listener); + return original.call(this, event, patchedListener || listener); + }; + } + + private _patchRemoveAllListeners(ee: EventEmitter, original: (...args: unknown[]) => unknown) { + // oxlint-disable-next-line @typescript-eslint/no-this-alias + const contextManager = this; + return function (this: unknown, event?: string) { + const map = contextManager._getPatchMap(ee); + if (map !== undefined) { + if (arguments.length === 0) { + contextManager._createPatchMap(ee); + } else if (event !== undefined && map[event] !== undefined) { + // oxlint-disable-next-line @typescript-eslint/no-dynamic-delete -- event-keyed listener map + delete map[event]; + } + } + return original.apply(this, arguments); + }; + } + + private _patchAddListener(ee: EventEmitter, original: (...args: unknown[]) => unknown, context: Context) { + // oxlint-disable-next-line @typescript-eslint/no-this-alias + const contextManager = this; + return function (this: unknown, event: string, listener: ListenerFn) { + if (contextManager._wrapped) { + return original.call(this, event, listener); + } + let map = contextManager._getPatchMap(ee); + if (map === undefined) { + map = contextManager._createPatchMap(ee); + } + let listeners = map[event]; + if (listeners === undefined) { + listeners = new WeakMap(); + map[event] = listeners; + } + const patchedListener = contextManager.bind(context, listener); + listeners.set(listener, patchedListener); + + contextManager._wrapped = true; + try { + return original.call(this, event, patchedListener); + } finally { + contextManager._wrapped = false; + } + }; + } + + private _createPatchMap(ee: EventEmitter): PatchMap { + const map = Object.create(null) as PatchMap; + (ee as unknown as Record)[this._kOtListeners] = map; + return map; + } + + private _getPatchMap(ee: EventEmitter): PatchMap | undefined { + return (ee as unknown as Record)[this._kOtListeners]; + } +} diff --git a/packages/opentelemetry/src/contextManager.ts b/packages/opentelemetry/src/contextManager.ts index f5a137397978..f1c3228c5dfa 100644 --- a/packages/opentelemetry/src/contextManager.ts +++ b/packages/opentelemetry/src/contextManager.ts @@ -1,16 +1,7 @@ import type { AsyncLocalStorage } from 'node:async_hooks'; import type { Context, ContextManager } from '@opentelemetry/api'; -import { trace } from '@opentelemetry/api'; -import type { Scope } from '@sentry/core'; -import { getCurrentScope, getIsolationScope } from '@sentry/core'; -import { - SENTRY_FORK_ISOLATION_SCOPE_CONTEXT_KEY, - SENTRY_FORK_SET_ISOLATION_SCOPE_CONTEXT_KEY, - SENTRY_FORK_SET_SCOPE_CONTEXT_KEY, - SENTRY_SCOPES_CONTEXT_KEY, - SENTRY_TRACE_STATE_CHILD_IGNORED, -} from './constants'; -import { getScopesFromContext, setContextOnScope, setScopesOnContext } from './utils/contextData'; +import { SENTRY_SCOPES_CONTEXT_KEY } from './constants'; +import { buildContextWithSentryScopes } from './utils/buildContextWithSentryScopes'; import { setIsSetup } from './utils/setupCheck'; export type AsyncLocalStorageLookup = { @@ -31,6 +22,8 @@ type ExtendedContextManagerInstance( ContextManagerClass: new (...args: unknown[]) => ContextManagerInstance, @@ -59,45 +52,7 @@ export function wrapContextManagerClass, ...args: A ): ReturnType { - // Remove ignored spans from context and restore the parent span so children - // naturally parent to the grandparent instead of starting a new trace. - // At this point, this.active() still holds the outer context (before super.with() - // updates AsyncLocalStorage), which has the grandparent span we want to restore. - const span = trace.getSpan(context); - let effectiveContext: Context; - if (span?.spanContext().traceState?.get(SENTRY_TRACE_STATE_CHILD_IGNORED) === '1') { - const contextWithoutSpan = trace.deleteSpan(context); - const parentSpan = trace.getSpan(this.active()); - effectiveContext = parentSpan ? trace.setSpan(contextWithoutSpan, parentSpan) : contextWithoutSpan; - } else { - effectiveContext = context; - } - - const currentScopes = getScopesFromContext(effectiveContext); - const currentScope = currentScopes?.scope || getCurrentScope(); - const currentIsolationScope = currentScopes?.isolationScope || getIsolationScope(); - - const shouldForkIsolationScope = effectiveContext.getValue(SENTRY_FORK_ISOLATION_SCOPE_CONTEXT_KEY) === true; - const scope = effectiveContext.getValue(SENTRY_FORK_SET_SCOPE_CONTEXT_KEY) as Scope | undefined; - const isolationScope = effectiveContext.getValue(SENTRY_FORK_SET_ISOLATION_SCOPE_CONTEXT_KEY) as - | Scope - | undefined; - - const newCurrentScope = scope || currentScope.clone(); - const newIsolationScope = - isolationScope || (shouldForkIsolationScope ? currentIsolationScope.clone() : currentIsolationScope); - const scopes = { scope: newCurrentScope, isolationScope: newIsolationScope }; - - const ctx1 = setScopesOnContext(effectiveContext, scopes); - - // Remove the unneeded values again - const ctx2 = ctx1 - .deleteValue(SENTRY_FORK_ISOLATION_SCOPE_CONTEXT_KEY) - .deleteValue(SENTRY_FORK_SET_SCOPE_CONTEXT_KEY) - .deleteValue(SENTRY_FORK_SET_ISOLATION_SCOPE_CONTEXT_KEY); - - setContextOnScope(newCurrentScope, ctx2); - + const ctx2 = buildContextWithSentryScopes(context, this.active()); return super.with(ctx2, fn, thisArg, ...args); } diff --git a/packages/opentelemetry/src/index.ts b/packages/opentelemetry/src/index.ts index f5260dc852c5..c5fe1d3376d7 100644 --- a/packages/opentelemetry/src/index.ts +++ b/packages/opentelemetry/src/index.ts @@ -41,6 +41,7 @@ export { setupEventContextTrace } from './setupEventContextTrace'; export { setOpenTelemetryContextAsyncContextStrategy } from './asyncContextStrategy'; export { wrapContextManagerClass } from './contextManager'; +export { SentryAsyncLocalStorageContextManager } from './asyncLocalStorageContextManager'; export type { AsyncLocalStorageLookup } from './contextManager'; export { SentryPropagator, shouldPropagateTraceForUrl } from './propagator'; export { SentrySpanProcessor } from './spanProcessor'; diff --git a/packages/opentelemetry/src/utils/buildContextWithSentryScopes.ts b/packages/opentelemetry/src/utils/buildContextWithSentryScopes.ts new file mode 100644 index 000000000000..ac8c2a4dc19e --- /dev/null +++ b/packages/opentelemetry/src/utils/buildContextWithSentryScopes.ts @@ -0,0 +1,56 @@ +import type { Context } from '@opentelemetry/api'; +import { trace } from '@opentelemetry/api'; +import type { Scope } from '@sentry/core'; +import { getCurrentScope, getIsolationScope } from '@sentry/core'; +import { + SENTRY_FORK_ISOLATION_SCOPE_CONTEXT_KEY, + SENTRY_FORK_SET_ISOLATION_SCOPE_CONTEXT_KEY, + SENTRY_FORK_SET_SCOPE_CONTEXT_KEY, + SENTRY_TRACE_STATE_CHILD_IGNORED, +} from '../constants'; +import { getScopesFromContext, setContextOnScope, setScopesOnContext } from './contextData'; + +/** + * Merge Sentry scopes into an OpenTelemetry {@link Context} and apply trace-context adjustments + * used by Sentry OpenTelemetry context manager(s). + * + * @param context - Context passed into `ContextManager.with`. + * @param activeContext - Context that was active before entering `with` (e.g. `this.active()`), used + * to restore the parent span when the incoming span is marked ignored for children. + * @returns A new context ready for `super.with` / `AsyncLocalStorage.run`. + */ +export function buildContextWithSentryScopes(context: Context, activeContext: Context): Context { + const span = trace.getSpan(context); + let effectiveContext: Context; + if (span?.spanContext().traceState?.get(SENTRY_TRACE_STATE_CHILD_IGNORED) === '1') { + const contextWithoutSpan = trace.deleteSpan(context); + const parentSpan = trace.getSpan(activeContext); + effectiveContext = parentSpan ? trace.setSpan(contextWithoutSpan, parentSpan) : contextWithoutSpan; + } else { + effectiveContext = context; + } + + const currentScopes = getScopesFromContext(effectiveContext); + const currentScope = currentScopes?.scope || getCurrentScope(); + const currentIsolationScope = currentScopes?.isolationScope || getIsolationScope(); + + const shouldForkIsolationScope = effectiveContext.getValue(SENTRY_FORK_ISOLATION_SCOPE_CONTEXT_KEY) === true; + const scope = effectiveContext.getValue(SENTRY_FORK_SET_SCOPE_CONTEXT_KEY) as Scope | undefined; + const isolationScope = effectiveContext.getValue(SENTRY_FORK_SET_ISOLATION_SCOPE_CONTEXT_KEY) as Scope | undefined; + + const newCurrentScope = scope || currentScope.clone(); + const newIsolationScope = + isolationScope || (shouldForkIsolationScope ? currentIsolationScope.clone() : currentIsolationScope); + const scopes = { scope: newCurrentScope, isolationScope: newIsolationScope }; + + const ctx1 = setScopesOnContext(effectiveContext, scopes); + + const ctx2 = ctx1 + .deleteValue(SENTRY_FORK_ISOLATION_SCOPE_CONTEXT_KEY) + .deleteValue(SENTRY_FORK_SET_SCOPE_CONTEXT_KEY) + .deleteValue(SENTRY_FORK_SET_ISOLATION_SCOPE_CONTEXT_KEY); + + setContextOnScope(newCurrentScope, ctx2); + + return ctx2; +} diff --git a/packages/opentelemetry/test/helpers/initOtel.ts b/packages/opentelemetry/test/helpers/initOtel.ts index bf281f716657..f3b176f13b10 100644 --- a/packages/opentelemetry/test/helpers/initOtel.ts +++ b/packages/opentelemetry/test/helpers/initOtel.ts @@ -1,5 +1,4 @@ import { context, diag, DiagLogLevel, propagation, trace } from '@opentelemetry/api'; -import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'; import { defaultResource, resourceFromAttributes } from '@opentelemetry/resources'; import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; import { @@ -8,7 +7,7 @@ import { SEMRESATTRS_SERVICE_NAMESPACE, } from '@opentelemetry/semantic-conventions'; import { debug, getClient, SDK_VERSION } from '@sentry/core'; -import { wrapContextManagerClass } from '../../src/contextManager'; +import { SentryAsyncLocalStorageContextManager } from '../../src/asyncLocalStorageContextManager'; import { DEBUG_BUILD } from '../../src/debug-build'; import { SentryPropagator } from '../../src/propagator'; import { SentrySampler } from '../../src/sampler'; @@ -72,12 +71,9 @@ export function setupOtel(client: TestClientInterface): [BasicTracerProvider, Se spanProcessors: [spanProcessor], }); - // We use a custom context manager to keep context in sync with sentry scope - const SentryContextManager = wrapContextManagerClass(AsyncLocalStorageContextManager); - trace.setGlobalTracerProvider(provider); propagation.setGlobalPropagator(new SentryPropagator()); - context.setGlobalContextManager(new SentryContextManager()); + context.setGlobalContextManager(new SentryAsyncLocalStorageContextManager()); return [provider, spanProcessor]; } diff --git a/yarn.lock b/yarn.lock index e70d6caa8b6d..5cb468aa1a8c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6227,11 +6227,6 @@ resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.9.1.tgz#c1b0346de336ba55af2d5a7970882037baedec05" integrity sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q== -"@opentelemetry/context-async-hooks@^2.6.1": - version "2.6.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/context-async-hooks/-/context-async-hooks-2.6.1.tgz#06e60d5b3fba992a832af7f034758574e951bba3" - integrity sha512-XHzhwRNkBpeP8Fs/qjGrAf9r9PRv67wkJQ/7ZPaBQQ68DYlTBBx5MF9LvPx7mhuXcDessKK2b+DcxqwpgkcivQ== - "@opentelemetry/core@2.6.1", "@opentelemetry/core@^2.0.0", "@opentelemetry/core@^2.6.1": version "2.6.1" resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-2.6.1.tgz#a59d22a9ae3be80bb41b280bbbe1fe9fbdb6c2a5" @@ -28562,7 +28557,6 @@ stylus@0.59.0, stylus@^0.59.0: sucrase@^3.27.0, sucrase@^3.35.0, sucrase@getsentry/sucrase#es2020-polyfills: version "3.36.0" - uid fd682f6129e507c00bb4e6319cc5d6b767e36061 resolved "https://codeload.github.com/getsentry/sucrase/tar.gz/fd682f6129e507c00bb4e6319cc5d6b767e36061" dependencies: "@jridgewell/gen-mapping" "^0.3.2"