Skip to content

Commit 3ad7e90

Browse files
committed
fix(browser): Fix INP span creation & transaction tagging
Instead of #12358, this is a simpler change which ensures we pick the transaction from the scope instead. I also added tests for the various different scenarios, to ensure we see how they behave: 1. INP is emitted _during_ pageload (span is active) 2. INP is emitted _after_ pageload a. Pageload is parametrized (route) b. Pageload is unparametrized (URL)
1 parent 7de5ea4 commit 3ad7e90

File tree

15 files changed

+566
-77
lines changed

15 files changed

+566
-77
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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: 1000,
10+
enableLongTask: false,
11+
enableInp: true,
12+
instrumentPageLoad: false,
13+
instrumentNavigation: false,
14+
}),
15+
],
16+
tracesSampleRate: 1,
17+
});
18+
19+
const client = Sentry.getClient();
20+
21+
// Force page load transaction name to a testable value
22+
Sentry.startBrowserTracingPageLoadSpan(client, {
23+
name: 'test-url',
24+
attributes: {
25+
[Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
26+
},
27+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
const blockUI = (delay = 70) => e => {
2+
const startTime = Date.now();
3+
4+
function getElasped() {
5+
const time = Date.now();
6+
return time - startTime;
7+
}
8+
9+
while (getElasped() < delay) {
10+
//
11+
}
12+
13+
e.target.classList.add('clicked');
14+
};
15+
16+
document.querySelector('[data-test-id=not-so-slow-button]').addEventListener('click', blockUI(300));
17+
document.querySelector('[data-test-id=slow-button]').addEventListener('click', blockUI(450));
18+
document.querySelector('[data-test-id=normal-button]').addEventListener('click', blockUI());
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
</head>
6+
<body>
7+
<div>Rendered Before Long Task</div>
8+
<button data-test-id="slow-button" data-sentry-element="SlowButton">Slow</button>
9+
<button data-test-id="not-so-slow-button" data-sentry-element="NotSoSlowButton">Not so slow</button>
10+
<button data-test-id="normal-button" data-sentry-element="NormalButton">Click Me</button>
11+
</body>
12+
</html>
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { expect } from '@playwright/test';
2+
import type { Event as SentryEvent, SpanEnvelope } from '@sentry/types';
3+
4+
import { sentryTest } from '../../../../utils/fixtures';
5+
import {
6+
getFirstSentryEnvelopeRequest,
7+
getMultipleSentryEnvelopeRequests,
8+
properFullEnvelopeRequestParser,
9+
shouldSkipTracingTest,
10+
} from '../../../../utils/helpers';
11+
12+
sentryTest('should capture an INP click event span after pageload', async ({ browserName, getLocalTestPath, page }) => {
13+
const supportedBrowsers = ['chromium'];
14+
15+
if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) {
16+
sentryTest.skip();
17+
}
18+
19+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
20+
return route.fulfill({
21+
status: 200,
22+
contentType: 'application/json',
23+
body: JSON.stringify({ id: 'test-id' }),
24+
});
25+
});
26+
27+
const url = await getLocalTestPath({ testDir: __dirname });
28+
29+
await page.goto(url);
30+
await getFirstSentryEnvelopeRequest<SentryEvent>(page); // wait for page load
31+
32+
const spanEnvelopePromise = getMultipleSentryEnvelopeRequests<SpanEnvelope>(
33+
page,
34+
1,
35+
{ envelopeType: 'span' },
36+
properFullEnvelopeRequestParser,
37+
);
38+
39+
await page.locator('[data-test-id=normal-button]').click();
40+
await page.locator('.clicked[data-test-id=normal-button]').isVisible();
41+
42+
await page.waitForTimeout(500);
43+
44+
// Page hide to trigger INP
45+
await page.evaluate(() => {
46+
window.dispatchEvent(new Event('pagehide'));
47+
});
48+
49+
// Get the INP span envelope
50+
const spanEnvelope = (await spanEnvelopePromise)[0];
51+
52+
const spanEnvelopeHeaders = spanEnvelope[0];
53+
const spanEnvelopeItem = spanEnvelope[1][0][1];
54+
55+
const traceId = spanEnvelopeHeaders.trace!.trace_id;
56+
expect(traceId).toMatch(/[a-f0-9]{32}/);
57+
58+
expect(spanEnvelopeHeaders).toEqual({
59+
sent_at: expect.any(String),
60+
trace: {
61+
environment: 'production',
62+
public_key: 'public',
63+
sample_rate: '1',
64+
sampled: 'true',
65+
trace_id: traceId,
66+
},
67+
});
68+
69+
const inpValue = spanEnvelopeItem.measurements?.inp.value;
70+
expect(inpValue).toBeGreaterThan(0);
71+
72+
expect(spanEnvelopeItem).toEqual({
73+
data: {
74+
'sentry.exclusive_time': inpValue,
75+
'sentry.op': 'ui.interaction.click',
76+
'sentry.origin': 'auto.http.browser.inp',
77+
'sentry.sample_rate': 1,
78+
'sentry.source': 'custom',
79+
transaction: 'test-url',
80+
},
81+
measurements: {
82+
inp: {
83+
unit: 'millisecond',
84+
value: inpValue,
85+
},
86+
},
87+
description: 'body > NormalButton',
88+
exclusive_time: inpValue,
89+
op: 'ui.interaction.click',
90+
origin: 'auto.http.browser.inp',
91+
is_segment: true,
92+
segment_id: spanEnvelopeItem.span_id,
93+
span_id: expect.stringMatching(/[a-f0-9]{16}/),
94+
start_timestamp: expect.any(Number),
95+
timestamp: expect.any(Number),
96+
trace_id: traceId,
97+
});
98+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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: 1000,
10+
enableLongTask: false,
11+
enableInp: true,
12+
instrumentPageLoad: false,
13+
instrumentNavigation: false,
14+
}),
15+
],
16+
tracesSampleRate: 1,
17+
});
18+
19+
const client = Sentry.getClient();
20+
21+
// Force page load transaction name to a testable value
22+
Sentry.startBrowserTracingPageLoadSpan(client, {
23+
name: 'test-route',
24+
attributes: {
25+
[Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
26+
},
27+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
const blockUI = (delay = 70) => e => {
2+
const startTime = Date.now();
3+
4+
function getElasped() {
5+
const time = Date.now();
6+
return time - startTime;
7+
}
8+
9+
while (getElasped() < delay) {
10+
//
11+
}
12+
13+
e.target.classList.add('clicked');
14+
};
15+
16+
document.querySelector('[data-test-id=not-so-slow-button]').addEventListener('click', blockUI(300));
17+
document.querySelector('[data-test-id=slow-button]').addEventListener('click', blockUI(450));
18+
document.querySelector('[data-test-id=normal-button]').addEventListener('click', blockUI());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
</head>
6+
<body>
7+
<div>Rendered Before Long Task</div>
8+
<button data-test-id="slow-button" data-sentry-element="SlowButton">Slow</button>
9+
<button data-test-id="not-so-slow-button" data-sentry-element="NotSoSlowButton">Not so slow</button>
10+
<button data-test-id="normal-button" data-sentry-element="NormalButton">Click Me</button>
11+
</body>
12+
</html>
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { expect } from '@playwright/test';
2+
import type { Event as SentryEvent, SpanEnvelope } from '@sentry/types';
3+
4+
import { sentryTest } from '../../../../utils/fixtures';
5+
import {
6+
getFirstSentryEnvelopeRequest,
7+
getMultipleSentryEnvelopeRequests,
8+
properFullEnvelopeRequestParser,
9+
shouldSkipTracingTest,
10+
} from '../../../../utils/helpers';
11+
12+
sentryTest(
13+
'should capture an INP click event span after pageload for a parametrized transaction',
14+
async ({ browserName, getLocalTestPath, page }) => {
15+
const supportedBrowsers = ['chromium'];
16+
17+
if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) {
18+
sentryTest.skip();
19+
}
20+
21+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
22+
return route.fulfill({
23+
status: 200,
24+
contentType: 'application/json',
25+
body: JSON.stringify({ id: 'test-id' }),
26+
});
27+
});
28+
29+
const url = await getLocalTestPath({ testDir: __dirname });
30+
31+
await page.goto(url);
32+
await getFirstSentryEnvelopeRequest<SentryEvent>(page); // wait for page load
33+
34+
const spanEnvelopePromise = getMultipleSentryEnvelopeRequests<SpanEnvelope>(
35+
page,
36+
1,
37+
{ envelopeType: 'span' },
38+
properFullEnvelopeRequestParser,
39+
);
40+
41+
await page.locator('[data-test-id=normal-button]').click();
42+
await page.locator('.clicked[data-test-id=normal-button]').isVisible();
43+
44+
await page.waitForTimeout(500);
45+
46+
// Page hide to trigger INP
47+
await page.evaluate(() => {
48+
window.dispatchEvent(new Event('pagehide'));
49+
});
50+
51+
// Get the INP span envelope
52+
const spanEnvelope = (await spanEnvelopePromise)[0];
53+
54+
const spanEnvelopeHeaders = spanEnvelope[0];
55+
const spanEnvelopeItem = spanEnvelope[1][0][1];
56+
57+
const traceId = spanEnvelopeHeaders.trace!.trace_id;
58+
expect(traceId).toMatch(/[a-f0-9]{32}/);
59+
60+
expect(spanEnvelopeHeaders).toEqual({
61+
sent_at: expect.any(String),
62+
trace: {
63+
environment: 'production',
64+
public_key: 'public',
65+
sample_rate: '1',
66+
sampled: 'true',
67+
trace_id: traceId,
68+
transaction: 'test-route',
69+
},
70+
});
71+
72+
const inpValue = spanEnvelopeItem.measurements?.inp.value;
73+
expect(inpValue).toBeGreaterThan(0);
74+
75+
expect(spanEnvelopeItem).toEqual({
76+
data: {
77+
'sentry.exclusive_time': inpValue,
78+
'sentry.op': 'ui.interaction.click',
79+
'sentry.origin': 'auto.http.browser.inp',
80+
'sentry.sample_rate': 1,
81+
'sentry.source': 'custom',
82+
transaction: 'test-route',
83+
},
84+
measurements: {
85+
inp: {
86+
unit: 'millisecond',
87+
value: inpValue,
88+
},
89+
},
90+
description: 'body > NormalButton',
91+
exclusive_time: inpValue,
92+
op: 'ui.interaction.click',
93+
origin: 'auto.http.browser.inp',
94+
is_segment: true,
95+
segment_id: spanEnvelopeItem.span_id,
96+
span_id: expect.stringMatching(/[a-f0-9]{16}/),
97+
start_timestamp: expect.any(Number),
98+
timestamp: expect.any(Number),
99+
trace_id: traceId,
100+
});
101+
},
102+
);
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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: 4000,
10+
enableLongTask: false,
11+
enableInp: true,
12+
instrumentPageLoad: false,
13+
instrumentNavigation: false,
14+
}),
15+
],
16+
tracesSampleRate: 1,
17+
});
18+
19+
const client = Sentry.getClient();
20+
21+
// Force page load transaction name to a testable value
22+
Sentry.startBrowserTracingPageLoadSpan(client, {
23+
name: 'test-route',
24+
attributes: {
25+
[Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
26+
},
27+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
const blockUI = (delay = 70) => e => {
2+
const startTime = Date.now();
3+
4+
function getElasped() {
5+
const time = Date.now();
6+
return time - startTime;
7+
}
8+
9+
while (getElasped() < delay) {
10+
//
11+
}
12+
13+
e.target.classList.add('clicked');
14+
};
15+
16+
document.querySelector('[data-test-id=not-so-slow-button]').addEventListener('click', blockUI(300));
17+
document.querySelector('[data-test-id=slow-button]').addEventListener('click', blockUI(450));
18+
document.querySelector('[data-test-id=normal-button]').addEventListener('click', blockUI());
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
</head>
6+
<body>
7+
<div>Rendered Before Long Task</div>
8+
<button data-test-id="slow-button" data-sentry-element="SlowButton">Slow</button>
9+
<button data-test-id="not-so-slow-button" data-sentry-element="NotSoSlowButton">Not so slow</button>
10+
<button data-test-id="normal-button" data-sentry-element="NormalButton">Click Me</button>
11+
</body>
12+
</html>

0 commit comments

Comments
 (0)