Skip to content

Commit 658d6d1

Browse files
author
Luca Forstner
authored
ref: Collect child spans references via non-enumerable on Span object (#10715)
1 parent 54b94da commit 658d6d1

File tree

7 files changed

+91
-43
lines changed

7 files changed

+91
-43
lines changed

packages/core/src/tracing/hubextensions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ The transaction will not be sampled. Please use the ${configInstrumenter} instru
7575
...customSamplingContext,
7676
});
7777
if (transaction.isRecording()) {
78-
transaction.initSpanRecorder(options._experiments && (options._experiments.maxSpans as number));
78+
transaction.initSpanRecorder();
7979
}
8080
if (client) {
8181
client.emit('startTransaction', transaction);
@@ -122,7 +122,7 @@ export function startIdleTransaction(
122122
...customSamplingContext,
123123
});
124124
if (transaction.isRecording()) {
125-
transaction.initSpanRecorder(options._experiments && (options._experiments.maxSpans as number));
125+
transaction.initSpanRecorder();
126126
}
127127
if (client) {
128128
client.emit('startTransaction', transaction);

packages/core/src/tracing/sentrySpan.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
} from '../utils/spanUtils';
3030
import type { SpanStatusType } from './spanstatus';
3131
import { setHttpStatus } from './spanstatus';
32+
import { addChildSpanToSpan } from './trace';
3233

3334
/**
3435
* Keeps track of finished spans for a given transaction
@@ -401,6 +402,11 @@ export class SentrySpan implements SpanInterface {
401402
childSpan.spanRecorder.add(childSpan);
402403
}
403404

405+
// To allow for interoperability we track the children of a span twice: Once with the span recorder (old) once with
406+
// the `addChildSpanToSpan`. Eventually we will only use `addChildSpanToSpan` and drop the span recorder.
407+
// To ensure interoperability with the `startSpan` API, `addChildSpanToSpan` is also called here.
408+
addChildSpanToSpan(this, childSpan);
409+
404410
const rootSpan = getRootSpan(this);
405411
// TODO: still set span.transaction here until we have a more permanent solution
406412
// Probably similarly to the weakmap we hold in node-experimental

packages/core/src/tracing/trace.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,10 @@ export function startInactiveSpan(context: StartSpanOptions): Span | undefined {
156156
});
157157
}
158158

159+
if (parentSpan) {
160+
addChildSpanToSpan(parentSpan, span);
161+
}
162+
159163
setCapturedScopesOnSpan(span, scope, isolationScope);
160164

161165
return span;
@@ -307,6 +311,10 @@ function createChildSpanOrTransaction(
307311
});
308312
}
309313

314+
if (parentSpan) {
315+
addChildSpanToSpan(parentSpan, span);
316+
}
317+
310318
setCapturedScopesOnSpan(span, scope, isolationScope);
311319

312320
return span;
@@ -330,6 +338,47 @@ function normalizeContext(context: StartSpanOptions): TransactionContext {
330338
return context;
331339
}
332340

341+
const CHILD_SPANS_FIELD = '_sentryChildSpans';
342+
343+
type SpanWithPotentialChildren = Span & {
344+
[CHILD_SPANS_FIELD]?: Set<Span>;
345+
};
346+
347+
/**
348+
* Adds an opaque child span reference to a span.
349+
*/
350+
export function addChildSpanToSpan(span: SpanWithPotentialChildren, childSpan: Span): void {
351+
if (span[CHILD_SPANS_FIELD] && span[CHILD_SPANS_FIELD].size < 1000) {
352+
span[CHILD_SPANS_FIELD].add(childSpan);
353+
} else {
354+
span[CHILD_SPANS_FIELD] = new Set([childSpan]);
355+
}
356+
}
357+
358+
/**
359+
* Obtains the entire span tree, meaning a span + all of its descendants for a particular span.
360+
*/
361+
export function getSpanTree(span: SpanWithPotentialChildren): Span[] {
362+
const resultSet = new Set<Span>();
363+
364+
function addSpanChildren(span: SpanWithPotentialChildren): void {
365+
// This exit condition is required to not infinitely loop in case of a circular dependency.
366+
if (resultSet.has(span)) {
367+
return;
368+
} else {
369+
resultSet.add(span);
370+
const childSpans = span[CHILD_SPANS_FIELD] ? Array.from(span[CHILD_SPANS_FIELD]) : [];
371+
for (const childSpan of childSpans) {
372+
addSpanChildren(childSpan);
373+
}
374+
}
375+
}
376+
377+
addSpanChildren(span);
378+
379+
return Array.from(resultSet);
380+
}
381+
333382
const SCOPE_ON_START_SPAN_FIELD = '_sentryScope';
334383
const ISOLATION_SCOPE_ON_START_SPAN_FIELD = '_sentryIsolationScope';
335384

packages/core/src/tracing/transaction.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE
2121
import { spanTimeInputToSeconds, spanToJSON, spanToTraceContext } from '../utils/spanUtils';
2222
import { getDynamicSamplingContextFromSpan } from './dynamicSamplingContext';
2323
import { SentrySpan, SpanRecorder } from './sentrySpan';
24-
import { getCapturedScopesOnSpan } from './trace';
24+
import { getCapturedScopesOnSpan, getSpanTree } from './trace';
2525

