Skip to content

Commit 8ebaf6b

Browse files
committed
WIP
1 parent 749e7e7 commit 8ebaf6b

10 files changed

Lines changed: 71 additions & 207 deletions

File tree

packages/core/src/tracing/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export { SPAN_STATUS_ERROR, SPAN_STATUS_OK, SPAN_STATUS_UNSET } from './spanstat
88
export {
99
startSpan,
1010
startInactiveSpan,
11+
_INTERNAL_startInactiveSpan,
1112
startSpanManual,
1213
continueTrace,
1314
withActiveSpan,

packages/core/src/tracing/sentrySpan.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,16 @@ export class SentrySpan implements Span {
200200
*/
201201
public updateName(name: string): this {
202202
this._name = name;
203-
this.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'custom');
203+
// Only set source to 'custom' when the span already has a source.
204+
// OTel instrumentations call updateName() on spans they create — in
205+
// SentryTraceProvider mode those are SentrySpans, not OTel SDK spans.
206+
// Child spans start without a source so that applyOtelSpanData can
207+
// infer the correct one (e.g. 'route', 'task') at span end.
208+
// Users who want to mark a name as intentionally chosen should use
209+
// updateSpanName() which always sets source via setAttributes().
210+
if (this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] !== undefined) {
211+
this.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'custom');
212+
}
204213
return this;
205214
}
206215

packages/core/src/tracing/trace.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,7 @@ import { generateTraceId } from '../utils/propagationContext';
2626
import { safeMathRandom } from '../utils/randomSafeContext';
2727
import { _getSpanForScope, _setSpanForScope } from '../utils/spanOnScope';
2828
import { dropUndefinedKeys } from '../utils/object';
29-
import {
30-
addChildSpanToSpan,
31-
getRootSpan,
32-
spanIsSampled,
33-
spanTimeInputToSeconds,
34-
spanToJSON,
35-
} from '../utils/spanUtils';
29+
import { addChildSpanToSpan, getRootSpan, spanIsSampled, spanTimeInputToSeconds, spanToJSON } from '../utils/spanUtils';
3630
import { propagationContextFromHeaders, shouldContinueTrace } from '../utils/tracing';
3731
import { freezeDscOnSpan, getDynamicSamplingContextFromSpan } from './dynamicSamplingContext';
3832
import { logSpanStart } from './logSpans';
@@ -197,6 +191,20 @@ export function startInactiveSpan(options: StartSpanOptions): Span {
197191
return acs.startInactiveSpan(options);
198192
}
199193

