Skip to content

Commit a2351ab

Browse files
committed
ref: SpanRecorder
1 parent 26bee09 commit a2351ab

File tree

5 files changed

+146
-40
lines changed

5 files changed

+146
-40
lines changed

packages/apm/src/hubextensions.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,15 @@ function startSpan(spanOrSpanContext?: Span | SpanContext, makeRoot: boolean = f
4444
const client = hub.getClient();
4545
let span;
4646

47+
// This flag determines if we already added the span as a child to the span that currently lives on the scope
48+
// If we do not have this, we will add it later on twice to the span recorder and therefore have too many spans
49+
let addedAsChild = false;
50+
4751
if (!isSpanInstance(spanOrSpanContext) && !makeRoot && scope) {
4852
const parentSpan = scope.getSpan() as Span;
4953
if (parentSpan) {
5054
span = parentSpan.child(spanOrSpanContext);
55+
addedAsChild = true;
5156
}
5257
}
5358

@@ -63,7 +68,7 @@ function startSpan(spanOrSpanContext?: Span | SpanContext, makeRoot: boolean = f
6368

6469
// We only want to create a span list if we sampled the transaction
6570
// in case we will discard the span anyway because sampled == false, we safe memory and do not store child spans
66-
if (span.sampled) {
71+
if (span.sampled && !addedAsChild) {
6772
const experimentsOptions = (client && client.getOptions()._experiments) || {};
6873
span.initSpanRecorder(experimentsOptions.maxSpans as number);
6974
}

packages/apm/src/integrations/tracing.ts

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { Event, EventProcessor, Hub, Integration, Span, SpanContext, SpanStatus } from '@sentry/types';
1+
import { Hub, Scope } from '@sentry/hub';
2+
import { Event, EventProcessor, Integration, Span, SpanContext, SpanStatus } from '@sentry/types';
23
import {
34
addInstrumentationHandler,
45
getGlobalObject,
@@ -272,6 +273,19 @@ export class Tracing implements Integration {
272273
* Unsets the current active transaction + activities
273274
*/
274275
private static _resetActiveTransaction(): void {
276+
// We want to clean up after ourselves
277+
// If there is still the active transaction on the scope we remove it
278+
const _getCurrentHub = Tracing._getCurrentHub;
279+
if (_getCurrentHub) {
280+
const hub = _getCurrentHub();
281+
const scope = hub.getScope();
282+
if (scope) {
283+
if (scope.getSpan() === Tracing._activeTransaction) {
284+
scope.setSpan(undefined);
285+
}
286+
}
287+
}
288+
// ------------------------------------------------------------------
275289
Tracing._activeTransaction = undefined;
276290
Tracing._activities = {};
277291
}
@@ -365,6 +379,13 @@ export class Tracing implements Integration {
365379
true,
366380
);
367381

382+
// We set the transaction on the scope so if there are any other spans started outside of this integration
383+
// we also add them to this transaction.
384+
// Once the idle transaction is finished, we make sure to remove it again.
385+
hub.configureScope((scope: Scope) => {
386+
scope.setSpan(Tracing._activeTransaction);
387+
});
388+
368389
// The reason we do this here is because of cached responses
369390
// If we start and transaction without an activity it would never finish since there is no activity
370391
const id = Tracing.pushActivity('idleTransactionStarted');
@@ -405,13 +426,6 @@ export class Tracing implements Integration {
405426

406427
const timeOrigin = Tracing._msToSec(performance.timeOrigin);
407428

408-
// tslint:disable-next-line: completed-docs
409-
function addSpan(span: SpanClass): void {
410-
if (transactionSpan.spanRecorder) {
411-
transactionSpan.spanRecorder.finishSpan(span);
412-
}
413-
}
414-
415429
// tslint:disable-next-line: completed-docs
416430
function addPerformanceNavigationTiming(parent: SpanClass, entry: { [key: string]: number }, event: string): void {
417431
const span = parent.child({
@@ -420,7 +434,6 @@ export class Tracing implements Integration {
420434
});
421435
span.startTimestamp = timeOrigin + Tracing._msToSec(entry[`${event}Start`]);
422436
span.timestamp = timeOrigin + Tracing._msToSec(entry[`${event}End`]);
423-
addSpan(span);
424437
}
425438

426439
// tslint:disable-next-line: completed-docs
@@ -431,14 +444,13 @@ export class Tracing implements Integration {
431444
});
432445
request.startTimestamp = timeOrigin + Tracing._msToSec(entry.requestStart);
433446
request.timestamp = timeOrigin + Tracing._msToSec(entry.responseEnd);
434-
addSpan(request);
447+
435448
const response = parent.child({
436449
description: 'response',
437450
op: 'browser',
438451
});
439452
response.startTimestamp = timeOrigin + Tracing._msToSec(entry.responseStart);
440453
response.timestamp = timeOrigin + Tracing._msToSec(entry.responseEnd);
441-
addSpan(response);
442454
}
443455

444456
let entryScriptSrc: string | undefined;
@@ -492,14 +504,13 @@ export class Tracing implements Integration {
492504
if (tracingInitMarkStartTime === undefined && entry.name === 'sentry-tracing-init') {
493505
tracingInitMarkStartTime = mark.startTimestamp;
494506
}
495-
addSpan(mark);
496507
break;
497508
case 'resource':
498509
const resourceName = entry.name.replace(window.location.origin, '');
499510
if (entry.initiatorType === 'xmlhttprequest' || entry.initiatorType === 'fetch') {
500511
// We need to update existing spans with new timing info
501512
if (transactionSpan.spanRecorder) {
502-
transactionSpan.spanRecorder.finishedSpans.map((finishedSpan: SpanClass) => {
513+
transactionSpan.spanRecorder.spans.map((finishedSpan: SpanClass) => {
503514
if (finishedSpan.description && finishedSpan.description.indexOf(resourceName) !== -1) {
504515
finishedSpan.startTimestamp = timeOrigin + startTime;
505516
finishedSpan.timestamp = finishedSpan.startTimestamp + duration;
@@ -517,7 +528,6 @@ export class Tracing implements Integration {
517528
if (entryScriptStartEndTime === undefined && (entryScriptSrc || '').includes(resourceName)) {
518529
entryScriptStartEndTime = resource.timestamp;
519530
}
520-
addSpan(resource);
521531
}
522532
break;
523533
default:
@@ -532,7 +542,6 @@ export class Tracing implements Integration {
532542
});
533543
evaluation.startTimestamp = entryScriptStartEndTime;
534544
evaluation.timestamp = tracingInitMarkStartTime;
535-
addSpan(evaluation);
536545
}
537546

538547
Tracing._performanceCursor = Math.max(performance.getEntries().length - 1, 0);

packages/apm/src/span.ts

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ export const TRACEPARENT_REGEXP = new RegExp(
1818
*/
1919
class SpanRecorder {
2020
private readonly _maxlen: number;
21-
private _openSpanCount: number = 0;
22-
public finishedSpans: Span[] = [];
21+
public spans: Span[] = [];
2322

2423
public constructor(maxlen: number = 1000) {
2524
this._maxlen = maxlen;
@@ -31,19 +30,12 @@ class SpanRecorder {
3130
* trace tree (i.e.the first n spans with the smallest
3231
* start_timestamp).
3332
*/
34-
public startSpan(span: Span): void {
35-
if (this._openSpanCount > this._maxlen) {
33+
public add(span: Span): void {
34+
if (this.spans.length > this._maxlen) {
3635
span.spanRecorder = undefined;
36+
} else {
37+
this.spans.push(span);
3738
}
38-
this._openSpanCount++;
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);
4739
}
4840
}
4941

@@ -176,7 +168,7 @@ export class Span implements SpanInterface, SpanContext {
176168
if (!this.spanRecorder) {
177169
this.spanRecorder = new SpanRecorder(maxlen);
178170
}
179-
this.spanRecorder.startSpan(this);
171+
this.spanRecorder.add(this);
180172
}
181173

182174
/**
@@ -194,7 +186,7 @@ export class Span implements SpanInterface, SpanContext {
194186

195187
span.spanRecorder = this.spanRecorder;
196188
if (span.spanRecorder) {
197-
span.spanRecorder.startSpan(span);
189+
span.spanRecorder.add(span);
198190
}
199191

200192
return span;
@@ -297,15 +289,13 @@ export class Span implements SpanInterface, SpanContext {
297289
return undefined;
298290
}
299291

300-
this.spanRecorder.finishSpan(this);
301-
302292
if (this.sampled !== true) {
303293
// At this point if `sampled !== true` we want to discard the transaction.
304294
logger.warn('Discarding transaction Span because it was span.sampled !== true');
305295
return undefined;
306296
}
307297

308-
const finishedSpans = this.spanRecorder ? this.spanRecorder.finishedSpans.filter(s => s !== this) : [];
298+
const finishedSpans = this.spanRecorder ? this.spanRecorder.spans.filter(s => s !== this && s.timestamp) : [];
309299

310300
if (trimEnd && finishedSpans.length > 0) {
311301
this.timestamp = finishedSpans.reduce((prev: Span, current: Span) => {
@@ -348,7 +338,16 @@ export class Span implements SpanInterface, SpanContext {
348338
/**
349339
* @inheritDoc
350340
*/
351-
public getTraceContext(): object {
341+
public getTraceContext(): {
342+
data?: { [key: string]: any };
343+
description?: string;
344+
op?: string;
345+
parent_span_id?: string;
346+
span_id: string;
347+
status?: string;
348+
tags?: { [key: string]: string };
349+
trace_id: string;
350+
} {
352351
return dropUndefinedKeys({
353352
data: Object.keys(this.data).length > 0 ? this.data : undefined,
354353
description: this.description,
@@ -364,7 +363,19 @@ export class Span implements SpanInterface, SpanContext {
364363
/**
365364
* @inheritDoc
366365
*/
367-
public toJSON(): object {
366+
public toJSON(): {
367+
data?: { [key: string]: any };
368+
description?: string;
369+
op?: string;
370+
parent_span_id?: string;
371+
sampled?: boolean;
372+
span_id: string;
373+
start_timestamp: number;
374+
tags?: { [key: string]: string };
375+
timestamp?: number;
376+
trace_id: string;
377+
transaction?: string;
378+
} {
368379
return dropUndefinedKeys({
369380
data: Object.keys(this.data).length > 0 ? this.data : undefined,
370381
description: this.description,

packages/apm/test/span.test.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,9 @@ describe('Span', () => {
238238
spanTwo.finish();
239239

240240
expect(spy).not.toHaveBeenCalled();
241-
expect((spanOne as any).spanRecorder.finishedSpans).toHaveLength(2);
241+
expect((spanOne as any).spanRecorder.spans).toHaveLength(3);
242+
// We only want two finished spans
243+
expect((spanOne as any).spanRecorder.spans.filter((s: Span) => !!s.timestamp)).toHaveLength(2);
242244
});
243245

244246
test("finish a span with another one on the scope shouldn't override contexts.trace", () => {
@@ -294,6 +296,62 @@ describe('Span', () => {
294296
expect((span as any).spanRecorder).toBeUndefined();
295297
expect(spy).not.toHaveBeenCalled();
296298
});
299+
300+
test('mixing hub.startSpan + span.child + maxSpans', () => {
301+
const _hub = new Hub(
302+
new BrowserClient({
303+
_experiments: { maxSpans: 2 },
304+
tracesSampleRate: 1,
305+
}),
306+
);
307+
const spy = jest.spyOn(_hub as any, 'captureEvent') as any;
308+
309+
const spanOne = _hub.startSpan();
310+
const childSpanOne = spanOne.child({ op: '1' });
311+
childSpanOne.finish();
312+
313+
_hub.configureScope(scope => {
314+
scope.setSpan(spanOne);
315+
});
316+
317+
const spanTwo = _hub.startSpan({ op: '2' });
318+
spanTwo.finish();
319+
320+
const spanThree = _hub.startSpan({ op: '3' });
321+
spanThree.finish();
322+
323+
spanOne.finish();
324+
325+
expect(spy).toHaveBeenCalled();
326+
expect(spy.mock.calls[0][0].spans).toHaveLength(2);
327+
});
328+
329+
test('tree structure of spans should be correct when mixing it with span on scope', () => {
330+
const spy = jest.spyOn(hub as any, 'captureEvent') as any;
331+
332+
const spanOne = hub.startSpan();
333+
const childSpanOne = spanOne.child();
334+
335+
const childSpanTwo = childSpanOne.child();
336+
childSpanTwo.finish();
337+
338+
childSpanOne.finish();
339+
340+
hub.configureScope(scope => {
341+
scope.setSpan(spanOne);
342+
});
343+
344+
const spanTwo = hub.startSpan();
345+
spanTwo.finish();
346+
spanOne.finish();
347+
348+
expect(spy).toHaveBeenCalled();
349+
expect(spy.mock.calls[0][0].spans).toHaveLength(3);
350+
expect(spy.mock.calls[0][0].contexts.trace).toEqual(spanOne.getTraceContext());
351+
expect(childSpanOne.toJSON().parent_span_id).toEqual(spanOne.toJSON().span_id);
352+
expect(childSpanTwo.toJSON().parent_span_id).toEqual(childSpanOne.toJSON().span_id);
353+
expect(spanTwo.toJSON().parent_span_id).toEqual(spanOne.toJSON().span_id);
354+
});
297355
});
298356
});
299357

packages/types/src/span.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,30 @@ export interface Span {
55
/** Return a traceparent compatible header string */
66
toTraceparent(): string;
77
/** Convert the object to JSON for w. spans array info only */
8-
getTraceContext(): object;
8+
getTraceContext(): {
9+
data?: { [key: string]: any };
10+
description?: string;
11+
op?: string;
12+
parent_span_id?: string;
13+
span_id: string;
14+
status?: string;
15+
tags?: { [key: string]: string };
16+
trace_id: string;
17+
};
918
/** Convert the object to JSON */
10-
toJSON(): object;
19+
toJSON(): {
20+
data?: { [key: string]: any };
21+
description?: string;
22+
op?: string;
23+
parent_span_id?: string;
24+
sampled?: boolean;
25+
span_id: string;
26+
start_timestamp: number;
27+
tags?: { [key: string]: string };
28+
timestamp?: number;
29+
trace_id: string;
30+
transaction?: string;
31+
};
1132

1233
/**
1334
* Sets the tag attribute on the current span
@@ -39,7 +60,9 @@ export interface Span {
3960
* Creates a new `Span` while setting the current `Span.id` as `parentSpanId`.
4061
* Also the `sampled` decision will be inherited.
4162
*/
42-
child(spanContext?: Pick<SpanContext, Exclude<keyof SpanContext, 'spanId' | 'sampled' | 'traceId' | 'parentSpanId'>>): Span;
63+
child(
64+
spanContext?: Pick<SpanContext, Exclude<keyof SpanContext, 'spanId' | 'sampled' | 'traceId' | 'parentSpanId'>>,
65+
): Span;
4366

4467
/**
4568
* Determines whether span was successful (HTTP200)

0 commit comments

Comments
 (0)