@@ -11,12 +11,28 @@ import { isSentryRequestSpan } from './utils/isSentryRequest';
1111import { mapOtelStatus } from './utils/mapOtelStatus' ;
1212import { parseSpanDescription } from './utils/parseOtelSpanDescription' ;
1313
14+ interface SpanProcessorOptions {
15+ /**
16+ * By default, if a span is started and we cannot find a Sentry parent span for it,
17+ * even if the OTEL span has a parent reference, we will still create the Sentry span as a root span.
18+ *
19+ * While this is more tolerant of errors, it means that the generated Spans in Sentry may have an incorrect hierarchy.
20+ *
21+ * When opting into strict span parent handling, we will discard any Spans where we can't find the corresponding parent.
22+ * This also requires that we defer clearing of references to the point where the root span is finished -
23+ * as sometimes these are not fired in correct order, leading to spans being dropped.
24+ *
25+ * Note that enabling this is the more correct option
26+ * and will probably eventually become the default in a future version.
27+ */
28+ strictSpanParentHandling : boolean ;
29+ }
30+
1431export const SENTRY_SPAN_PROCESSOR_MAP : Map < string , SentrySpan > = new Map < string , SentrySpan > ( ) ;
1532
16- // make sure to remove references in maps, to ensure this can be GCed
17- function clearSpan ( otelSpanId : string ) : void {
18- SENTRY_SPAN_PROCESSOR_MAP . delete ( otelSpanId ) ;
19- }
33+ // A map of a sentry span ID to a list of otel span IDs
34+ // When the sentry span is finished, clear all references of the given otel spans
35+ export const SCHEDULE_TO_CLEAR : Map < string , string [ ] > = new Map < string , string [ ] > ( ) ;
2036
2137/** Get a Sentry span for an otel span ID. */
2238export function getSentrySpan ( otelSpanId : string ) : SentrySpan | undefined {
@@ -28,7 +44,12 @@ export function getSentrySpan(otelSpanId: string): SentrySpan | undefined {
2844 * the Sentry SDK.
2945 */
3046export class SentrySpanProcessor implements OtelSpanProcessor {
31- public constructor ( ) {
47+ private _strictSpanParentHandling : boolean ;
48+
49+ public constructor ( { strictSpanParentHandling } : Partial < SpanProcessorOptions > = { } ) {
50+ // Default to false
51+ this . _strictSpanParentHandling = ! ! strictSpanParentHandling ;
52+
3253 addTracingExtensions ( ) ;
3354
3455 addGlobalEventProcessor ( event => {
@@ -64,6 +85,13 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
6485 // so we cannot use hub.getSpan(), as we cannot rely on this being on the current span
6586 const sentryParentSpan = otelParentSpanId && SENTRY_SPAN_PROCESSOR_MAP . get ( otelParentSpanId ) ;
6687
88+ if ( this . _strictSpanParentHandling && ! ! otelParentSpanId && ! sentryParentSpan ) {
89+ logger . warn (
90+ `SentrySpanProcessor could not find parent span with OTEL-spanId ${ otelParentSpanId } . Dropping the span with OTEL-spanID ${ otelSpanId } ...` ,
91+ ) ;
92+ return ;
93+ }
94+
6795 if ( sentryParentSpan ) {
6896 const sentryChildSpan = sentryParentSpan . startChild ( {
6997 description : otelSpan . name ,
@@ -105,7 +133,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
105133 // leading to an infinite loop.
106134 // In this case, we do not want to finish the span, in order to avoid sending it to Sentry
107135 if ( isSentryRequestSpan ( otelSpan ) ) {
108- clearSpan ( otelSpanId ) ;
136+ this . _clearSpan ( otelSpanId ) ;
109137 return ;
110138 }
111139
@@ -115,7 +143,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
115143 client && client . emit && client ?. emit ( 'otelSpanEnd' , otelSpan , mutableOptions ) ;
116144
117145 if ( mutableOptions . drop ) {
118- clearSpan ( otelSpanId ) ;
146+ this . _clearSpan ( otelSpanId ) ;
119147 return ;
120148 }
121149
@@ -168,7 +196,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
168196
169197 sentrySpan . finish ( convertOtelTimeToSeconds ( otelSpan . endTime ) ) ;
170198
171- clearSpan ( otelSpanId ) ;
199+ this . _clearSpan ( otelSpanId ) ;
172200 }
173201
174202 /**
@@ -188,6 +216,17 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
188216 }
189217 return Promise . resolve ( ) ;
190218 }
219+
220+ /**
221+ * Clear all references for a given OTEL span.
222+ */
223+ private _clearSpan ( otelSpanId : string ) : void {
224+ if ( this . _strictSpanParentHandling ) {
225+ scheduleToClear ( otelSpanId ) ;
226+ } else {
227+ clearSpan ( otelSpanId ) ;
228+ }
229+ }
191230}
192231
193232function getTraceData ( otelSpan : OtelSpan , parentContext : Context ) : Partial < TransactionContext > {
@@ -263,3 +302,52 @@ function updateTransactionWithOtelData(transaction: Transaction, otelSpan: OtelS
263302function convertOtelTimeToSeconds ( [ seconds , nano ] : [ number , number ] ) : number {
264303 return seconds + nano / 1_000_000_000 ;
265304}
305+
306+ function scheduleToClear ( otelSpanId : string ) : void {
307+ const span = SENTRY_SPAN_PROCESSOR_MAP . get ( otelSpanId ) ;
308+
309+ if ( ! span ) {
310+ // hmm, something is fishy here, but abort...
311+ return ;
312+ }
313+
314+ const sentrySpanId = span . spanId ;
315+
316+ // This is the root, clear all that have been scheduled
317+ if ( spanIsRoot ( span ) || ! span . transaction ) {
318+ const toClear = SCHEDULE_TO_CLEAR . get ( sentrySpanId ) || [ ] ;
319+ toClear . push ( otelSpanId ) ;
320+
321+ toClear . forEach ( otelSpanIdToClear => clearSpan ( otelSpanIdToClear ) ) ;
322+ SCHEDULE_TO_CLEAR . delete ( sentrySpanId ) ;
323+ return ;
324+ }
325+
326+ // Clear when root span is cleared
327+ const root = span . transaction ;
328+ const rootSentrySpanId = root . spanId ;
329+
330+ const toClear = SCHEDULE_TO_CLEAR . get ( root . spanId ) ;
331+
332+ // If this does not exist, it means we prob. already cleaned it up before
333+ // So we ignore the parent and just clean this span up right now
334+ if ( ! toClear ) {
335+ clearSpan ( otelSpanId ) ;
336+ return ;
337+ }
338+
339+ toClear . push ( otelSpanId ) ;
340+
341+ if ( ! SCHEDULE_TO_CLEAR . has ( rootSentrySpanId ) ) {
342+ SCHEDULE_TO_CLEAR . set ( rootSentrySpanId , toClear ) ;
343+ }
344+ }
345+
346+ function spanIsRoot ( span : SentrySpan ) : span is Transaction {
347+ return span . transaction === span ;
348+ }
349+
350+ // make sure to remove references in maps, to ensure this can be GCed
351+ function clearSpan ( otelSpanId : string ) : void {
352+ SENTRY_SPAN_PROCESSOR_MAP . delete ( otelSpanId ) ;
353+ }
0 commit comments