@@ -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 "${ otelSpan . name } " with OTEL-spanID ${ otelSpanId } ...` ,
91+ ) ;
92+ return ;
93+ }
94+
6795 if ( sentryParentSpan ) {
6896 const sentryChildSpan = sentryParentSpan . startChild ( {
6997 description : otelSpan . name ,
@@ -84,6 +112,10 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
84112 } ) ;
85113
86114 SENTRY_SPAN_PROCESSOR_MAP . set ( otelSpanId , transaction ) ;
115+
116+ if ( this . _strictSpanParentHandling ) {
117+ SCHEDULE_TO_CLEAR . set ( transaction . spanId , [ ] ) ;
118+ }
87119 }
88120 }
89121
@@ -97,6 +129,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
97129 if ( ! sentrySpan ) {
98130 __DEBUG_BUILD__ &&
99131 logger . error ( `SentrySpanProcessor could not find span with OTEL-spanId ${ otelSpanId } to finish.` ) ;
132+ this . _clearSpan ( otelSpanId ) ;
100133 return ;
101134 }
102135
@@ -105,7 +138,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
105138 // leading to an infinite loop.
106139 // In this case, we do not want to finish the span, in order to avoid sending it to Sentry
107140 if ( isSentryRequestSpan ( otelSpan ) ) {
108- clearSpan ( otelSpanId ) ;
141+ this . _clearSpan ( otelSpanId ) ;
109142 return ;
110143 }
111144
@@ -115,7 +148,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
115148 client && client . emit && client ?. emit ( 'otelSpanEnd' , otelSpan , mutableOptions ) ;
116149
117150 if ( mutableOptions . drop ) {
118- clearSpan ( otelSpanId ) ;
151+ this . _clearSpan ( otelSpanId ) ;
119152 return ;
120153 }
121154
@@ -168,7 +201,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
168201
169202 sentrySpan . finish ( convertOtelTimeToSeconds ( otelSpan . endTime ) ) ;
170203
171- clearSpan ( otelSpanId ) ;
204+ this . _clearSpan ( otelSpanId ) ;
172205 }
173206
174207 /**
@@ -188,6 +221,17 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
188221 }
189222 return Promise . resolve ( ) ;
190223 }
224+
225+ /**
226+ * Clear all references for a given OTEL span.
227+ */
228+ private _clearSpan ( otelSpanId : string ) : void {
229+ if ( this . _strictSpanParentHandling ) {
230+ scheduleToClear ( otelSpanId ) ;
231+ } else {
232+ clearSpan ( otelSpanId ) ;
233+ }
234+ }
191235}
192236
193237function getTraceData ( otelSpan : OtelSpan , parentContext : Context ) : Partial < TransactionContext > {
@@ -263,3 +307,50 @@ function updateTransactionWithOtelData(transaction: Transaction, otelSpan: OtelS
263307function convertOtelTimeToSeconds ( [ seconds , nano ] : [ number , number ] ) : number {
264308 return seconds + nano / 1_000_000_000 ;
265309}
310+
311+ function scheduleToClear ( otelSpanId : string ) : void {
312+ const span = SENTRY_SPAN_PROCESSOR_MAP . get ( otelSpanId ) ;
313+
314+ if ( ! span ) {
315+ // hmm, something is fishy here, but abort...
316+ // But to be sure we still try to delete the SCHEDULE_TO_CLEAR, to avoid leaks
317+ SCHEDULE_TO_CLEAR . delete ( otelSpanId ) ;
318+ return ;
319+ }
320+
321+ const sentrySpanId = span . spanId ;
322+
323+ // This is the root, clear all that have been scheduled
324+ if ( spanIsRoot ( span ) || ! span . transaction ) {
325+ const toClear = SCHEDULE_TO_CLEAR . get ( sentrySpanId ) || [ ] ;
326+ toClear . push ( otelSpanId ) ;
327+
328+ toClear . forEach ( otelSpanIdToClear => clearSpan ( otelSpanIdToClear ) ) ;
329+ SCHEDULE_TO_CLEAR . delete ( sentrySpanId ) ;
330+ return ;
331+ }
332+
333+ // Clear when root span is cleared
334+ const root = span . transaction ;
335+ const rootSentrySpanId = root . spanId ;
336+
337+ const toClear = SCHEDULE_TO_CLEAR . get ( rootSentrySpanId ) ;
338+
339+ // If this does not exist, it means we prob. already cleaned it up before
340+ // So we ignore the parent and just clean this span up right now
341+ if ( ! toClear ) {
342+ clearSpan ( otelSpanId ) ;
343+ return ;
344+ }
345+
346+ toClear . push ( otelSpanId ) ;
347+ }
348+
349+ function spanIsRoot ( span : SentrySpan ) : span is Transaction {
350+ return span . transaction === span ;
351+ }
352+
353+ // make sure to remove references in maps, to ensure this can be GCed
354+ function clearSpan ( otelSpanId : string ) : void {
355+ SENTRY_SPAN_PROCESSOR_MAP . delete ( otelSpanId ) ;
356+ }
0 commit comments