From caca38b468db04b14d7e6be19ddeae04d2201e97 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 24 Jul 2026 09:08:55 +0200 Subject: [PATCH 1/3] ref(core): Remove otel-based `suppressTracing` implementation --- packages/cloudflare/src/async.ts | 9 --------- .../core/src/asyncContext/stackStrategy.ts | 2 ++ packages/core/src/tracing/trace.ts | 19 ++++++++++++++----- packages/deno/src/async.ts | 9 --------- .../opentelemetry/src/asyncContextStrategy.ts | 3 --- packages/opentelemetry/src/exports.ts | 2 -- .../src/utils/suppressTracing.ts | 18 ------------------ 7 files changed, 16 insertions(+), 46 deletions(-) delete mode 100644 packages/opentelemetry/src/utils/suppressTracing.ts diff --git a/packages/cloudflare/src/async.ts b/packages/cloudflare/src/async.ts index d7e6f909a4c9..bde7bbfc8f47 100644 --- a/packages/cloudflare/src/async.ts +++ b/packages/cloudflare/src/async.ts @@ -69,16 +69,7 @@ export function setAsyncLocalStorageAsyncContextStrategy(): void { }); } - // In contrast to the browser, we can rely on async context isolation here - function suppressTracing(callback: () => T): T { - return withScope(scope => { - scope.setSDKProcessingMetadata({ __SENTRY_SUPPRESS_TRACING__: true }); - return callback(); - }); - } - setAsyncContextStrategy({ - suppressTracing, withScope, withSetScope, withIsolationScope, diff --git a/packages/core/src/asyncContext/stackStrategy.ts b/packages/core/src/asyncContext/stackStrategy.ts index 36c1d2127530..12be8c994550 100644 --- a/packages/core/src/asyncContext/stackStrategy.ts +++ b/packages/core/src/asyncContext/stackStrategy.ts @@ -1,6 +1,7 @@ import type { Client } from '../client'; import { getDefaultCurrentScope, getDefaultIsolationScope } from '../defaultScopes'; import { Scope } from '../scope'; +import { suppressTracingInStack } from '../tracing/trace'; import { chainAndCopyPromiseLike } from '../utils/chain-and-copy-promiselike'; import { isThenable } from '../utils/is'; import { getMainCarrier, getSentryCarrier } from './../carrier'; @@ -148,6 +149,7 @@ function withIsolationScope(callback: (isolationScope: Scope) => T): T { */ export function getStackAsyncContextStrategy(): AsyncContextStrategy { return { + suppressTracing: suppressTracingInStack, withIsolationScope, withScope, withSetScope, diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index 7505b7889c1d..c6623562a82d 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -293,11 +293,20 @@ export function suppressTracing(callback: () => T): T { } return withScope(scope => { - // Note: We do not wait for the callback to finish before we reset the metadata - // the reason for this is that otherwise, in the browser this can lead to very weird behavior - // as there is only a single top scope, if the callback takes longer to finish, - // other, unrelated spans may also be suppressed, which we do not want - // so instead, we only suppress tracing synchronoysly in the browser + scope.setSDKProcessingMetadata({ [SUPPRESS_TRACING_KEY]: true }); + return callback(); + }); +} + +/** + * In stack-based ACS, we do not wait for the callback to finish before we reset the metadata + * the reason for this is that otherwise, in the stack this can lead to very weird behavior + * as there is only a single top scope, if the callback takes longer to finish, + * other, unrelated spans may also be suppressed, which we do not want + * so instead, we only suppress tracing synchronoysly in the stack. + */ +export function suppressTracingInStack(callback: () => T): T { + return withScope(scope => { scope.setSDKProcessingMetadata({ [SUPPRESS_TRACING_KEY]: true }); const res = callback(); scope.setSDKProcessingMetadata({ [SUPPRESS_TRACING_KEY]: undefined }); diff --git a/packages/deno/src/async.ts b/packages/deno/src/async.ts index 302bdce9d702..425905216162 100644 --- a/packages/deno/src/async.ts +++ b/packages/deno/src/async.ts @@ -77,16 +77,7 @@ export function setAsyncLocalStorageAsyncContextStrategy(): void { }); } - // In contrast to the browser, we can rely on async context isolation here - function suppressTracing(callback: () => T): T { - return withScope(scope => { - scope.setSDKProcessingMetadata({ __SENTRY_SUPPRESS_TRACING__: true }); - return callback(); - }); - } - setAsyncContextStrategy({ - suppressTracing, withScope, withSetScope, withIsolationScope, diff --git a/packages/opentelemetry/src/asyncContextStrategy.ts b/packages/opentelemetry/src/asyncContextStrategy.ts index 71c9e41f5f77..38b19048f8c1 100644 --- a/packages/opentelemetry/src/asyncContextStrategy.ts +++ b/packages/opentelemetry/src/asyncContextStrategy.ts @@ -11,7 +11,6 @@ import type { CurrentScopes } from './types'; import { getContextFromScope, getScopesFromContext } from './utils/contextData'; import { getActiveSpan } from './utils/getActiveSpan'; import { getTraceData } from './utils/getTraceData'; -import { suppressTracing, isTracingSuppressed } from './utils/suppressTracing'; /** * Sets the async context strategy to use follow the OTEL context under the hood. @@ -103,8 +102,6 @@ export function setOpenTelemetryContextAsyncContextStrategy(options?: { startSpanManual, startInactiveSpan, getActiveSpan, - suppressTracing, - isTracingSuppressed, getTraceData, continueTrace, startNewTrace, diff --git a/packages/opentelemetry/src/exports.ts b/packages/opentelemetry/src/exports.ts index 13d98a4a90ff..e545dbd24bb4 100644 --- a/packages/opentelemetry/src/exports.ts +++ b/packages/opentelemetry/src/exports.ts @@ -4,8 +4,6 @@ export { enhanceDscWithOpenTelemetryRootSpanName } from './utils/enhanceDscWithO export { getTraceContextForScope } from './trace'; -export { suppressTracing } from './utils/suppressTracing'; - export { setupEventContextTrace } from './setupEventContextTrace'; // eslint-disable-next-line typescript/no-deprecated diff --git a/packages/opentelemetry/src/utils/suppressTracing.ts b/packages/opentelemetry/src/utils/suppressTracing.ts deleted file mode 100644 index f37fadee3d4c..000000000000 --- a/packages/opentelemetry/src/utils/suppressTracing.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { context } from '@opentelemetry/api'; -import { - suppressTracing as suppressTracingImpl, - isTracingSuppressed as isTracingSuppressedImpl, -} from '@opentelemetry/core'; -import type { Scope } from '@sentry/core'; -import { getContextFromScope } from './contextData'; - -/** Suppress tracing in the given callback, ensuring no spans are generated inside of it. */ -export function suppressTracing(callback: () => T): T { - const ctx = suppressTracingImpl(context.active()); - return context.with(ctx, callback); -} - -export function isTracingSuppressed(scope?: Scope): boolean { - const ctx = scope ? getContextFromScope(scope) : context.active(); - return ctx ? isTracingSuppressedImpl(ctx) : false; -} From f1b4b4bf4ad8294d66ca31a4ee0c21a550eb6087 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 24 Jul 2026 09:21:42 +0200 Subject: [PATCH 2/3] fixes --- .../core/src/asyncContext/stackStrategy.ts | 18 +++++++++++++++++- .../integrations/http/client-subscriptions.ts | 6 +++--- packages/core/src/tracing/constants.ts | 1 + packages/core/src/tracing/index.ts | 1 - packages/core/src/tracing/trace.ts | 19 +------------------ .../http/client-subscriptions.test.ts | 2 +- packages/core/test/lib/tracing/trace.test.ts | 2 +- 7 files changed, 24 insertions(+), 25 deletions(-) create mode 100644 packages/core/src/tracing/constants.ts diff --git a/packages/core/src/asyncContext/stackStrategy.ts b/packages/core/src/asyncContext/stackStrategy.ts index 12be8c994550..e4b973d64e57 100644 --- a/packages/core/src/asyncContext/stackStrategy.ts +++ b/packages/core/src/asyncContext/stackStrategy.ts @@ -1,7 +1,7 @@ import type { Client } from '../client'; import { getDefaultCurrentScope, getDefaultIsolationScope } from '../defaultScopes'; import { Scope } from '../scope'; -import { suppressTracingInStack } from '../tracing/trace'; +import { SUPPRESS_TRACING_KEY } from '../tracing/constants'; import { chainAndCopyPromiseLike } from '../utils/chain-and-copy-promiselike'; import { isThenable } from '../utils/is'; import { getMainCarrier, getSentryCarrier } from './../carrier'; @@ -160,3 +160,19 @@ export function getStackAsyncContextStrategy(): AsyncContextStrategy { getIsolationScope: () => getAsyncContextStack().getIsolationScope(), }; } + +/** + * In stack-based ACS, we do not wait for the callback to finish before we reset the metadata + * the reason for this is that otherwise, in the stack this can lead to very weird behavior + * as there is only a single top scope, if the callback takes longer to finish, + * other, unrelated spans may also be suppressed, which we do not want + * so instead, we only suppress tracing synchronoysly in the stack. + */ +function suppressTracingInStack(callback: () => T): T { + return withScope(scope => { + scope.setSDKProcessingMetadata({ [SUPPRESS_TRACING_KEY]: true }); + const res = callback(); + scope.setSDKProcessingMetadata({ [SUPPRESS_TRACING_KEY]: undefined }); + return res; + }); +} diff --git a/packages/core/src/integrations/http/client-subscriptions.ts b/packages/core/src/integrations/http/client-subscriptions.ts index 4a80987f6918..19386f7d95f9 100644 --- a/packages/core/src/integrations/http/client-subscriptions.ts +++ b/packages/core/src/integrations/http/client-subscriptions.ts @@ -18,10 +18,10 @@ import { addOutgoingRequestBreadcrumb } from './add-outgoing-request-breadcrumb' import { bindScopeToEmitter, getSpanStatusFromHttpCode, + isTracingSuppressed, SPAN_STATUS_ERROR, SPAN_STATUS_UNSET, startInactiveSpan, - SUPPRESS_TRACING_KEY, withActiveSpan, } from '../../tracing'; import { debug } from '../../utils/debug-logger'; @@ -33,7 +33,7 @@ import type { HttpInstrumentationOptions, HttpClientRequest, HttpIncomingMessage import { DEBUG_BUILD } from '../../debug-build'; import { LOG_PREFIX, HTTP_ON_CLIENT_REQUEST } from './constants'; import type { ClientSubscriptionName } from './constants'; -import { getClient, getCurrentScope } from '../../currentScopes'; +import { getClient } from '../../currentScopes'; import { hasSpansEnabled } from '../../utils/hasSpansEnabled'; import { doubleWrapWarning } from './double-wrap-warning'; @@ -48,7 +48,7 @@ export function getHttpClientSubscriptions(options: HttpInstrumentationOptions): const onHttpClientRequestCreated: ChannelListener = (data: unknown): void => { // Skip all instrumentation if tracing is suppressed // (e.g., Sentry's own transport uses this to avoid self-instrumentation) - if (getCurrentScope().getScopeData().sdkProcessingMetadata[SUPPRESS_TRACING_KEY] === true) { + if (isTracingSuppressed()) { return; } diff --git a/packages/core/src/tracing/constants.ts b/packages/core/src/tracing/constants.ts new file mode 100644 index 000000000000..a3b7c4c1eae0 --- /dev/null +++ b/packages/core/src/tracing/constants.ts @@ -0,0 +1 @@ +export const SUPPRESS_TRACING_KEY = '__SENTRY_SUPPRESS_TRACING__'; diff --git a/packages/core/src/tracing/index.ts b/packages/core/src/tracing/index.ts index b71f13471c19..198b64ce16c4 100644 --- a/packages/core/src/tracing/index.ts +++ b/packages/core/src/tracing/index.ts @@ -32,7 +32,6 @@ export { isTracingSuppressed, startNewTrace, spanIsIgnored, - SUPPRESS_TRACING_KEY, } from './trace'; export { bindScopeToEmitter } from './bindScopeToEmitter'; export { diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index c6623562a82d..ff9e9b7f8d22 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -34,8 +34,7 @@ import { SentrySpan } from './sentrySpan'; import { SPAN_STATUS_ERROR } from './spanstatus'; import { setCapturedScopesOnSpan } from './utils'; import type { Client } from '../client'; - -export const SUPPRESS_TRACING_KEY = '__SENTRY_SUPPRESS_TRACING__'; +import { SUPPRESS_TRACING_KEY } from './constants'; /** * Wraps a function with a transaction/span and finishes the span after the function is done. @@ -298,22 +297,6 @@ export function suppressTracing(callback: () => T): T { }); } -/** - * In stack-based ACS, we do not wait for the callback to finish before we reset the metadata - * the reason for this is that otherwise, in the stack this can lead to very weird behavior - * as there is only a single top scope, if the callback takes longer to finish, - * other, unrelated spans may also be suppressed, which we do not want - * so instead, we only suppress tracing synchronoysly in the stack. - */ -export function suppressTracingInStack(callback: () => T): T { - return withScope(scope => { - scope.setSDKProcessingMetadata({ [SUPPRESS_TRACING_KEY]: true }); - const res = callback(); - scope.setSDKProcessingMetadata({ [SUPPRESS_TRACING_KEY]: undefined }); - return res; - }); -} - /** Check if tracing is suppressed. */ export function isTracingSuppressed(scope = getCurrentScope()): boolean { const acs = getAcs(); diff --git a/packages/core/test/lib/integrations/http/client-subscriptions.test.ts b/packages/core/test/lib/integrations/http/client-subscriptions.test.ts index 46b486368cdc..3ee3acf5b3f7 100644 --- a/packages/core/test/lib/integrations/http/client-subscriptions.test.ts +++ b/packages/core/test/lib/integrations/http/client-subscriptions.test.ts @@ -3,7 +3,7 @@ import * as breadcrumbModule from '../../../../src/integrations/http/add-outgoin import { HTTP_ON_CLIENT_REQUEST } from '../../../../src/integrations/http/constants'; import { getHttpClientSubscriptions } from '../../../../src/integrations/http/client-subscriptions'; import type { HttpClientRequest, HttpIncomingMessage } from '../../../../src/integrations/http/types'; -import { SUPPRESS_TRACING_KEY } from '../../../../src/tracing'; +import { SUPPRESS_TRACING_KEY } from '../../../../src/tracing/constants'; import { getCurrentScope, withScope } from '../../../../src/currentScopes'; function makeMockRequest(): HttpClientRequest & { diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index 891155652ed8..3f79ed862845 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -25,7 +25,6 @@ import { startInactiveSpan, startSpan, startSpanManual, - SUPPRESS_TRACING_KEY, suppressTracing, withActiveSpan, } from '../../../src/tracing'; @@ -37,6 +36,7 @@ import type { StartSpanOptions } from '../../../src/types/startSpanOptions'; import { _setSpanForScope } from '../../../src/utils/spanOnScope'; import { getActiveSpan, getRootSpan, getSpanDescendants, spanIsSampled } from '../../../src/utils/spanUtils'; import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; +import { SUPPRESS_TRACING_KEY } from '../../../src/tracing/constants'; const enum Type { Sync = 'sync', From e8aa6c4e3b853b0782b55810d50def29f29ab3a2 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 24 Jul 2026 11:07:56 +0200 Subject: [PATCH 3/3] fixes --- packages/node/src/integrations/fs/index.ts | 8 +- packages/opentelemetry/src/propagator.ts | 3 +- packages/opentelemetry/src/sampler.ts | 6 +- packages/opentelemetry/src/trace.ts | 92 +++++++++---------- packages/opentelemetry/src/tracer.ts | 2 +- .../src/utils/suppressTracing.ts | 13 +++ .../opentelemetry/test/propagator.test.ts | 7 +- packages/opentelemetry/test/trace.test.ts | 15 +-- .../opentelemetry/test/tracerProvider.test.ts | 8 +- 9 files changed, 84 insertions(+), 70 deletions(-) create mode 100644 packages/opentelemetry/src/utils/suppressTracing.ts diff --git a/packages/node/src/integrations/fs/index.ts b/packages/node/src/integrations/fs/index.ts index 6dcd4352a1c9..d1b7fc17575c 100644 --- a/packages/node/src/integrations/fs/index.ts +++ b/packages/node/src/integrations/fs/index.ts @@ -17,7 +17,13 @@ export const fsIntegration = defineIntegration((options: FsInstrumentationConfig return { name: INTEGRATION_NAME, setupOnce() { - enableFsInstrumentation(options); + // Defer patching `fs` by a tick. Integrations are set up during `Sentry.init` before the + // OpenTelemetry context manager is registered (which happens later in `initOpenTelemetry`). If we + // patched synchronously, `fs` operations that run during the remainder of init would go through + // our instrumentation while OpenTelemetry's `NoopContextManager` is still active - and there + // `suppressTracing` (which we call for every `fs` operation) cannot fork the scope, so it leaks + // the suppression flag onto the long-lived current scope and silently disables tracing. + setImmediate(() => enableFsInstrumentation(options)); }, }; }); diff --git a/packages/opentelemetry/src/propagator.ts b/packages/opentelemetry/src/propagator.ts index e8123813b108..5a3d6fce3152 100644 --- a/packages/opentelemetry/src/propagator.ts +++ b/packages/opentelemetry/src/propagator.ts @@ -1,6 +1,6 @@ import type { Baggage, Context, Span, SpanContext, TextMapGetter, TextMapSetter } from '@opentelemetry/api'; import { context, INVALID_TRACEID, propagation, trace, TraceFlags } from '@opentelemetry/api'; -import { isTracingSuppressed, W3CBaggagePropagator } from '@opentelemetry/core'; +import { W3CBaggagePropagator } from '@opentelemetry/core'; import { HTTP_URL, URL_FULL } from '@sentry/conventions/attributes'; import type { Client, continueTrace, DynamicSamplingContext, Scope } from '@sentry/core'; import { @@ -27,6 +27,7 @@ import { getScopesFromContext, setScopesOnContext } from './utils/contextData'; import { getSampledForPropagation, getSamplingDecision } from './utils/getSamplingDecision'; import { makeTraceState } from './utils/makeTraceState'; import { setIsSetup } from './utils/setupCheck'; +import { isTracingSuppressed } from './utils/suppressTracing'; /** * Injects and extracts `sentry-trace` and `baggage` headers from carriers. diff --git a/packages/opentelemetry/src/sampler.ts b/packages/opentelemetry/src/sampler.ts index ad532a8e4ff9..f4f7bbc7aff0 100644 --- a/packages/opentelemetry/src/sampler.ts +++ b/packages/opentelemetry/src/sampler.ts @@ -32,6 +32,7 @@ import { getScopesFromContext } from './utils/contextData'; import { getSamplingDecision } from './utils/getSamplingDecision'; import { inferSpanData } from './utils/parseSpanDescription'; import { setIsSetup } from './utils/setupCheck'; +import { isTracingSuppressed } from './utils/suppressTracing'; /** * A custom OTEL sampler that uses Sentry sampling rates to make its decision @@ -61,7 +62,10 @@ export class SentrySampler implements Sampler { const parentSpan = getValidSpan(context); const parentContext = parentSpan?.spanContext(); - if (!hasSpansEnabled(options)) { + // When tracing is suppressed, we never want to record a span. The full OpenTelemetry `Tracer` + // relies on the sampler for this (unlike our `SentryTracer`, which short-circuits itself), so we + // enforce the scope-based suppression here as well. + if (isTracingSuppressed(context) || !hasSpansEnabled(options)) { return wrapSamplingDecision({ decision: undefined, context, spanAttributes }); } diff --git a/packages/opentelemetry/src/trace.ts b/packages/opentelemetry/src/trace.ts index c1b49bb35094..f003432a7960 100644 --- a/packages/opentelemetry/src/trace.ts +++ b/packages/opentelemetry/src/trace.ts @@ -1,6 +1,5 @@ import type { Context, Span, SpanContext, SpanOptions, TimeInput, Tracer } from '@opentelemetry/api'; import { context, SpanStatusCode, trace, TraceFlags } from '@opentelemetry/api'; -import { isTracingSuppressed, suppressTracing } from '@opentelemetry/core'; import type { Client, continueTrace as baseContinueTrace, @@ -25,6 +24,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_OP, spanToJSON, spanToTraceContext, + suppressTracing, } from '@sentry/core'; import { continueTraceAsRemoteSpan } from './propagator'; import type { OpenTelemetrySpanContext } from './types'; @@ -49,59 +49,44 @@ function _startSpan(options: OpenTelemetrySpanContext, callback: (span: Span) return wrapper(() => { const activeCtx = getContext(options.scope, options.forceTransaction); const missingRequiredParent = options.onlyIfParent && !trace.getSpan(activeCtx); - const ctx = missingRequiredParent ? suppressTracing(activeCtx) : activeCtx; if (missingRequiredParent) { getClient()?.recordDroppedEvent('no_parent_span', 'span'); } const spanOptions = getSpanOptions(options); - - // If spans are not enabled, ensure we suppress tracing for the span creation - // but preserve the original context for the callback execution - // This ensures that we don't create spans when tracing is disabled which - // would otherwise be a problem for users that don't enable tracing but use - // custom OpenTelemetry setups. - if (!hasSpansEnabled()) { - const suppressedCtx = isTracingSuppressed(ctx) ? ctx : suppressTracing(ctx); - - return context.with(suppressedCtx, () => { - return tracer.startActiveSpan(name, spanOptions, suppressedCtx, span => { - patchSpanEnd(span); - // Restore the original unsuppressed context for the callback execution - // so that custom OpenTelemetry spans maintain the correct context. - // We use activeCtx (not ctx) because ctx may be suppressed when onlyIfParent is true - // and no parent span exists. Using activeCtx ensures custom OTel spans are never - // inadvertently suppressed. - return context.with(activeCtx, () => { - return handleCallbackErrors( - () => callback(span), - () => { - // Only set the span status to ERROR when there wasn't any error status set before, in order to avoid stomping useful span statuses - if (spanToJSON(span).status === 'ok') { - span.setStatus({ code: SpanStatusCode.ERROR }); - } - }, - autoEnd ? () => span.end() : undefined, - ); - }); - }); + const spansEnabled = hasSpansEnabled(); + + const startActiveSpan = (ctx: Context): T => { + return tracer.startActiveSpan(name, spanOptions, ctx, span => { + patchSpanEnd(span); + const runCallback = (): T => + handleCallbackErrors( + () => callback(span), + () => { + // Only set the span status to ERROR when there wasn't any error status set before, in order to avoid stomping useful span statuses + if (spanToJSON(span).status === 'ok') { + span.setStatus({ code: SpanStatusCode.ERROR }); + } + }, + autoEnd ? () => span.end() : undefined, + ); + + // If spans are disabled, we suppress the span creation above but preserve the original, + // unsuppressed context for the callback execution. This ensures that we don't create spans + // when tracing is disabled, which would otherwise be a problem for users that don't enable + // tracing but use custom OpenTelemetry setups. + return spansEnabled ? runCallback() : context.with(activeCtx, runCallback); }); + }; + + // We suppress tracing (making the span non-recording) when a required parent is missing, or when + // spans are disabled entirely. We rely on `@sentry/core`'s scope-based suppression for this. + if (missingRequiredParent || !spansEnabled) { + return context.with(activeCtx, () => suppressTracing(() => startActiveSpan(context.active()))); } - return tracer.startActiveSpan(name, spanOptions, ctx, span => { - patchSpanEnd(span); - return handleCallbackErrors( - () => callback(span), - () => { - // Only set the span status to ERROR when there wasn't any error status set before, in order to avoid stomping useful span statuses - if (spanToJSON(span).status === 'ok') { - span.setStatus({ code: SpanStatusCode.ERROR }); - } - }, - autoEnd ? () => span.end() : undefined, - ); - }); + return startActiveSpan(activeCtx); }); } @@ -156,7 +141,6 @@ export function startInactiveSpan(options: OpenTelemetrySpanContext): Span { return wrapper(() => { const activeCtx = getContext(options.scope, options.forceTransaction); const missingRequiredParent = options.onlyIfParent && !trace.getSpan(activeCtx); - let ctx = missingRequiredParent ? suppressTracing(activeCtx) : activeCtx; if (missingRequiredParent) { getClient()?.recordDroppedEvent('no_parent_span', 'span'); @@ -164,13 +148,19 @@ export function startInactiveSpan(options: OpenTelemetrySpanContext): Span { const spanOptions = getSpanOptions(options); - if (!hasSpansEnabled()) { - ctx = isTracingSuppressed(ctx) ? ctx : suppressTracing(ctx); + const startSpan = (ctx: Context): Span => { + const span = tracer.startSpan(name, spanOptions, ctx); + patchSpanEnd(span); + return span; + }; + + // We suppress tracing (making the span non-recording) when a required parent is missing, or when + // spans are disabled entirely. We rely on `@sentry/core`'s scope-based suppression for this. + if (missingRequiredParent || !hasSpansEnabled()) { + return context.with(activeCtx, () => suppressTracing(() => startSpan(context.active()))); } - const span = tracer.startSpan(name, spanOptions, ctx); - patchSpanEnd(span); - return span; + return startSpan(activeCtx); }); } diff --git a/packages/opentelemetry/src/tracer.ts b/packages/opentelemetry/src/tracer.ts index 69dab7b9ef1c..d0bd04bbfb4d 100644 --- a/packages/opentelemetry/src/tracer.ts +++ b/packages/opentelemetry/src/tracer.ts @@ -1,6 +1,5 @@ import type { Context, Span as OpenTelemetrySpan, SpanOptions, Tracer } from '@opentelemetry/api'; import { context, trace } from '@opentelemetry/api'; -import { isTracingSuppressed } from '@opentelemetry/core'; import { _INTERNAL_safeMathRandom, _INTERNAL_setSpanForScope, @@ -24,6 +23,7 @@ import type { Span, SpanAttributes, SpanLink } from '@sentry/core'; import { applyOtelSpanData } from './applyOtelSpanData'; import { SENTRY_FORK_SET_ISOLATION_SCOPE_CONTEXT_KEY, SENTRY_TRACE_STATE_DSC } from './constants'; import { getSamplingDecision } from './utils/getSamplingDecision'; +import { isTracingSuppressed } from './utils/suppressTracing'; import { SENTRY_KIND } from '@sentry/conventions/attributes'; export class SentryTracer implements Tracer { diff --git a/packages/opentelemetry/src/utils/suppressTracing.ts b/packages/opentelemetry/src/utils/suppressTracing.ts new file mode 100644 index 000000000000..b9827aaf42a2 --- /dev/null +++ b/packages/opentelemetry/src/utils/suppressTracing.ts @@ -0,0 +1,13 @@ +import type { Context } from '@opentelemetry/api'; +import { isTracingSuppressed as isScopeTracingSuppressed } from '@sentry/core'; +import { getScopesFromContext } from './contextData'; + +/** + * Check if tracing is suppressed on the given context. + * + * This adapts `@sentry/core`'s scope-based `isTracingSuppressed` to an OpenTelemetry context by reading + * the Sentry scope stored on it. Suppression is set via `@sentry/core`'s `Sentry.suppressTracing()`. + */ +export function isTracingSuppressed(context: Context): boolean { + return isScopeTracingSuppressed(getScopesFromContext(context)?.scope); +} diff --git a/packages/opentelemetry/test/propagator.test.ts b/packages/opentelemetry/test/propagator.test.ts index 7610f040a72d..0067258793c9 100644 --- a/packages/opentelemetry/test/propagator.test.ts +++ b/packages/opentelemetry/test/propagator.test.ts @@ -7,8 +7,7 @@ import { trace, TraceFlags, } from '@opentelemetry/api'; -import { suppressTracing } from '@opentelemetry/core'; -import { getCurrentScope, withScope } from '@sentry/core'; +import { getCurrentScope, suppressTracing, withScope } from '@sentry/core'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { SENTRY_BAGGAGE_HEADER, SENTRY_SCOPES_CONTEXT_KEY, SENTRY_TRACE_HEADER } from '../src/constants'; import { SentryPropagator } from '../src/propagator'; @@ -550,8 +549,8 @@ describe('SentryPropagator', () => { traceFlags: TraceFlags.SAMPLED, }; - const context = suppressTracing(trace.setSpanContext(ROOT_CONTEXT, spanContext)); - propagator.inject(context, carrier, defaultTextMapSetter); + const suppressedContext = suppressTracing(() => trace.setSpanContext(context.active(), spanContext)); + propagator.inject(suppressedContext, carrier, defaultTextMapSetter); expect(carrier[SENTRY_TRACE_HEADER]).toBe(undefined); expect(carrier[SENTRY_BAGGAGE_HEADER]).toBe(undefined); }); diff --git a/packages/opentelemetry/test/trace.test.ts b/packages/opentelemetry/test/trace.test.ts index dcc1b1aaf118..a83ea886c43b 100644 --- a/packages/opentelemetry/test/trace.test.ts +++ b/packages/opentelemetry/test/trace.test.ts @@ -530,7 +530,7 @@ describe('trace', () => { // TODO: propagation scope is not picked up by spans... describe('onlyIfParent', () => { - it('does not create a span and records no_parent_span client report if there is no parent', () => { + it('does not create a recording span and records no_parent_span client report if there is no parent', () => { const client = getClient()!; const spyOnDroppedEvent = vi.spyOn(client, 'recordDroppedEvent'); @@ -538,7 +538,8 @@ describe('trace', () => { return span; }); - expect(isSpan(span)).toBe(false); + expect(span.isRecording()).toBe(false); + expect(spanIsSampled(span)).toBe(false); expect(spyOnDroppedEvent).toHaveBeenCalledWith('no_parent_span', 'span'); expect(spyOnDroppedEvent).toHaveBeenCalledTimes(1); }); @@ -859,13 +860,14 @@ describe('trace', () => { }); describe('onlyIfParent', () => { - it('does not create a span and records no_parent_span client report if there is no parent', () => { + it('does not create a recording span and records no_parent_span client report if there is no parent', () => { const client = getClient()!; const spyOnDroppedEvent = vi.spyOn(client, 'recordDroppedEvent'); const span = startInactiveSpan({ name: 'test span', onlyIfParent: true }); - expect(isSpan(span)).toBe(false); + expect(span.isRecording()).toBe(false); + expect(spanIsSampled(span)).toBe(false); expect(spyOnDroppedEvent).toHaveBeenCalledWith('no_parent_span', 'span'); expect(spyOnDroppedEvent).toHaveBeenCalledTimes(1); }); @@ -1260,7 +1262,7 @@ describe('trace', () => { }); describe('onlyIfParent', () => { - it('does not create a span and records no_parent_span client report if there is no parent', () => { + it('does not create a recording span and records no_parent_span client report if there is no parent', () => { const client = getClient()!; const spyOnDroppedEvent = vi.spyOn(client, 'recordDroppedEvent'); @@ -1268,7 +1270,8 @@ describe('trace', () => { return span; }); - expect(isSpan(span)).toBe(false); + expect(span.isRecording()).toBe(false); + expect(spanIsSampled(span)).toBe(false); expect(spyOnDroppedEvent).toHaveBeenCalledWith('no_parent_span', 'span'); expect(spyOnDroppedEvent).toHaveBeenCalledTimes(1); }); diff --git a/packages/opentelemetry/test/tracerProvider.test.ts b/packages/opentelemetry/test/tracerProvider.test.ts index 27e074bbd85a..abd2bb4fb3f2 100644 --- a/packages/opentelemetry/test/tracerProvider.test.ts +++ b/packages/opentelemetry/test/tracerProvider.test.ts @@ -1,5 +1,4 @@ import { context, SpanKind, trace, TraceFlags } from '@opentelemetry/api'; -import { suppressTracing } from '@opentelemetry/core'; import { getActiveSpan, getCapturedScopesOnSpan, @@ -9,6 +8,7 @@ import { SPAN_STATUS_ERROR, SPAN_STATUS_OK, startSpanManual, + suppressTracing, type Span, withIsolationScope, } from '@sentry/core'; @@ -78,8 +78,7 @@ describe('SentryTracerProvider', () => { it('links non-recording spans to a suppressed active parent', () => { trace.getTracer('test').startActiveSpan('parent', parent => { - const suppressedContext = suppressTracing(context.active()); - const child = trace.getTracer('test').startSpan('child', {}, suppressedContext); + const child = suppressTracing(() => trace.getTracer('test').startSpan('child')); expect(child.isRecording()).toBe(false); expect(spanToJSON(child as Span).trace_id).toBe(parent.spanContext().traceId); @@ -93,8 +92,7 @@ describe('SentryTracerProvider', () => { it('captures scopes on suppressed spans so startActiveSpan can fork the isolation scope', () => { withIsolationScope(isolationScope => { - const suppressedContext = suppressTracing(context.active()); - const span = trace.getTracer('test').startSpan('child', {}, suppressedContext); + const span = suppressTracing(() => trace.getTracer('test').startSpan('child')); // Without captured scopes, startActiveSpan cannot fork the isolation scope onto the context. expect(getCapturedScopesOnSpan(span as unknown as Span).isolationScope).toBe(isolationScope);