2626
/** JSDoc */
2727
export class Transaction extends SentrySpan implements TransactionInterface {
@@ -293,11 +293,8 @@ export class Transaction extends SentrySpan implements TransactionInterface {
293293
return undefined;
294294
}
295295

296-
// eslint-disable-next-line deprecation/deprecation
297-
const finishedSpans = this.spanRecorder
298-
? // eslint-disable-next-line deprecation/deprecation
299-
this.spanRecorder.spans.filter(span => span !== this && spanToJSON(span).timestamp)
300-
: [];
296+
// We only want to include finished spans in the event
297+
const finishedSpans = getSpanTree(this).filter(span => span !== this && spanToJSON(span).timestamp);
301298

302299
if (this._trimEnd && finishedSpans.length > 0) {
303300
const endTimes = finishedSpans.map(span => spanToJSON(span).timestamp).filter(Boolean) as number[];

packages/core/test/lib/tracing/trace.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,16 @@ describe('startSpan', () => {
436436
expect.anything(),
437437
);
438438
});
439+
440+
it('sets a child span reference on the parent span', () => {
441+
expect.assertions(1);
442+
startSpan({ name: 'outer' }, (outerSpan: any) => {
443+
startSpan({ name: 'inner' }, innerSpan => {
444+
const childSpans = Array.from(outerSpan._sentryChildSpans);
445+
expect(childSpans).toContain(innerSpan);
446+
});
447+
});
448+
});
439449
});
440450

441451
describe('startSpanManual', () => {
@@ -545,6 +555,16 @@ describe('startSpanManual', () => {
545555
expect(span).toBeDefined();
546556
});
547557
});
558+
559+
it('sets a child span reference on the parent span', () => {
560+
expect.assertions(1);
561+
startSpan({ name: 'outer' }, (outerSpan: any) => {
562+
startSpanManual({ name: 'inner' }, innerSpan => {
563+
const childSpans = Array.from(outerSpan._sentryChildSpans);
564+
expect(childSpans).toContain(innerSpan);
565+
});
566+
});
567+
});
548568
});
549569

550570
describe('startInactiveSpan', () => {
@@ -674,6 +694,15 @@ describe('startInactiveSpan', () => {
674694
expect.anything(),
675695
);
676696
});
697+
698+
it('sets a child span reference on the parent span', () => {
699+
expect.assertions(1);
700+
startSpan({ name: 'outer' }, (outerSpan: any) => {
701+
const innerSpan = startInactiveSpan({ name: 'inner' });
702+
const childSpans = Array.from(outerSpan._sentryChildSpans);
703+
expect(childSpans).toContain(innerSpan);
704+
});
705+
});
677706
});
678707

679708
describe('continueTrace', () => {

packages/opentelemetry/src/custom/transaction.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Hub } from '@sentry/core';
22
import { Transaction } from '@sentry/core';
3-
import type { ClientOptions, Hub as HubInterface, TransactionContext } from '@sentry/types';
3+
import type { Hub as HubInterface, TransactionContext } from '@sentry/types';
44

55
/**
66
* This is a fork of core's tracing/hubextensions.ts _startTransaction,
@@ -9,13 +9,12 @@ import type { ClientOptions, Hub as HubInterface, TransactionContext } from '@se
99
export function startTransaction(hub: HubInterface, transactionContext: TransactionContext): Transaction {
1010
// eslint-disable-next-line deprecation/deprecation
1111
const client = hub.getClient();
12-
const options: Partial<ClientOptions> = (client && client.getOptions()) || {};
1312

1413
// eslint-disable-next-line deprecation/deprecation
1514
const transaction = new Transaction(transactionContext, hub as Hub);
1615
// Since we do not do sampling here, we assume that this is _always_ sampled
1716
// Any sampling decision happens in OpenTelemetry's sampler
18-
transaction.initSpanRecorder(options._experiments && (options._experiments.maxSpans as number));
17+
transaction.initSpanRecorder();
1918

2019
if (client) {
2120
client.emit('startTransaction', transaction);

packages/tracing/test/span.test.ts

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -272,22 +272,6 @@ describe('SentrySpan', () => {
272272
expect(spy.mock.calls[0][0].contexts.trace).toEqual(transaction.getTraceContext());
273273
});
274274

275-
test('maxSpans correctly limits number of spans', () => {
276-
const options = getDefaultBrowserClientOptions({
277-
_experiments: { maxSpans: 3 },
278-
tracesSampleRate: 1,
279-
});
280-
const _hub = new Hub(new BrowserClient(options));
281-
const spy = jest.spyOn(_hub as any, 'captureEvent') as any;
282-
const transaction = _hub.startTransaction({ name: 'test' });
283-
for (let i = 0; i < 10; i++) {
284-
const child = transaction.startChild();
285-
child.end();
286-
}
287-
transaction.end();
288-
expect(spy.mock.calls[0][0].spans).toHaveLength(3);
289-
});
290-
291275
test('no span recorder created if transaction.sampled is false', () => {
292276
const options = getDefaultBrowserClientOptions({
293277
tracesSampleRate: 1,
@@ -421,22 +405,6 @@ describe('SentrySpan', () => {
421405
expect(spy.mock.calls[0][0].contexts.trace).toEqual(transaction.getTraceContext());
422406
});
423407

424-
test('maxSpans correctly limits number of spans', () => {
425-
const options = getDefaultBrowserClientOptions({
426-
_experiments: { maxSpans: 3 },
427-
tracesSampleRate: 1,
428-
});
429-
const _hub = new Hub(new BrowserClient(options));
430-
const spy = jest.spyOn(_hub as any, 'captureEvent') as any;
431-
const transaction = _hub.startTransaction({ name: 'test' });
432-
for (let i = 0; i < 10; i++) {
433-
const child = transaction.startChild();
434-
child.end();
435-
}
436-
transaction.end();
437-
expect(spy.mock.calls[0][0].spans).toHaveLength(3);
438-
});
439-
440408
test('no span recorder created if transaction.sampled is false', () => {
441409
const options = getDefaultBrowserClientOptions({
442410
tracesSampleRate: 1,

0 commit comments

Comments
 (0)