1
+ // tslint:disable:max-classes-per-file
2
+
1
3
import { Span as SpanInterface , SpanContext } from '@sentry/types' ;
2
- import { timestampWithMs , uuid4 } from '@sentry/utils' ;
4
+ import { logger , timestampWithMs , uuid4 } from '@sentry/utils' ;
3
5
4
6
import { getCurrentHub , Hub } from './hub' ;
5
7
6
- export const TRACEPARENT_REGEXP = / ^ [ \t ] * ( [ 0 - 9 a - f ] { 32 } ) ? - ? ( [ 0 - 9 a - f ] { 16 } ) ? - ? ( [ 0 1 ] ) ? [ \t ] * $ / ;
8
+ export const TRACEPARENT_REGEXP = new RegExp (
9
+ '^[ \\t]*' + // whitespace
10
+ '([0-9a-f]{32})?' + // trace_id
11
+ '-?([0-9a-f]{16})?' + // span_id
12
+ '-?([01])?' + // sampled
13
+ '[ \\t]*$' , // whitespace
14
+ ) ;
15
+
16
+ /**
17
+ * Keeps track of finished spans for a given transaction
18
+ */
19
+ class SpanRecorder {
20
+ private readonly _maxlen : number ;
21
+ private _openSpanCount : number = 0 ;
22
+ public finishedSpans : Span [ ] = [ ] ;
23
+
24
+ public constructor ( maxlen : number ) {
25
+ this . _maxlen = maxlen ;
26
+ }
27
+
28
+ /**
29
+ * This is just so that we don't run out of memory while recording a lot
30
+ * of spans. At some point we just stop and flush out the start of the
31
+ * trace tree (i.e.the first n spans with the smallest
32
+ * start_timestamp).
33
+ */
34
+ public startSpan ( span : Span ) : void {
35
+ this . _openSpanCount += 1 ;
36
+ if ( this . _openSpanCount > this . _maxlen ) {
37
+ span . spanRecorder = undefined ;
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Appends a span to finished spans table
43
+ * @param span Span to be added
44
+ */
45
+ public finishSpan ( span : Span ) : void {
46
+ this . finishedSpans . push ( span ) ;
47
+ }
48
+ }
7
49
8
50
/**
9
51
* Span contains all data about a span
@@ -32,7 +74,7 @@ export class Span implements SpanInterface, SpanContext {
32
74
/**
33
75
* @inheritDoc
34
76
*/
35
- public readonly sampled ?: boolean ;
77
+ public sampled ?: boolean ;
36
78
37
79
/**
38
80
* Timestamp when the span was created.
@@ -72,7 +114,7 @@ export class Span implements SpanInterface, SpanContext {
72
114
/**
73
115
* List of spans that were finalized
74
116
*/
75
- public finishedSpans : Span [ ] = [ ] ;
117
+ public spanRecorder ?: SpanRecorder ;
76
118
77
119
public constructor ( spanContext ?: SpanContext , hub ?: Hub ) {
78
120
if ( hub instanceof Hub ) {
@@ -112,6 +154,17 @@ export class Span implements SpanInterface, SpanContext {
112
154
}
113
155
}
114
156
157
+ /**
158
+ * Attaches SpanRecorder to the span itself
159
+ * @param maxlen maximum number of spans that can be recorded
160
+ */
161
+ public initFinishedSpans ( maxlen : number ) : void {
162
+ if ( ! this . spanRecorder ) {
163
+ this . spanRecorder = new SpanRecorder ( maxlen ) ;
164
+ }
165
+ this . spanRecorder . startSpan ( this ) ;
166
+ }
167
+
115
168
/**
116
169
* Creates a new `Span` while setting the current `Span.id` as `parentSpanId`.
117
170
* Also the `sampled` decision will be inherited.
@@ -124,7 +177,7 @@ export class Span implements SpanInterface, SpanContext {
124
177
traceId : this . _traceId ,
125
178
} ) ;
126
179
127
- span . finishedSpans = this . finishedSpans ;
180
+ span . spanRecorder = this . spanRecorder ;
128
181
129
182
return span ;
130
183
}
@@ -208,26 +261,41 @@ export class Span implements SpanInterface, SpanContext {
208
261
* Sets the finish timestamp on the current span
209
262
*/
210
263
public finish ( ) : string | undefined {
211
- // Don't allow for finishing more than once
212
- if ( typeof this . timestamp === 'number' ) {
264
+ // This transaction is already finished, so we should not flush it again.
265
+ if ( this . timestamp !== undefined ) {
213
266
return undefined ;
214
267
}
215
268
216
269
this . timestamp = timestampWithMs ( ) ;
217
- this . finishedSpans . push ( this ) ;
218
270
219
- // Don't send non-transaction spans
220
- if ( typeof this . transaction !== 'string' ) {
271
+ if ( this . spanRecorder === undefined ) {
272
+ return undefined ;
273
+ }
274
+
275
+ this . spanRecorder . finishSpan ( this ) ;
276
+
277
+ if ( this . transaction === undefined ) {
278
+ // If this has no transaction set we assume there's a parent
279
+ // transaction for this span that would be flushed out eventually.
280
+ return undefined ;
281
+ }
282
+
283
+ if ( this . sampled === undefined ) {
284
+ // At this point a `sampled === undefined` should have already been
285
+ // resolved to a concrete decision. If `sampled` is `undefined`, it's
286
+ // likely that somebody used `Sentry.startSpan(...)` on a
287
+ // non-transaction span and later decided to make it a transaction.
288
+ logger . warn ( 'Discarding transaction Span without sampling decision' ) ;
221
289
return undefined ;
222
290
}
223
291
224
- // TODO: if sampled do what?
225
- const finishedSpans = this . finishedSpans . filter ( s => s !== this ) ;
226
- this . finishedSpans = [ ] ;
292
+ const finishedSpans = ! this . spanRecorder ? [ ] : this . spanRecorder . finishedSpans . filter ( s => s !== this ) ;
227
293
228
294
return this . _hub . captureEvent ( {
295
+ // TODO: Is this necessary? We already do store contextx in in applyToEvent,
296
+ // so maybe we can move `getTraceContext` call there as well?
229
297
contexts : { trace : this . getTraceContext ( ) } ,
230
- spans : finishedSpans . length > 0 ? finishedSpans : undefined ,
298
+ spans : finishedSpans ,
231
299
start_timestamp : this . startTimestamp ,
232
300
timestamp : this . timestamp ,
233
301
transaction : this . transaction ,
0 commit comments