@@ -71,6 +71,9 @@ export class SentrySpanExporter {
71
71
private _finishedSpanBucketSize : number ;
72
72
private _spansToBucketEntry : WeakMap < ReadableSpan , FinishedSpanBucket > ;
73
73
private _lastCleanupTimestampInS : number ;
74
+ // Essentially a a set of span ids that are already sent. The values are expiration
75
+ // times in this cache so we don't hold onto them indefinitely.
76
+ private _sentSpans : Map < string , number > ;
74
77
75
78
public constructor ( options ?: {
76
79
/** Lower bound of time in seconds until spans that are buffered but have not been sent as part of a transaction get cleared from memory. */
@@ -80,6 +83,48 @@ export class SentrySpanExporter {
80
83
this . _finishedSpanBuckets = new Array ( this . _finishedSpanBucketSize ) . fill ( undefined ) ;
81
84
this . _lastCleanupTimestampInS = Math . floor ( Date . now ( ) / 1000 ) ;
82
85
this . _spansToBucketEntry = new WeakMap ( ) ;
86
+ this . _sentSpans = new Map < string , number > ( ) ;
87
+ }
88
+
89
+ /**
90
+ * Check if a span with the given ID has already been sent using the `_sentSpans` as a cache.
91
+ * Purges "expired" spans from the cache upon checking.
92
+ * @param spanId The span id to check.
93
+ * @returns Whether the span is already sent in the past X seconds.
94
+ */
95
+ public isSpanAlreadySent ( spanId : string ) : boolean {
96
+ const expirationTime = this . _sentSpans . get ( spanId ) ;
97
+ if ( expirationTime ) {
98
+ if ( Date . now ( ) >= expirationTime ) {
99
+ this . _sentSpans . delete ( spanId ) ; // Remove expired span
100
+ } else {
101
+ return true ;
102
+ }
103
+ }
104
+ return false ;
105
+ }
106
+
107
+ /** Remove "expired" span id entries from the _sentSpans cache. */
108
+ public flushSentSpanCache ( ) : void {
109
+ const currentTimestamp = Date . now ( ) ;
110
+ // Note, it is safe to delete items from the map as we go: https://stackoverflow.com/a/35943995/90297
111
+ for ( const [ spanId , expirationTime ] of this . _sentSpans . entries ( ) ) {
112
+ if ( expirationTime <= currentTimestamp ) {
113
+ this . _sentSpans . delete ( spanId ) ;
114
+ }
115
+ }
116
+ }
117
+
118
+ /** Check if a node is a completed root node or a node whose parent has already been sent */
119
+ public nodeIsCompletedRootNode ( node : SpanNode ) : node is SpanNodeCompleted {
120
+ return ! ! node . span && ( ! node . parentNode || this . isSpanAlreadySent ( node . parentNode . id ) ) ;
121
+ }
122
+
123
+ /** Get all completed root nodes from a list of nodes */
124
+ public getCompletedRootNodes ( nodes : SpanNode [ ] ) : SpanNodeCompleted [ ] {
125
+ // TODO: We should be able to remove the explicit `node is SpanNodeCompleted` type guard
126
+ // once we stop supporting TS < 5.5
127
+ return nodes . filter ( ( node ) : node is SpanNodeCompleted => this . nodeIsCompletedRootNode ( node ) ) ;
83
128
}
84
129
85
130
/** Export a single span. */
@@ -113,7 +158,8 @@ export class SentrySpanExporter {
113
158
this . _spansToBucketEntry . set ( span , currentBucket ) ;
114
159
115
160
// If the span doesn't have a local parent ID (it's a root span), we're gonna flush all the ended spans
116
- if ( ! getLocalParentId ( span ) ) {
161
+ const localParentId = getLocalParentId ( span ) ;
162
+ if ( ! localParentId || this . isSpanAlreadySent ( localParentId ) ) {
117
163
this . _clearTimeout ( ) ;
118
164
119
165
// If we got a parent span, we try to send the span tree
@@ -128,30 +174,29 @@ export class SentrySpanExporter {
128
174
public flush ( ) : void {
129
175
this . _clearTimeout ( ) ;
130
176
131
- const finishedSpans : ReadableSpan [ ] = [ ] ;
132
- this . _finishedSpanBuckets . forEach ( bucket => {
133
- if ( bucket ) {
134
- finishedSpans . push ( ...bucket . spans ) ;
135
- }
136
- } ) ;
177
+ const finishedSpans : ReadableSpan [ ] = this . _finishedSpanBuckets . flatMap ( bucket =>
178
+ bucket ? Array . from ( bucket . spans ) : [ ] ,
179
+ ) ;
137
180
138
- const sentSpans = maybeSend ( finishedSpans ) ;
181
+ this . flushSentSpanCache ( ) ;
182
+ const sentSpans = this . _maybeSend ( finishedSpans ) ;
183
+ for ( const span of finishedSpans ) {
184
+ this . _sentSpans . set ( span . spanContext ( ) . spanId , Date . now ( ) + DEFAULT_TIMEOUT * 1000 ) ;
185
+ }
139
186
140
187
const sentSpanCount = sentSpans . size ;
141
-
142
188
const remainingOpenSpanCount = finishedSpans . length - sentSpanCount ;
143
-
144
189
DEBUG_BUILD &&
145
190
logger . log (
146
191
`SpanExporter exported ${ sentSpanCount } spans, ${ remainingOpenSpanCount } spans are waiting for their parent spans to finish` ,
147
192
) ;
148
193
149
- sentSpans . forEach ( span => {
194
+ for ( const span of sentSpans ) {
150
195
const bucketEntry = this . _spansToBucketEntry . get ( span ) ;
151
196
if ( bucketEntry ) {
152
197
bucketEntry . spans . delete ( span ) ;
153
198
}
154
- } ) ;
199
+ }
155
200
}
156
201
157
202
/** Clear the exporter. */
@@ -167,59 +212,51 @@ export class SentrySpanExporter {
167
212
this . _flushTimeout = undefined ;
168
213
}
169
214
}
170
- }
171
-
172
- /**
173
- * Send the given spans, but only if they are part of a finished transaction.
174
- *
175
- * Returns the sent spans.
176
- * Spans remain unsent when their parent span is not yet finished.
177
- * This will happen regularly, as child spans are generally finished before their parents.
178
- * But it _could_ also happen because, for whatever reason, a parent span was lost.
179
- * In this case, we'll eventually need to clean this up.
180
- */
181
- function maybeSend ( spans : ReadableSpan [ ] ) : Set < ReadableSpan > {
182
- const grouped = groupSpansWithParents ( spans ) ;
183
- const sentSpans = new Set < ReadableSpan > ( ) ;
184
215
185
- const rootNodes = getCompletedRootNodes ( grouped ) ;
216
+ /**
217
+ * Send the given spans, but only if they are part of a finished transaction.
218
+ *
219
+ * Returns the sent spans.
220
+ * Spans remain unsent when their parent span is not yet finished.
221
+ * This will happen regularly, as child spans are generally finished before their parents.
222
+ * But it _could_ also happen because, for whatever reason, a parent span was lost.
223
+ * In this case, we'll eventually need to clean this up.
224
+ */
225
+ private _maybeSend ( spans : ReadableSpan [ ] ) : Set < ReadableSpan > {
226
+ const grouped = groupSpansWithParents ( spans ) ;
227
+ const sentSpans = new Set < ReadableSpan > ( ) ;
186
228
187
- rootNodes . forEach ( root => {
188
- const span = root . span ;
189
- sentSpans . add ( span ) ;
190
- const transactionEvent = createTransactionForOtelSpan ( span ) ;
229
+ const rootNodes = this . getCompletedRootNodes ( grouped ) ;
191
230
192
- // We'll recursively add all the child spans to this array
193
- const spans = transactionEvent . spans || [ ] ;
231
+ for ( const root of rootNodes ) {
232
+ const span = root . span ;
233
+ sentSpans . add ( span ) ;
234
+ const transactionEvent = createTransactionForOtelSpan ( span ) ;
194
235
195
- root . children . forEach ( child => {
196
- createAndFinishSpanForOtelSpan ( child , spans , sentSpans ) ;
197
- } ) ;
236
+ // We'll recursively add all the child spans to this array
237
+ const spans = transactionEvent . spans || [ ] ;
198
238
199
- // spans.sort() mutates the array, but we do not use this anymore after this point
200
- // so we can safely mutate it here
201
- transactionEvent . spans =
202
- spans . length > MAX_SPAN_COUNT
203
- ? spans . sort ( ( a , b ) => a . start_timestamp - b . start_timestamp ) . slice ( 0 , MAX_SPAN_COUNT )
204
- : spans ;
239
+ for ( const child of root . children ) {
240
+ createAndFinishSpanForOtelSpan ( child , spans , sentSpans ) ;
241
+ }
205
242
206
- const measurements = timedEventsToMeasurements ( span . events ) ;
207
- if ( measurements ) {
208
- transactionEvent . measurements = measurements ;
209
- }
243
+ // spans.sort() mutates the array, but we do not use this anymore after this point
244
+ // so we can safely mutate it here
245
+ transactionEvent . spans =
246
+ spans . length > MAX_SPAN_COUNT
247
+ ? spans . sort ( ( a , b ) => a . start_timestamp - b . start_timestamp ) . slice ( 0 , MAX_SPAN_COUNT )
248
+ : spans ;
210
249
211
- captureEvent ( transactionEvent ) ;
212
- } ) ;
213
-
214
- return sentSpans ;
215
- }
250
+ const measurements = timedEventsToMeasurements ( span . events ) ;
251
+ if ( measurements ) {
252
+ transactionEvent . measurements = measurements ;
253
+ }
216
254
217
- function nodeIsCompletedRootNode ( node : SpanNode ) : node is SpanNodeCompleted {
218
- return ! ! node . span && ! node . parentNode ;
219
- }
255
+ captureEvent ( transactionEvent ) ;
256
+ }
220
257
221
- function getCompletedRootNodes ( nodes : SpanNode [ ] ) : SpanNodeCompleted [ ] {
222
- return nodes . filter ( nodeIsCompletedRootNode ) ;
258
+ return sentSpans ;
259
+ }
223
260
}
224
261
225
262
function parseSpan ( span : ReadableSpan ) : { op ?: string ; origin ?: SpanOrigin ; source ?: TransactionSource } {
0 commit comments