@@ -11,12 +11,28 @@ import { isSentryRequestSpan } from './utils/isSentryRequest';
11
11
import { mapOtelStatus } from './utils/mapOtelStatus' ;
12
12
import { parseSpanDescription } from './utils/parseOtelSpanDescription' ;
13
13
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
+
14
31
export const SENTRY_SPAN_PROCESSOR_MAP : Map < string , SentrySpan > = new Map < string , SentrySpan > ( ) ;
15
32
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 [ ] > ( ) ;
20
36
21
37
/** Get a Sentry span for an otel span ID. */
22
38
export function getSentrySpan ( otelSpanId : string ) : SentrySpan | undefined {
@@ -28,7 +44,12 @@ export function getSentrySpan(otelSpanId: string): SentrySpan | undefined {
28
44
* the Sentry SDK.
29
45
*/
30
46
export 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
+
32
53
addTracingExtensions ( ) ;
33
54
34
55
addGlobalEventProcessor ( event => {
@@ -64,6 +85,13 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
64
85
// so we cannot use hub.getSpan(), as we cannot rely on this being on the current span
65
86
const sentryParentSpan = otelParentSpanId && SENTRY_SPAN_PROCESSOR_MAP . get ( otelParentSpanId ) ;
66
87
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
+
67
95
if ( sentryParentSpan ) {
68
96
const sentryChildSpan = sentryParentSpan . startChild ( {
69
97
description : otelSpan . name ,
@@ -105,7 +133,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
105
133
// leading to an infinite loop.
106
134
// In this case, we do not want to finish the span, in order to avoid sending it to Sentry
107
135
if ( isSentryRequestSpan ( otelSpan ) ) {
108
- clearSpan ( otelSpanId ) ;
136
+ this . _clearSpan ( otelSpanId ) ;
109
137
return ;
110
138
}
111
139
@@ -115,7 +143,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
115
143
client && client . emit && client ?. emit ( 'otelSpanEnd' , otelSpan , mutableOptions ) ;
116
144
117
145
if ( mutableOptions . drop ) {
118
- clearSpan ( otelSpanId ) ;
146
+ this . _clearSpan ( otelSpanId ) ;
119
147
return ;
120
148
}
121
149
@@ -168,7 +196,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
168
196
169
197
sentrySpan . finish ( convertOtelTimeToSeconds ( otelSpan . endTime ) ) ;
170
198
171
- clearSpan ( otelSpanId ) ;
199
+ this . _clearSpan ( otelSpanId ) ;
172
200
}
173
201
174
202
/**
@@ -188,6 +216,17 @@ export class SentrySpanProcessor implements OtelSpanProcessor {
188
216
}
189
217
return Promise . resolve ( ) ;
190
218
}
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
+ }
191
230
}
192
231
193
232
function getTraceData ( otelSpan : OtelSpan , parentContext : Context ) : Partial < TransactionContext > {
@@ -263,3 +302,52 @@ function updateTransactionWithOtelData(transaction: Transaction, otelSpan: OtelS
263
302
function convertOtelTimeToSeconds ( [ seconds , nano ] : [ number , number ] ) : number {
264
303
return seconds + nano / 1_000_000_000 ;
265
304
}
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