Skip to content
Merged
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
7 changes: 6 additions & 1 deletion packages/core/src/tracing/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
11 changes: 9 additions & 2 deletions packages/core/src/tracing/sentrySpan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

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

Expand Down
24 changes: 24 additions & 0 deletions packages/core/src/tracing/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Scope>;
};

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) {
Expand All @@ -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;
}
22 changes: 22 additions & 0 deletions packages/core/test/lib/tracing/sentrySpan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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', () => {
Expand Down
Loading