194+
return _startInactiveSpanImpl(options);
195+
}
196+
197+
/**
198+
* Internal version of startInactiveSpan that bypasses the ACS check.
199+
* Used by SentryTraceProvider to create spans without triggering recursion
200+
* through ACS overrides.
201+
* @hidden
202+
*/
203+
export function _INTERNAL_startInactiveSpan(options: StartSpanOptions): Span {
204+
return _startInactiveSpanImpl(options);
205+
}
206+
207+
function _startInactiveSpanImpl(options: StartSpanOptions): Span {
200208
const spanArguments = parseSentrySpanArguments(options);
201209
const { forceTransaction, parentSpan: customParentSpan } = options;
202210

packages/node/src/sdk/initOtel.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ export function setupOtel(
9292
options: AdditionalOpenTelemetryOptions = {},
9393
): [OpenTelemetryTraceProvider | undefined, AsyncLocalStorageLookup | undefined] {
9494
if (client.getOptions()._experiments?.useSentryTraceProvider) {
95-
setOpenTelemetryContextAsyncContextStrategy({ useOpenTelemetrySpanCreation: false });
95+
setOpenTelemetryContextAsyncContextStrategy();
9696
return setupSentryTraceProvider(client, options);
9797
}
9898

Lines changed: 5 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,23 @@
11
import * as api from '@opentelemetry/api';
2-
import type { Scope, Span, withActiveSpan as defaultWithActiveSpan } from '@sentry/core';
3-
import {
4-
_INTERNAL_safeMathRandom,
5-
_INTERNAL_setSpanForScope,
6-
baggageHeaderToDynamicSamplingContext,
7-
getDefaultCurrentScope,
8-
getDefaultIsolationScope,
9-
setAsyncContextStrategy,
10-
} from '@sentry/core';
2+
import type { Scope, withActiveSpan as defaultWithActiveSpan } from '@sentry/core';
3+
import { getDefaultCurrentScope, getDefaultIsolationScope, setAsyncContextStrategy } from '@sentry/core';
114
import {
125
SENTRY_FORK_ISOLATION_SCOPE_CONTEXT_KEY,
136
SENTRY_FORK_SET_ISOLATION_SCOPE_CONTEXT_KEY,
147
SENTRY_FORK_SET_SCOPE_CONTEXT_KEY,
15-
SENTRY_TRACE_STATE_DSC,
16-
SENTRY_TRACE_STATE_SAMPLE_RAND,
178
} from './constants';
189
import { continueTrace, startInactiveSpan, startNewTrace, startSpan, startSpanManual, withActiveSpan } from './trace';
1910
import type { CurrentScopes } from './types';
2011
import { getContextFromScope, getScopesFromContext } from './utils/contextData';
21-
import { getSamplingDecision } from './utils/getSamplingDecision';
2212
import { getActiveSpan } from './utils/getActiveSpan';
2313
import { getTraceData } from './utils/getTraceData';
2414
import { suppressTracing } from './utils/suppressTracing';
25-
import { isSentryTraceProviderSpan } from './sentryTraceProvider';
2615

2716
/**
2817
* Sets the async context strategy to use follow the OTEL context under the hood.
2918
* We handle forking a hub inside of our custom OTEL Context Manager (./otelContextManager.ts)
3019
*/
31-
export function setOpenTelemetryContextAsyncContextStrategy(
32-
options: { useOpenTelemetrySpanCreation?: boolean } = {},
33-
): void {
34-
const { useOpenTelemetrySpanCreation = true } = options;
35-
20+
export function setOpenTelemetryContextAsyncContextStrategy(): void {
3621
function getScopes(): CurrentScopes {
3722
const ctx = api.context.active();
3823
const scopes = getScopesFromContext(ctx);
@@ -52,11 +37,6 @@ export function setOpenTelemetryContextAsyncContextStrategy(
5237
function withScope<T>(callback: (scope: Scope) => T): T {
5338
const ctx = api.context.active();
5439

55-
// We depend on the otelContextManager to handle the context/hub
56-
// We set the `SENTRY_FORK_ISOLATION_SCOPE_CONTEXT_KEY` context value, which is picked up by
57-
// the OTEL context manager, which uses the presence of this key to determine if it should
58-
// fork the isolation scope, or not
59-
// as by default, we don't want to fork this, unless triggered explicitly by `withScope`
6040
return api.context.with(ctx, () => {
6141
return callback(getCurrentScope());
6242
});
@@ -65,9 +45,6 @@ export function setOpenTelemetryContextAsyncContextStrategy(
6545
function withSetScope<T>(scope: Scope, callback: (scope: Scope) => T): T {
6646
const ctx = getContextFromScope(scope) || api.context.active();
6747

68-
// We depend on the otelContextManager to handle the context/hub
69-
// We set the `SENTRY_FORK_SET_SCOPE_CONTEXT_KEY` context value, which is picked up by
70-
// the OTEL context manager, which picks up this scope as the current scope
7148
return api.context.with(ctx.setValue(SENTRY_FORK_SET_SCOPE_CONTEXT_KEY, scope), () => {
7249
return callback(scope);
7350
});
@@ -76,10 +53,6 @@ export function setOpenTelemetryContextAsyncContextStrategy(
7653
function withIsolationScope<T>(callback: (isolationScope: Scope) => T): T {
7754
const ctx = api.context.active();
7855

79-
// We depend on the otelContextManager to handle the context/hub
80-
// We set the `SENTRY_FORK_ISOLATION_SCOPE_CONTEXT_KEY` context value, which is picked up by
81-
// the OTEL context manager, which uses the presence of this key to determine if it should
82-
// fork the isolation scope, or not
8356
return api.context.with(ctx.setValue(SENTRY_FORK_ISOLATION_SCOPE_CONTEXT_KEY, true), () => {
8457
return callback(getIsolationScope());
8558
});
@@ -88,102 +61,26 @@ export function setOpenTelemetryContextAsyncContextStrategy(
8861
function withSetIsolationScope<T>(isolationScope: Scope, callback: (isolationScope: Scope) => T): T {
8962
const ctx = api.context.active();
9063

91-
// We depend on the otelContextManager to handle the context/hub
92-
// We set the `SENTRY_FORK_ISOLATION_SCOPE_CONTEXT_KEY` context value, which is picked up by
93-
// the OTEL context manager, which uses the presence of this key to determine if it should
94-
// fork the isolation scope, or not
9564
return api.context.with(ctx.setValue(SENTRY_FORK_SET_ISOLATION_SCOPE_CONTEXT_KEY, isolationScope), () => {
9665
return callback(getIsolationScope());
9766
});
9867
}
9968

10069
function getCurrentScope(): Scope {
101-
const scope = getScopes().scope;
102-
if (!useOpenTelemetrySpanCreation) {
103-
syncOpenTelemetrySpanWithScope(scope);
104-
}
105-
return scope;
70+
return getScopes().scope;
10671
}
10772

10873
function getIsolationScope(): Scope {
10974
return getScopes().isolationScope;
11075
}
11176

112-
function withActiveSpanContextOnly<T>(span: Span | null, callback: (scope: Scope) => T): T {
113-
const ctx = span
114-
? api.trace.setSpan(api.context.active(), span as api.Span)
115-
: api.trace.deleteSpan(api.context.active());
116-
117-
return api.context.with(ctx, () => {
118-
const scope = getCurrentScope();
119-
_INTERNAL_setSpanForScope(scope, span || undefined);
120-
return callback(scope);
121-
});
122-
}
123-
124-
function syncOpenTelemetrySpanWithScope(scope: Scope): void {
125-
const activeSpan = api.trace.getSpan(api.context.active()) as Span | undefined;
126-
127-
if (!activeSpan) {
128-
return;
129-
}
130-
131-
const scopeSpan = scope.getScopeData().span;
132-
if (scopeSpan === activeSpan) {
133-
return;
134-
}
135-
136-
const activeSpanContext = activeSpan.spanContext();
137-
if (activeSpanContext.isRemote) {
138-
if (scopeSpan) {
139-
return;
140-
}
141-
142-
// A remote OTel span context represents an incoming parent, not a local span
143-
// we can finish and send. Store it as propagation context so the next core
144-
// root span continues the trace and becomes the transaction segment.
145-
const dsc =
146-
baggageHeaderToDynamicSamplingContext(activeSpanContext.traceState?.get(SENTRY_TRACE_STATE_DSC)) ?? {};
147-
const sampleRandString = activeSpanContext.traceState?.get(SENTRY_TRACE_STATE_SAMPLE_RAND) ?? dsc?.sample_rand;
148-
const sampleRand = typeof sampleRandString === 'string' ? Number(sampleRandString) : undefined;
149-
150-
scope.setPropagationContext({
151-
traceId: activeSpanContext.traceId,
152-
parentSpanId: activeSpanContext.spanId,
153-
sampled: getSamplingDecision(activeSpanContext),
154-
dsc,
155-
sampleRand:
156-
typeof sampleRand === 'number' && !Number.isNaN(sampleRand) ? sampleRand : _INTERNAL_safeMathRandom(),
157-
});
158-
return;
159-
}
160-
161-
if (scopeSpan && !isSentryTraceProviderSpan(scopeSpan)) {
162-
return;
163-
}
164-
165-
_INTERNAL_setSpanForScope(scope, activeSpan);
166-
}
167-
168-
const baseStrategy = {
77+
setAsyncContextStrategy({
16978
withScope,
17079
withSetScope,
17180
withSetIsolationScope,
17281
withIsolationScope,
17382
getCurrentScope,
17483
getIsolationScope,
175-
};
176-
177-
if (!useOpenTelemetrySpanCreation) {
178-
setAsyncContextStrategy({
179-
...baseStrategy,
180-
withActiveSpan: withActiveSpanContextOnly as typeof defaultWithActiveSpan,
181-
});
182-
return;
183-
}
184-
185-
setAsyncContextStrategy({
186-
...baseStrategy,
18784
startSpan,
18885
startSpanManual,
18986
startInactiveSpan,
@@ -192,8 +89,6 @@ export function setOpenTelemetryContextAsyncContextStrategy(
19289
getTraceData,
19390
continueTrace,
19491
startNewTrace,
195-
// The types here don't fully align, because our own `Span` type is narrower
196-
// than the OTEL one - but this is OK for here, as we now we'll only have OTEL spans passed around
19792
withActiveSpan: withActiveSpan as typeof defaultWithActiveSpan,
19893
});
19994
}

packages/opentelemetry/src/sentryTraceProvider.ts

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
spanToJSON,
2424
SPAN_STATUS_ERROR,
2525
SPAN_STATUS_OK,
26-
startInactiveSpan,
26+
_INTERNAL_startInactiveSpan,
2727
startNewTrace,
2828
withScope,
2929
} from '@sentry/core';
@@ -34,16 +34,6 @@ import { setIsSetup } from './utils/setupCheck';
3434

3535
type SentrySpanWithOtelKind = Span & { kind?: SpanKind };
3636
type SentrySpanWithOtelSourceInference = Span & { _sentryOtelInferSource?: boolean };
37-
type SentryTraceProviderSpan = Span & { _sentryTraceProviderSpan?: true };
38-
39-
export function isSentryTraceProviderSpan(span: Span | undefined): boolean {
40-
return (span as SentryTraceProviderSpan | undefined)?._sentryTraceProviderSpan === true;
41-
}
42-
43-
function markSentryTraceProviderSpan(span: Span): Span {
44-
addNonEnumerableProperty(span as SentryTraceProviderSpan, '_sentryTraceProviderSpan', true);
45-
return span;
46-
}
4737

4838
const HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE = 'http.response.status_code';
4939
const LEGACY_HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE = 'http.status_code';
@@ -137,7 +127,7 @@ class SentryTracer implements Tracer {
137127

138128
return context.with(parentContext, () => {
139129
const span = this._startSentrySpan(name, options, parentSpan, ctx !== undefined);
140-
markSentryTraceProviderSpan(span);
130+
141131
applyOtelSpanKind(span, options.kind);
142132
if (options.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === undefined) {
143133
addNonEnumerableProperty(span as SentrySpanWithOtelSourceInference, '_sentryOtelInferSource', true);
@@ -195,25 +185,25 @@ class SentryTracer implements Tracer {
195185
};
196186

197187
if (options.root) {
198-
return startNewTrace(() => startInactiveSpan({ ...sentryOptions, parentSpan: null }));
188+
return startNewTrace(() => _INTERNAL_startInactiveSpan({ ...sentryOptions, parentSpan: null }));
199189
}
200190

201191
if (parentSpan?.spanContext().isRemote) {
202192
return this._startRootSpanWithRemoteParent(sentryOptions, parentSpan);
203193
}
204194

205195
if (parentSpan) {
206-
return startInactiveSpan({ ...sentryOptions, parentSpan: parentSpan as unknown as Span });
196+
return _INTERNAL_startInactiveSpan({ ...sentryOptions, parentSpan: parentSpan as unknown as Span });
207197
}
208198

209-
return startInactiveSpan({
199+
return _INTERNAL_startInactiveSpan({
210200
...sentryOptions,
211201
parentSpan: hasExplicitContext ? null : undefined,
212202
});
213203
}
214204

215205
private _startRootSpanWithRemoteParent(
216-
options: Parameters<typeof startInactiveSpan>[0],
206+
options: Parameters<typeof _INTERNAL_startInactiveSpan>[0],
217207
parentSpan: OpenTelemetrySpan,
218208
): Span {
219209
const { spanId, traceId } = parentSpan.spanContext();
@@ -231,17 +221,14 @@ class SentryTracer implements Tracer {
231221
});
232222
_INTERNAL_setSpanForScope(scope, undefined);
233223

234-
return startInactiveSpan({ ...options, parentSpan: null });
224+
return _INTERNAL_startInactiveSpan({ ...options, parentSpan: null });
235225
});
236226
}
237227

238228
private _createNonRecordingSpan(parentSpan: OpenTelemetrySpan | undefined): OpenTelemetrySpan {
239-
const span = new SentryNonRecordingSpan({
229+
return new SentryNonRecordingSpan({
240230
traceId: parentSpan?.spanContext().traceId,
241-
});
242-
markSentryTraceProviderSpan(span);
243-
244-
return span as OpenTelemetrySpan;
231+
}) as OpenTelemetrySpan;
245232
}
246233
}
247234

@@ -266,9 +253,16 @@ export function applyOtelSpanData(span: Span, options: { finalizeStatus?: boolea
266253
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, inferred.op);
267254
}
268255

256+
// Don't apply 'url' source at creation time — only at span end (finalizeStatus).
257+
// At creation, http.route may not be set yet, so inference falls back to 'url'.
258+
// Keeping the default 'custom' source from _startRootSpan allows
259+
// enhanceDscWithOpenTelemetryRootSpanName to include the transaction name in
260+
// the DSC. At span end, http.route is typically available and inference returns
261+
// 'route' instead. If it's still 'url', it's applied then.
269262
const shouldApplyInferredSource =
270263
inferred.source !== undefined &&
271264
inferred.source !== 'custom' &&
265+
(options.finalizeStatus || inferred.source !== 'url') &&
272266
(spanJSON.parent_span_id === undefined || kind === SpanKind.SERVER);
273267

274268
if (

packages/opentelemetry/src/utils/enhanceDscWithOpenTelemetryRootSpanName.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import type { Client } from '@sentry/core';
22
import { hasSpansEnabled, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, spanToJSON } from '@sentry/core';
33
import { getSamplingDecision } from './getSamplingDecision';
44
import { parseSpanDescription } from './parseSpanDescription';
5-
import { spanHasName } from './spanTypes';
65

76
/**
87
* Setup a DSC handler on the passed client,
@@ -24,9 +23,11 @@ export function enhanceDscWithOpenTelemetryRootSpanName(client: Client): void {
2423
const attributes = jsonSpan.data;
2524
const source = attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE];
2625

27-
const { description } = spanHasName(rootSpan) ? parseSpanDescription(rootSpan) : { description: undefined };
28-
if (source !== 'url' && description) {
29-
dsc.transaction = description;
26+
if (jsonSpan.description) {
27+
const { description } = parseSpanDescription(rootSpan);
28+
if (source !== 'url' && description) {
29+
dsc.transaction = description;
30+
}
3031
}
3132

3233
// Also ensure sampling decision is correctly inferred

0 commit comments

Comments
 (0)