Skip to content

Commit 38bd57b

Browse files
authored
fix(tracing): Ensure sent spans are limited to 1000 (#12252)
To avoid too large payloads, we should only send up to 1000 spans. We actually had two different problems here in browser and node: 1. In browser, we _did_ limit to 1000, but we did so in an unfortunate way, which is to reset the set after 1000 spans. So if a span had 1010 children, only the last 10 would be send. 2. In node, we just sent all spans. I rewrote this to now consistently send the first 1000 spans of a transaction. I decided to keep all spans (no matter the limit) on the internal parent-child map. Due to this this may grow a lot, but we can avoid inconsistent state (e.g. what happens if a span has a parent in the map, but the parent does not have the span as children, ...) All of these should be garbage collected together ideally, so this should be fine hopefully.
1 parent 8007c13 commit 38bd57b

File tree

7 files changed

+86
-3
lines changed

7 files changed

+86
-3
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
5+
Sentry.init({
6+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
7+
integrations: [],
8+
tracesSampleRate: 1,
9+
});
10+
11+
Sentry.startSpan({ name: 'parent' }, () => {
12+
for (let i = 0; i < 5000; i++) {
13+
Sentry.startInactiveSpan({ name: `child ${i}` }).end();
14+
}
15+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { expect } from '@playwright/test';
2+
import { sentryTest } from '../../../utils/fixtures';
3+
import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequestOnUrl } from '../../../utils/helpers';
4+
5+
sentryTest('it limits spans to 1000', async ({ getLocalTestUrl, page }) => {
6+
if (shouldSkipTracingTest()) {
7+
sentryTest.skip();
8+
}
9+
10+
const url = await getLocalTestUrl({ testDir: __dirname });
11+
await page.goto(url);
12+
13+
const req = await waitForTransactionRequestOnUrl(page, url);
14+
const transaction = envelopeRequestParser(req);
15+
16+
expect(transaction.spans).toHaveLength(1000);
17+
expect(transaction.spans).toContainEqual(expect.objectContaining({ description: 'child 0' }));
18+
expect(transaction.spans).toContainEqual(expect.objectContaining({ description: 'child 999' }));
19+
expect(transaction.spans).not.toContainEqual(expect.objectContaining({ description: 'child 1000' }));
20+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
2+
import * as Sentry from '@sentry/node';
3+
4+
Sentry.init({
5+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
6+
release: '1.0',
7+
tracesSampleRate: 1.0,
8+
transport: loggingTransport,
9+
});
10+
11+
Sentry.startSpan({ name: 'parent' }, () => {
12+
for (let i = 0; i < 5000; i++) {
13+
Sentry.startInactiveSpan({ name: `child ${i}` }).end();
14+
}
15+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { SpanJSON } from '@sentry/types';
2+
import { createRunner } from '../../../utils/runner';
3+
4+
test('it limits spans to 1000', done => {
5+
const expectedSpans: SpanJSON[] = [];
6+
for (let i = 0; i < 1000; i++) {
7+
expectedSpans.push(expect.objectContaining({ description: `child ${i}` }));
8+
}
9+
10+
createRunner(__dirname, 'scenario.ts')
11+
.ignore('session', 'sessions')
12+
.expect({
13+
transaction: {
14+
transaction: 'parent',
15+
spans: expectedSpans,
16+
},
17+
})
18+
.start(done);
19+
});

packages/core/src/tracing/sentrySpan.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ import { logSpanEnd } from './logSpans';
4141
import { timedEventsToMeasurements } from './measurement';
4242
import { getCapturedScopesOnSpan } from './utils';
4343

44+
const MAX_SPAN_COUNT = 1000;
45+
4446
/**
4547
* Span contains all data about a span
4648
*/
@@ -310,7 +312,12 @@ export class SentrySpan implements Span {
310312
contexts: {
311313
trace: spanToTransactionTraceContext(this),
312314
},
313-
spans,
315+
spans:
316+
// spans.sort() mutates the array, but `spans` is already a copy so we can safely do this here
317+
// we do not use spans anymore after this point
318+
spans.length > MAX_SPAN_COUNT
319+
? spans.sort((a, b) => a.start_timestamp - b.start_timestamp).slice(0, MAX_SPAN_COUNT)
320+
: spans,
314321
start_timestamp: this._startTime,
315322
timestamp: this._endTime,
316323
transaction: this._name,

packages/core/src/utils/spanUtils.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ export function addChildSpanToSpan(span: SpanWithPotentialChildren, childSpan: S
208208

209209
// We store a list of child spans on the parent span
210210
// We need this for `getSpanDescendants()` to work
211-
if (span[CHILD_SPANS_FIELD] && span[CHILD_SPANS_FIELD].size < 1000) {
211+
if (span[CHILD_SPANS_FIELD]) {
212212
span[CHILD_SPANS_FIELD].add(childSpan);
213213
} else {
214214
addNonEnumerableProperty(span, CHILD_SPANS_FIELD, new Set([childSpan]));

packages/opentelemetry/src/spanExporter.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import { parseSpanDescription } from './utils/parseSpanDescription';
3131

3232
type SpanNodeCompleted = SpanNode & { span: ReadableSpan };
3333

34+
const MAX_SPAN_COUNT = 1000;
35+
3436
/**
3537
* A Sentry-specific exporter that converts OpenTelemetry Spans to Sentry Spans & Transactions.
3638
*/
@@ -140,7 +142,12 @@ function maybeSend(spans: ReadableSpan[]): ReadableSpan[] {
140142
createAndFinishSpanForOtelSpan(child, spans, remaining);
141143
});
142144

143-
transactionEvent.spans = spans;
145+
// spans.sort() mutates the array, but we do not use this anymore after this point
146+
// so we can safely mutate it here
147+
transactionEvent.spans =
148+
spans.length > MAX_SPAN_COUNT
149+
? spans.sort((a, b) => a.start_timestamp - b.start_timestamp).slice(0, MAX_SPAN_COUNT)
150+
: spans;
144151

145152
const measurements = timedEventsToMeasurements(span.events);
146153
if (measurements) {

0 commit comments

Comments
 (0)