Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions packages/core/src/tracing/sentrySpan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -458,18 +458,31 @@ export class SentrySpan implements Span {
// TODO(standalone): drop the `isStandaloneSpan(descendant)` check once the static trace lifecycle is gone.
options.onSpanCaptured?.(this);
const spans: SpanJSON[] = [];
let droppedUnfinishedSpanCount = 0;
for (const descendant of getSpanDescendants(this)) {
if (descendant === this || isStandaloneSpan(descendant) || options.isSpanAlreadyCaptured?.(descendant)) {
continue;
}
const spanJSON = spanToStaticSpanJSON(descendant);
if (!isFullFinishedSpan(spanJSON)) {
// A strategy tracking captured spans re-emits this child as an orphan transaction once it ends.
if (!options.onSpanCaptured) {
droppedUnfinishedSpanCount++;
}
continue;
}
options.onSpanCaptured?.(descendant);
spans.push(spanJSON);
}

const client = capturedSpanScope?.getClient() || getClient();
if (droppedUnfinishedSpanCount) {
client?.recordDroppedEvent('invalid', 'span', droppedUnfinishedSpanCount);
}
if (spans.length > MAX_SPAN_COUNT) {
client?.recordDroppedEvent('buffer_overflow', 'span', spans.length - MAX_SPAN_COUNT);
}

const source = this._attributes[SENTRY_SEGMENT_NAME_SOURCE];

// remove internal root span attributes we don't need to send.
Expand Down
33 changes: 33 additions & 0 deletions packages/core/test/lib/tracing/deferSegmentSpanCapture.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,39 @@ describe('deferred segment-span capture', () => {
expect(transactions[1]!.contexts?.trace?.data?.['sentry.parent_span_already_sent']).toBe(true);
});

it('does not record a span outcome for a child that is later sent as an orphan transaction', () => {
const recordDroppedEventSpy = vi.spyOn(client, 'recordDroppedEvent');

const root = startInactiveSpan({ name: 'root' });
const child = withActiveSpan(root, () => startInactiveSpan({ name: 'child' }));

root.end();
vi.advanceTimersByTime(100);
child.end();
vi.advanceTimersByTime(100);

expect(transactions).toHaveLength(2);
expect(recordDroppedEventSpy).not.toHaveBeenCalledWith('invalid', 'span', expect.anything());
});

it('records a `buffer_overflow` span outcome for spans beyond the 1000 span limit once', () => {
const recordDroppedEventSpy = vi.spyOn(client, 'recordDroppedEvent');

const root = startInactiveSpan({ name: 'root' });
withActiveSpan(root, () => {
for (let i = 0; i < 1003; i++) {
startInactiveSpan({ name: `child-${i}` }).end();
}
});
root.end();
vi.advanceTimersByTime(100);

expect(transactions).toHaveLength(1);
expect(transactions[0]!.spans).toHaveLength(1000);
expect(recordDroppedEventSpy).toHaveBeenCalledWith('buffer_overflow', 'span', 3);
expect(recordDroppedEventSpy).not.toHaveBeenCalledWith('invalid', 'span', expect.anything());
});

it('drains pending captures synchronously on flush', () => {
const root = startInactiveSpan({ name: 'root' });
root.end();
Expand Down
71 changes: 71 additions & 0 deletions packages/core/test/lib/tracing/sentrySpan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,77 @@ describe('SentrySpan', () => {
type: 'transaction',
});
});

test('records an `invalid` span outcome for unfinished child spans', () => {
const client = new TestClient(getDefaultTestClientOptions({ tracesSampleRate: 1 }));
setCurrentClient(client);
const recordDroppedEventSpy = vi.spyOn(client, 'recordDroppedEvent');
const captureEventSpy = vi.spyOn(getCurrentScope(), 'captureEvent').mockImplementation(() => 'testId');

const root = startInactiveSpan({ name: 'root' });
withActiveSpan(root, () => {
startInactiveSpan({ name: 'unfinished-1' });
startInactiveSpan({ name: 'unfinished-2' });
startInactiveSpan({ name: 'finished' }).end();
});
root.end();

expect(captureEventSpy).toHaveBeenCalledWith(
expect.objectContaining({ spans: [expect.objectContaining({ description: 'finished' })] }),
);
expect(recordDroppedEventSpy).toHaveBeenCalledTimes(1);
expect(recordDroppedEventSpy).toHaveBeenCalledWith('invalid', 'span', 2);
});

test('records a `buffer_overflow` span outcome for spans beyond the 1000 span limit', () => {
const client = new TestClient(getDefaultTestClientOptions({ tracesSampleRate: 1 }));
setCurrentClient(client);
const recordDroppedEventSpy = vi.spyOn(client, 'recordDroppedEvent');
const captureEventSpy = vi.spyOn(getCurrentScope(), 'captureEvent').mockImplementation(() => 'testId');

const root = startInactiveSpan({ name: 'root' });
withActiveSpan(root, () => {
for (let i = 0; i < 1005; i++) {
startInactiveSpan({ name: `child-${i}` }).end();
}
});
root.end();

expect(captureEventSpy).toHaveBeenCalledWith(
expect.objectContaining({ spans: expect.objectContaining({ length: 1000 }) }),
);
expect(recordDroppedEventSpy).toHaveBeenCalledTimes(1);
expect(recordDroppedEventSpy).toHaveBeenCalledWith('buffer_overflow', 'span', 5);
});

test('does not record span outcomes for an unsampled root span', () => {
const client = new TestClient(getDefaultTestClientOptions({ tracesSampleRate: 1 }));
setCurrentClient(client);
const recordDroppedEventSpy = vi.spyOn(client, 'recordDroppedEvent');

const root = new SentrySpan({ name: 'root', sampled: false });
withActiveSpan(root, () => {
startInactiveSpan({ name: 'unfinished' });
});
root.end();

expect(recordDroppedEventSpy).not.toHaveBeenCalledWith('invalid', 'span', expect.anything());
});

test('does not record span outcomes for unfinished standalone child spans', () => {
const client = new TestClient(getDefaultTestClientOptions({ tracesSampleRate: 1 }));
setCurrentClient(client);
const recordDroppedEventSpy = vi.spyOn(client, 'recordDroppedEvent');
vi.spyOn(getCurrentScope(), 'captureEvent').mockImplementation(() => 'testId');

const root = startInactiveSpan({ name: 'root' });
withActiveSpan(root, () => {
startInactiveSpan({ name: 'inp', experimental: { standalone: true } });
});
root.end();

expect(recordDroppedEventSpy).not.toHaveBeenCalled();
});
});

describe('setAttribute', () => {
Expand Down
Loading