Skip to content
Draft
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
9 changes: 0 additions & 9 deletions packages/cloudflare/src/async.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,16 +69,7 @@ export function setAsyncLocalStorageAsyncContextStrategy(): void {
});
}

// In contrast to the browser, we can rely on async context isolation here
function suppressTracing<T>(callback: () => T): T {
return withScope(scope => {
scope.setSDKProcessingMetadata({ __SENTRY_SUPPRESS_TRACING__: true });
return callback();
});
}

setAsyncContextStrategy({
suppressTracing,
withScope,
withSetScope,
withIsolationScope,
Expand Down
18 changes: 18 additions & 0 deletions packages/core/src/asyncContext/stackStrategy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Client } from '../client';
import { getDefaultCurrentScope, getDefaultIsolationScope } from '../defaultScopes';
import { Scope } from '../scope';
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';
Expand Down Expand Up @@ -148,6 +149,7 @@ function withIsolationScope<T>(callback: (isolationScope: Scope) => T): T {
*/
export function getStackAsyncContextStrategy(): AsyncContextStrategy {
return {
suppressTracing: suppressTracingInStack,
withIsolationScope,
withScope,
withSetScope,
Expand All @@ -158,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<T>(callback: () => T): T {
return withScope(scope => {
scope.setSDKProcessingMetadata({ [SUPPRESS_TRACING_KEY]: true });
const res = callback();
scope.setSDKProcessingMetadata({ [SUPPRESS_TRACING_KEY]: undefined });
return res;
});
}
6 changes: 3 additions & 3 deletions packages/core/src/integrations/http/client-subscriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';

Expand All @@ -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;
}

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/tracing/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const SUPPRESS_TRACING_KEY = '__SENTRY_SUPPRESS_TRACING__';
1 change: 0 additions & 1 deletion packages/core/src/tracing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ export {
isTracingSuppressed,
startNewTrace,
spanIsIgnored,
SUPPRESS_TRACING_KEY,
} from './trace';
export { bindScopeToEmitter } from './bindScopeToEmitter';
export {
Expand Down
12 changes: 2 additions & 10 deletions packages/core/src/tracing/trace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -293,15 +292,8 @@ export function suppressTracing<T>(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 });
const res = callback();
scope.setSDKProcessingMetadata({ [SUPPRESS_TRACING_KEY]: undefined });
return res;
return callback();
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 & {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/test/lib/tracing/trace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import {
startInactiveSpan,
startSpan,
startSpanManual,
SUPPRESS_TRACING_KEY,
suppressTracing,
withActiveSpan,
} from '../../../src/tracing';
Expand All @@ -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',
Expand Down
9 changes: 0 additions & 9 deletions packages/deno/src/async.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,16 +77,7 @@ export function setAsyncLocalStorageAsyncContextStrategy(): void {
});
}

// In contrast to the browser, we can rely on async context isolation here
function suppressTracing<T>(callback: () => T): T {
return withScope(scope => {
scope.setSDKProcessingMetadata({ __SENTRY_SUPPRESS_TRACING__: true });
return callback();
});
}

setAsyncContextStrategy({
suppressTracing,
withScope,
withSetScope,
withIsolationScope,
Expand Down
8 changes: 7 additions & 1 deletion packages/node/src/integrations/fs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
},
};
});
3 changes: 0 additions & 3 deletions packages/opentelemetry/src/asyncContextStrategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -103,8 +102,6 @@ export function setOpenTelemetryContextAsyncContextStrategy(options?: {
startSpanManual,
startInactiveSpan,
getActiveSpan,
suppressTracing,
isTracingSuppressed,
Comment thread
cursor[bot] marked this conversation as resolved.
getTraceData,
continueTrace,
startNewTrace,
Expand Down
2 changes: 0 additions & 2 deletions packages/opentelemetry/src/exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ export { enhanceDscWithOpenTelemetryRootSpanName } from './utils/enhanceDscWithO

export { getTraceContextForScope } from './trace';

export { suppressTracing } from './utils/suppressTracing';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Public export removed without notice

Medium Severity

suppressTracing was removed from @sentry/opentelemetry's public exports without a deprecation period. This violates the PR review rule on breaking changes for publicly exported APIs. Flagged because it was mentioned in the rules file.

Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit a7c1182. Configure here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we do not care about opentelemetry package removals


export { setupEventContextTrace } from './setupEventContextTrace';

// eslint-disable-next-line typescript/no-deprecated
Expand Down
3 changes: 2 additions & 1 deletion packages/opentelemetry/src/propagator.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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.
Expand Down
6 changes: 5 additions & 1 deletion packages/opentelemetry/src/sampler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 });
}

Expand Down
92 changes: 41 additions & 51 deletions packages/opentelemetry/src/trace.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -25,6 +24,7 @@ import {
SEMANTIC_ATTRIBUTE_SENTRY_OP,
spanToJSON,
spanToTraceContext,
suppressTracing,
} from '@sentry/core';
import { continueTraceAsRemoteSpan } from './propagator';
import type { OpenTelemetrySpanContext } from './types';
Expand All @@ -49,59 +49,44 @@ function _startSpan<T>(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);
});
}

Expand Down Expand Up @@ -156,21 +141,26 @@ 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');
}

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);
});
}

Expand Down
2 changes: 1 addition & 1 deletion packages/opentelemetry/src/tracer.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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 {
Expand Down
Loading
Loading