Skip to content

Commit 3c76c5d

Browse files
authored
fix(browser): Ensure idle span duration is adjusted when child spans are ignored (#17700)
This PR fixes a problem reported in #17451 where ignoring spans in idle root spans (pageload and navigations most prominently) caused the root span duration to be perceived much longer than reasonable. This is only a problem for idle spans in browser, so this PR applies a pragmatic fix to adjust the end time stamp: We already adjust the time stamp when we end the idle span. So we might as well at this point take any to-be-removed-because-of-`ignoreSpans` spans out of this calculation. This should work well enough without throwing a bunch of idle-span specific logic into the client or completely changing the point in the event lifecycle where `ignoreSpans` is applied. closes #17451
1 parent d039640 commit 3c76c5d

File tree

4 files changed

+95
-4
lines changed

4 files changed

+95
-4
lines changed
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
5+
Sentry.init({
6+
dsn: 'https://[email protected]/1337',
7+
integrations: [
8+
Sentry.browserTracingIntegration({
9+
idleTimeout: 3000,
10+
finalTimeout: 3000,
11+
childSpanTimeout: 3000,
12+
}),
13+
],
14+
ignoreSpans: [/ignore/],
15+
tracesSampleRate: 1,
16+
debug: true,
17+
});
18+
19+
const waitFor = time => new Promise(resolve => setTimeout(resolve, time));
20+
21+
Sentry.startSpanManual(
22+
{
23+
name: 'take-me',
24+
},
25+
async span => {
26+
await waitFor(500);
27+
span.end();
28+
},
29+
);
30+
31+
Sentry.startSpanManual(
32+
{
33+
name: 'ignore-me',
34+
},
35+
async span => {
36+
await waitFor(1500);
37+
span.end();
38+
},
39+
);
40+
41+
Sentry.startSpanManual(
42+
{
43+
name: 'ignore-me-too',
44+
},
45+
async span => {
46+
await waitFor(2500);
47+
span.end();
48+
},
49+
);
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { expect } from '@playwright/test';
2+
import { sentryTest } from '../../../utils/fixtures';
3+
import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../utils/helpers';
4+
5+
sentryTest(
6+
'adjusts the end timestamp of the root idle span if child spans are ignored',
7+
async ({ getLocalTestUrl, page }) => {
8+
if (shouldSkipTracingTest()) {
9+
sentryTest.skip();
10+
}
11+
12+
const pageloadRequestPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === 'pageload');
13+
const url = await getLocalTestUrl({ testDir: __dirname });
14+
await page.goto(url);
15+
16+
const eventData = envelopeRequestParser(await pageloadRequestPromise);
17+
18+
const { start_timestamp: startTimestamp, timestamp: endTimestamp } = eventData;
19+
const durationSeconds = endTimestamp! - startTimestamp!;
20+
21+
const spans = eventData.spans || [];
22+
23+
expect(durationSeconds).toBeGreaterThan(0);
24+
expect(durationSeconds).toBeLessThan(1.5);
25+
26+
expect(spans.some(span => span.description === 'take-me')).toBe(true);
27+
expect(spans.some(span => span.description?.includes('ignore-me'))).toBe(false);
28+
},
29+
);

packages/core/src/client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1347,6 +1347,7 @@ function processBeforeSend(
13471347
if (droppedSpans) {
13481348
client.recordDroppedEvent('before_send', 'span', droppedSpans);
13491349
}
1350+
13501351
processedEvent.spans = processedSpans;
13511352
}
13521353
}

packages/core/src/tracing/idleSpan.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { Span } from '../types-hoist/span';
66
import type { StartSpanOptions } from '../types-hoist/startSpanOptions';
77
import { debug } from '../utils/debug-logger';
88
import { hasSpansEnabled } from '../utils/hasSpansEnabled';
9+
import { shouldIgnoreSpan } from '../utils/should-ignore-span';
910
import { _setSpanForScope } from '../utils/spanOnScope';
1011
import {
1112
getActiveSpan,
@@ -156,10 +157,21 @@ export function startIdleSpan(startSpanOptions: StartSpanOptions, options: Parti
156157
return Reflect.apply(target, thisArg, [spanEndTimestamp, ...rest]);
157158
}
158159

159-
const childEndTimestamps = spans
160-
.map(span => spanToJSON(span).timestamp)
161-
.filter(timestamp => !!timestamp) as number[];
162-
const latestSpanEndTimestamp = childEndTimestamps.length ? Math.max(...childEndTimestamps) : undefined;
160+
const ignoreSpans = client.getOptions().ignoreSpans;
161+
162+
const latestSpanEndTimestamp = spans?.reduce((acc: number | undefined, current) => {
163+
const currentSpanJson = spanToJSON(current);
164+
if (!currentSpanJson.timestamp) {
165+
return acc;
166+
}
167+
// Ignored spans will get dropped later (in the client) but since we already adjust
168+
// the idle span end timestamp here, we can already take to-be-ignored spans out of
169+
// the calculation here.
170+
if (ignoreSpans && shouldIgnoreSpan(currentSpanJson, ignoreSpans)) {
171+
return acc;
172+
}
173+
return acc ? Math.max(acc, currentSpanJson.timestamp) : currentSpanJson.timestamp;
174+
}, undefined);
163175

164176
// In reality this should always exist here, but type-wise it may be undefined...
165177
const spanStartTimestamp = spanToJSON(span).start_timestamp;

0 commit comments

Comments
 (0)