From e3c79783f95bd8d7279c9a2f40a4919395997449 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Tue, 9 Jun 2026 14:35:29 +0200 Subject: [PATCH] ref(core): Gate `updateName()` custom source on an OTel inference brand SentrySpan's `updateName()` now sets `sentry.source: 'custom'` only if the span was not started by a tracer (SentryTrace in the future). With the upcoming SentryTracerProvider, that creates native SentrySpans instead of OTel SDK spans, we need a way to make sure `updateName()` does not set source 'custom' on all spans unconditionally. Otherwise, spans created, that had their name updated, outside of Sentry's influence (i.e. inside frameworks like nextjs) would also have source set to custom which interferes with the final source inference at the end of a span. The need for this will be more apparent in follow-up PRs when we introduce the SentryTracerProvider and data inference that used to be done in our span exporter before. --- packages/core/src/tracing/index.ts | 7 +++++- packages/core/src/tracing/sentrySpan.ts | 11 +++++++-- packages/core/src/tracing/utils.ts | 24 +++++++++++++++++++ .../core/test/lib/tracing/sentrySpan.test.ts | 22 +++++++++++++++++ 4 files changed, 61 insertions(+), 3 deletions(-) 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', () => {