diff --git a/packages/core/src/tracing/index.ts b/packages/core/src/tracing/index.ts index 9b56045b37f3..57303947249b 100644 --- a/packages/core/src/tracing/index.ts +++ b/packages/core/src/tracing/index.ts @@ -1,5 +1,10 @@ export { registerSpanErrorInstrumentation } from './errors'; -export { setCapturedScopesOnSpan, getCapturedScopesOnSpan } from './utils'; +export { + setCapturedScopesOnSpan, + getCapturedScopesOnSpan, + markSpanForOtelSourceInference, + spanShouldInferOtelSource, +} from './utils'; export { startIdleSpan, TRACING_DEFAULTS } from './idleSpan'; export { SentrySpan } from './sentrySpan'; export { SentryNonRecordingSpan } from './sentryNonRecordingSpan'; diff --git a/packages/core/src/tracing/sentrySpan.ts b/packages/core/src/tracing/sentrySpan.ts index 84c78e73356d..8387c788d5fd 100644 --- a/packages/core/src/tracing/sentrySpan.ts +++ b/packages/core/src/tracing/sentrySpan.ts @@ -46,7 +46,7 @@ import { getDynamicSamplingContextFromSpan } from './dynamicSamplingContext'; import { logSpanEnd } from './logSpans'; import { timedEventsToMeasurements } from './measurement'; import { hasSpanStreamingEnabled } from './spans/hasSpanStreamingEnabled'; -import { getCapturedScopesOnSpan } from './utils'; +import { getCapturedScopesOnSpan, spanShouldInferOtelSource } from './utils'; const MAX_SPAN_COUNT = 1000; @@ -200,7 +200,14 @@ export class SentrySpan implements Span { */ public updateName(name: string): this { this._name = name; - this.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'custom'); + // Renaming a span marks its name as explicitly chosen, so we stamp `custom`. + // The exception is spans created by SentryTraceProvider: those are branded for + // OTel-style source inference at span end (mirroring OTel SDK spans, which have + // no Sentry source concept), so instrumentations renaming them must not pin + // `custom` — applyOtelSpanData infers the correct source (e.g. 'route', 'task'). + if (!spanShouldInferOtelSource(this)) { + this.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'custom'); + } return this; } diff --git a/packages/core/src/tracing/utils.ts b/packages/core/src/tracing/utils.ts index 0c0417a71a90..9de3a6f4ae77 100644 --- a/packages/core/src/tracing/utils.ts +++ b/packages/core/src/tracing/utils.ts @@ -6,11 +6,21 @@ import { derefWeakRef, makeWeakRef, type MaybeWeakRef } from '../utils/weakRef'; const SCOPE_ON_START_SPAN_FIELD = '_sentryScope'; const ISOLATION_SCOPE_ON_START_SPAN_FIELD = '_sentryIsolationScope'; +// Brand marking a span whose `sentry.source` should be inferred OTel-style at span end (by +// `applyOtelSpanData`) rather than pinned. `SentryTraceProvider` sets it on the spans it creates +// so they behave like OTel SDK spans, which carry no Sentry source concept. We use `Symbol.for` +// so the key is shared across duplicated copies of `@sentry/core`. +const OTEL_SOURCE_INFERENCE_SPAN_FIELD = Symbol.for('sentry.otelSourceInference'); + type SpanWithScopes = Span & { [SCOPE_ON_START_SPAN_FIELD]?: Scope; [ISOLATION_SCOPE_ON_START_SPAN_FIELD]?: MaybeWeakRef; }; +type SpanWithOtelSourceInference = Span & { + [OTEL_SOURCE_INFERENCE_SPAN_FIELD]?: boolean; +}; + /** Store the scope & isolation scope for a span, which can the be used when it is finished. */ export function setCapturedScopesOnSpan(span: Span | undefined, scope: Scope, isolationScope: Scope): void { if (span) { @@ -33,3 +43,17 @@ export function getCapturedScopesOnSpan(span: Span): { scope?: Scope; isolationS isolationScope: derefWeakRef(spanWithScopes[ISOLATION_SCOPE_ON_START_SPAN_FIELD]), }; } + +/** + * Mark a span as eligible for OTel-style `sentry.source` inference at span end. + * Set by `SentryTraceProvider` on the spans it creates; read by `SentrySpan.updateName()` and + * `applyOtelSpanData()`. + */ +export function markSpanForOtelSourceInference(span: Span): void { + addNonEnumerableProperty(span, OTEL_SOURCE_INFERENCE_SPAN_FIELD, true); +} + +/** Whether a span is marked for OTel-style `sentry.source` inference (see {@link markSpanForOtelSourceInference}). */ +export function spanShouldInferOtelSource(span: Span): boolean { + return (span as SpanWithOtelSourceInference)[OTEL_SOURCE_INFERENCE_SPAN_FIELD] === true; +} diff --git a/packages/core/test/lib/tracing/sentrySpan.test.ts b/packages/core/test/lib/tracing/sentrySpan.test.ts index 76b7186cd28f..dfb7840b4125 100644 --- a/packages/core/test/lib/tracing/sentrySpan.test.ts +++ b/packages/core/test/lib/tracing/sentrySpan.test.ts @@ -4,6 +4,7 @@ import { setCurrentClient } from '../../../src/sdk'; import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '../../../src/semanticAttributes'; import { SentrySpan } from '../../../src/tracing/sentrySpan'; import { SPAN_STATUS_ERROR } from '../../../src/tracing/spanstatus'; +import { markSpanForOtelSourceInference } from '../../../src/tracing/utils'; import type { SpanJSON } from '../../../src/types/span'; import { spanToJSON, TRACE_FLAG_NONE, TRACE_FLAG_SAMPLED } from '../../../src/utils/spanUtils'; import { timestampInSeconds } from '../../../src/utils/time'; @@ -37,6 +38,27 @@ describe('SentrySpan', () => { expect(spanJson.description).toEqual('new name'); expect(spanJson.data[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]).toEqual('custom'); }); + + it('sets the source to custom when calling updateName on a span without a source', () => { + const span = new SentrySpan({ name: 'original name' }); + + span.updateName('new name'); + + const spanJson = spanToJSON(span); + expect(spanJson.description).toEqual('new name'); + expect(spanJson.data[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]).toEqual('custom'); + }); + + it('does not set the source when calling updateName on a span marked for OTel source inference', () => { + const span = new SentrySpan({ name: 'original name' }); + markSpanForOtelSourceInference(span); + + span.updateName('new name'); + + const spanJson = spanToJSON(span); + expect(spanJson.description).toEqual('new name'); + expect(spanJson.data[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]).toBeUndefined(); + }); }); describe('setters', () => {