|
| 1 | +import type { Context, Span as OpenTelemetrySpan, SpanOptions, Tracer } from '@opentelemetry/api'; |
| 2 | +import { context, trace } from '@opentelemetry/api'; |
| 3 | +import { isTracingSuppressed } from '@opentelemetry/core'; |
| 4 | +import { |
| 5 | + _INTERNAL_safeMathRandom, |
| 6 | + _INTERNAL_setSpanForScope, |
| 7 | + _INTERNAL_startInactiveSpan, |
| 8 | + addChildSpanToSpan, |
| 9 | + getCapturedScopesOnSpan, |
| 10 | + getCurrentScope, |
| 11 | + getDynamicSamplingContextFromSpan, |
| 12 | + getIsolationScope, |
| 13 | + markSpanForOtelSourceInference, |
| 14 | + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, |
| 15 | + SentryNonRecordingSpan, |
| 16 | + setCapturedScopesOnSpan, |
| 17 | + startNewTrace, |
| 18 | + withScope, |
| 19 | +} from '@sentry/core'; |
| 20 | +import type { Span, SpanAttributes, SpanLink } from '@sentry/core'; |
| 21 | +import { applyOtelSpanData, applyOtelSpanKind } from './applyOtelSpanData'; |
| 22 | +import { SENTRY_FORK_SET_ISOLATION_SCOPE_CONTEXT_KEY } from './constants'; |
| 23 | +import { getSamplingDecision } from './utils/getSamplingDecision'; |
| 24 | + |
| 25 | +export class SentryTracer implements Tracer { |
| 26 | + /** @inheritdoc */ |
| 27 | + public startSpan(name: string, options: SpanOptions = {}, ctx?: Context): OpenTelemetrySpan { |
| 28 | + const parentContext = ctx || context.active(); |
| 29 | + const parentSpan = options.root ? undefined : trace.getSpan(parentContext); |
| 30 | + |
| 31 | + if (isTracingSuppressed(parentContext)) { |
| 32 | + return this._createNonRecordingSpan(parentSpan); |
| 33 | + } |
| 34 | + |
| 35 | + const span = this._startSentrySpan(name, options, parentSpan, ctx !== undefined); |
| 36 | + |
| 37 | + applyOtelSpanKind(span, options.kind); |
| 38 | + if (options.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === undefined) { |
| 39 | + markSpanForOtelSourceInference(span); |
| 40 | + } |
| 41 | + applyOtelSpanData(span); |
| 42 | + return span as OpenTelemetrySpan; |
| 43 | + } |
| 44 | + |
| 45 | + /** @inheritdoc */ |
| 46 | + public startActiveSpan<F extends (span: OpenTelemetrySpan) => unknown>(name: string, fn: F): ReturnType<F>; |
| 47 | + public startActiveSpan<F extends (span: OpenTelemetrySpan) => unknown>( |
| 48 | + name: string, |
| 49 | + options: SpanOptions, |
| 50 | + fn: F, |
| 51 | + ): ReturnType<F>; |
| 52 | + public startActiveSpan<F extends (span: OpenTelemetrySpan) => unknown>( |
| 53 | + name: string, |
| 54 | + options: SpanOptions, |
| 55 | + ctx: Context, |
| 56 | + fn: F, |
| 57 | + ): ReturnType<F>; |
| 58 | + public startActiveSpan<F extends (span: OpenTelemetrySpan) => unknown>( |
| 59 | + name: string, |
| 60 | + optionsOrFn: SpanOptions | F, |
| 61 | + contextOrFn?: Context | F, |
| 62 | + fn?: F, |
| 63 | + ): ReturnType<F> { |
| 64 | + const options = typeof optionsOrFn === 'function' ? {} : optionsOrFn; |
| 65 | + const ctx = typeof contextOrFn === 'function' || contextOrFn === undefined ? context.active() : contextOrFn; |
| 66 | + const callback = ( |
| 67 | + typeof optionsOrFn === 'function' ? optionsOrFn : typeof contextOrFn === 'function' ? contextOrFn : fn |
| 68 | + ) as F; |
| 69 | + |
| 70 | + const span = this.startSpan(name, options, ctx); |
| 71 | + let ctxWithSpan = trace.setSpan(ctx, span); |
| 72 | + |
| 73 | + const capturedIsolationScope = getCapturedScopesOnSpan(span as unknown as Span).isolationScope; |
| 74 | + if (capturedIsolationScope) { |
| 75 | + ctxWithSpan = ctxWithSpan.setValue(SENTRY_FORK_SET_ISOLATION_SCOPE_CONTEXT_KEY, capturedIsolationScope); |
| 76 | + } |
| 77 | + |
| 78 | + return context.with(ctxWithSpan, () => { |
| 79 | + _INTERNAL_setSpanForScope(getCurrentScope(), span as unknown as Span); |
| 80 | + return callback(span) as ReturnType<F>; |
| 81 | + }); |
| 82 | + } |
| 83 | + |
| 84 | + private _startSentrySpan( |
| 85 | + name: string, |
| 86 | + options: SpanOptions, |
| 87 | + parentSpan: OpenTelemetrySpan | undefined, |
| 88 | + hasExplicitContext: boolean, |
| 89 | + ): Span { |
| 90 | + const sentryOptions = { |
| 91 | + name, |
| 92 | + attributes: options.attributes as SpanAttributes | undefined, |
| 93 | + links: options.links as SpanLink[] | undefined, |
| 94 | + startTime: options.startTime, |
| 95 | + }; |
| 96 | + |
| 97 | + if (options.root) { |
| 98 | + return startNewTrace(() => _INTERNAL_startInactiveSpan({ ...sentryOptions, parentSpan: null })); |
| 99 | + } |
| 100 | + |
| 101 | + if (parentSpan?.spanContext().isRemote) { |
| 102 | + return this._startRootSpanWithRemoteParent(sentryOptions, parentSpan); |
| 103 | + } |
| 104 | + |
| 105 | + if (parentSpan) { |
| 106 | + return _INTERNAL_startInactiveSpan({ ...sentryOptions, parentSpan: parentSpan as unknown as Span }); |
| 107 | + } |
| 108 | + |
| 109 | + return _INTERNAL_startInactiveSpan({ |
| 110 | + ...sentryOptions, |
| 111 | + parentSpan: hasExplicitContext ? null : undefined, |
| 112 | + }); |
| 113 | + } |
| 114 | + |
| 115 | + private _startRootSpanWithRemoteParent( |
| 116 | + options: Parameters<typeof _INTERNAL_startInactiveSpan>[0], |
| 117 | + parentSpan: OpenTelemetrySpan, |
| 118 | + ): Span { |
| 119 | + const { spanId, traceId } = parentSpan.spanContext(); |
| 120 | + const dsc = getDynamicSamplingContextFromSpan(parentSpan as unknown as Span); |
| 121 | + const sampleRand = typeof dsc.sample_rand === 'string' ? Number(dsc.sample_rand) : undefined; |
| 122 | + |
| 123 | + return withScope(scope => { |
| 124 | + scope.setPropagationContext({ |
| 125 | + traceId, |
| 126 | + parentSpanId: spanId, |
| 127 | + sampled: getSamplingDecision(parentSpan.spanContext()), |
| 128 | + dsc, |
| 129 | + sampleRand: |
| 130 | + typeof sampleRand === 'number' && !Number.isNaN(sampleRand) ? sampleRand : _INTERNAL_safeMathRandom(), |
| 131 | + }); |
| 132 | + _INTERNAL_setSpanForScope(scope, undefined); |
| 133 | + |
| 134 | + return _INTERNAL_startInactiveSpan({ ...options, parentSpan: null }); |
| 135 | + }); |
| 136 | + } |
| 137 | + |
| 138 | + private _createNonRecordingSpan(parentSpan: OpenTelemetrySpan | undefined): OpenTelemetrySpan { |
| 139 | + const span = new SentryNonRecordingSpan({ traceId: parentSpan?.spanContext().traceId }); |
| 140 | + // Link to the parent (like core's `createChildOrRootSpan`) so `getRootSpan` and DSC |
| 141 | + // resolution reach the parent. Non-recording spans no longer carry a `parentSpanId`. |
| 142 | + if (parentSpan) { |
| 143 | + addChildSpanToSpan(parentSpan as unknown as Span, span); |
| 144 | + } |
| 145 | + // Capture the scopes (mirroring `createChildOrRootSpan`) so `startActiveSpan` can |
| 146 | + // fork the isolation scope onto the OTel context for work inside a suppressed span. |
| 147 | + setCapturedScopesOnSpan(span, getCurrentScope(), getIsolationScope()); |
| 148 | + return span as OpenTelemetrySpan; |
| 149 | + } |
| 150 | +} |
0 commit comments