Skip to content

Commit 3601e3d

Browse files
committed
Bring APM implementation up to date with python
1 parent 11e3f8b commit 3601e3d

File tree

4 files changed

+131
-33
lines changed

4 files changed

+131
-33
lines changed

packages/hub/src/hub.ts

+37-11
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@ declare module 'domain' {
3535
}
3636
}
3737

38+
/**
39+
* Checks whether given value is instance of Span
40+
* @param span value to check
41+
*/
42+
function isSpanInstance(span: unknown): span is Span {
43+
return span instanceof Span;
44+
}
45+
3846
/**
3947
* API compatibility version of this hub.
4048
*
@@ -382,9 +390,9 @@ export class Hub implements HubInterface {
382390
* @inheritDoc
383391
*/
384392
public traceHeaders(): { [key: string]: string } {
385-
const top = this.getStackTop();
386-
if (top.scope && top.client) {
387-
const span = top.scope.getSpan();
393+
const scope = this.getScope();
394+
if (scope) {
395+
const span = scope.getSpan();
388396
if (span) {
389397
return {
390398
'sentry-trace': span.toTraceparent(),
@@ -397,18 +405,36 @@ export class Hub implements HubInterface {
397405
/**
398406
* @inheritDoc
399407
*/
400-
public startSpan(spanContext?: SpanContext): Span {
401-
const top = this.getStackTop();
408+
public startSpan(spanOrSpanContext?: Span | SpanContext): Span {
409+
const scope = this.getScope();
410+
const client = this.getClient();
411+
let span;
412+
413+
if (!isSpanInstance(spanOrSpanContext)) {
414+
if (scope) {
415+
const parentSpan = scope.getSpan();
416+
if (parentSpan) {
417+
span = parentSpan.child(spanOrSpanContext);
418+
}
419+
}
420+
}
402421

403-
if (top.scope) {
404-
const span = top.scope.getSpan();
422+
if (!isSpanInstance(span)) {
423+
span = new Span(spanOrSpanContext, this);
424+
}
405425

406-
if (span) {
407-
return span.child(spanContext);
408-
}
426+
if (span.sampled === undefined && span.transaction !== undefined) {
427+
const sampleRate = (client && client.getOptions().tracesSampleRate) || 0;
428+
span.sampled = Math.random() < sampleRate;
429+
}
430+
431+
if (span.sampled) {
432+
const experimentsOptions = (client && client.getOptions()._experiments) || {};
433+
const maxSpans = experimentsOptions.maxSpans || 1000;
434+
span.initFinishedSpans(maxSpans);
409435
}
410436

411-
return new Span(spanContext);
437+
return span;
412438
}
413439
}
414440

packages/hub/src/span.ts

+82-14
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,51 @@
1+
// tslint:disable:max-classes-per-file
2+
13
import { Span as SpanInterface, SpanContext } from '@sentry/types';
2-
import { timestampWithMs, uuid4 } from '@sentry/utils';
4+
import { logger, timestampWithMs, uuid4 } from '@sentry/utils';
35

46
import { getCurrentHub, Hub } from './hub';
57

6-
export const TRACEPARENT_REGEXP = /^[ \t]*([0-9a-f]{32})?-?([0-9a-f]{16})?-?([01])?[ \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+
}
749

850
/**
951
* Span contains all data about a span
@@ -32,7 +74,7 @@ export class Span implements SpanInterface, SpanContext {
3274
/**
3375
* @inheritDoc
3476
*/
35-
public readonly sampled?: boolean;
77+
public sampled?: boolean;
3678

3779
/**
3880
* Timestamp when the span was created.
@@ -72,7 +114,7 @@ export class Span implements SpanInterface, SpanContext {
72114
/**
73115
* List of spans that were finalized
74116
*/
75-
public finishedSpans: Span[] = [];
117+
public spanRecorder?: SpanRecorder;
76118

77119
public constructor(spanContext?: SpanContext, hub?: Hub) {
78120
if (hub instanceof Hub) {
@@ -112,6 +154,17 @@ export class Span implements SpanInterface, SpanContext {
112154
}
113155
}
114156

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+
115168
/**
116169
* Creates a new `Span` while setting the current `Span.id` as `parentSpanId`.
117170
* Also the `sampled` decision will be inherited.
@@ -124,7 +177,7 @@ export class Span implements SpanInterface, SpanContext {
124177
traceId: this._traceId,
125178
});
126179

127-
span.finishedSpans = this.finishedSpans;
180+
span.spanRecorder = this.spanRecorder;
128181

129182
return span;
130183
}
@@ -208,26 +261,41 @@ export class Span implements SpanInterface, SpanContext {
208261
* Sets the finish timestamp on the current span
209262
*/
210263
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) {
213266
return undefined;
214267
}
215268

216269
this.timestamp = timestampWithMs();
217-
this.finishedSpans.push(this);
218270

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');
221289
return undefined;
222290
}
223291

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);
227293

228294
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?
229297
contexts: { trace: this.getTraceContext() },
230-
spans: finishedSpans.length > 0 ? finishedSpans : undefined,
298+
spans: finishedSpans,
231299
start_timestamp: this.startTimestamp,
232300
timestamp: this.timestamp,
233301
transaction: this.transaction,

packages/types/src/hub.ts

+5-8
Original file line numberDiff line numberDiff line change
@@ -173,14 +173,11 @@ export interface Hub {
173173
traceHeaders(): { [key: string]: string };
174174

175175
/**
176-
* This functions starts a span. If just a `SpanContext` is passed and there is already a Span
177-
* on the Scope, the created Span will have a reference to the one on the Scope.
178-
* If a Span is on the current Scope it is considered a `transaction`.
179-
* When using the second parameter it will set the created Span on the Scope (replacing whats there).
180-
* This can be used as a shortcut to not set it manually on the Scope.
176+
* This functions starts a span. If argument passed is of type `Span`, it'll run sampling on it if configured
177+
* and attach a `SpanRecorder`. If it's of type `SpanContext` and there is already a `Span` on the Scope,
178+
* the created Span will have a reference to it and become it's child. Otherwise it'll crete a new `Span`.
181179
*
182-
* @param spanContext Properties with which the span should be created
183-
* @param bindOnScope Determines if the started span will be set on the Scope
180+
* @param span Already constructed span which should be started or properties with which the span should be created
184181
*/
185-
startSpan(spanContext?: SpanContext, bindOnScope?: boolean): Span;
182+
startSpan(span?: Span | SpanContext): Span;
186183
}

packages/types/src/options.ts

+7
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ export interface Options {
7878
/** A global sample rate to apply to all events (0 - 1). */
7979
sampleRate?: number;
8080

81+
/** A global sample rate to apply to all transactions (0 - 1). */
82+
tracesSampleRate?: number;
83+
8184
/** Attaches stacktraces to pure capture message / log integrations */
8285
attachStacktrace?: boolean;
8386

@@ -110,4 +113,8 @@ export interface Options {
110113
* @returns The breadcrumb that will be added | null.
111114
*/
112115
beforeBreadcrumb?(breadcrumb: Breadcrumb, hint?: BreadcrumbHint): Breadcrumb | null;
116+
117+
_experiments?: {
118+
[key: string]: any;
119+
};
113120
}

0 commit comments

Comments
 (0)