Skip to content

Commit d679e7c

Browse files
mydeas1gr1d
andauthored
feat(browser): Add INP support for v8 (#11650)
This is a step to support INP on v8. It tries to forward port some stuff that has been merged into v7. It is a bit tricky to do this properly as this was spread over multiple PRs and cannot be directly ported at once. Closes #10945 --------- Co-authored-by: s1gr1d <sigrid.huemer@posteo.at> Co-authored-by: Sigrid Huemer <32902192+s1gr1d@users.noreply.github.com>
1 parent 934943c commit d679e7c

File tree

26 files changed

+519
-93
lines changed

26 files changed

+519
-93
lines changed

.size-limit.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ module.exports = [
5252
path: 'packages/browser/build/npm/esm/index.js',
5353
import: createImport('init', 'browserTracingIntegration', 'replayIntegration', 'feedbackIntegration'),
5454
gzip: true,
55-
limit: '86 KB',
55+
limit: '87 KB',
5656
},
5757
{
5858
name: '@sentry/browser (incl. Feedback)',
@@ -165,7 +165,7 @@ module.exports = [
165165
path: createCDNPath('bundle.tracing.replay.feedback.min.js'),
166166
gzip: false,
167167
brotli: false,
168-
limit: '261 KB',
168+
limit: '264 KB',
169169
},
170170
// Next.js SDK (ESM)
171171
{
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const delay = e => {
1+
const blockUI = e => {
22
const startTime = Date.now();
33

44
function getElasped() {
@@ -13,6 +13,6 @@ const delay = e => {
1313
e.target.classList.add('clicked');
1414
};
1515

16-
document.querySelector('[data-test-id=interaction-button]').addEventListener('click', delay);
17-
document.querySelector('[data-test-id=annotated-button]').addEventListener('click', delay);
18-
document.querySelector('[data-test-id=styled-button]').addEventListener('click', delay);
16+
document.querySelector('[data-test-id=interaction-button]').addEventListener('click', blockUI);
17+
document.querySelector('[data-test-id=annotated-button]').addEventListener('click', blockUI);
18+
document.querySelector('[data-test-id=styled-button]').addEventListener('click', blockUI);

dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/template.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,5 @@
88
<button data-test-id="interaction-button">Click Me</button>
99
<button data-test-id="annotated-button" data-sentry-component="AnnotatedButton" data-sentry-element="StyledButton">Click Me</button>
1010
<button data-test-id="styled-button" data-sentry-element="StyledButton">Click Me</button>
11-
<script src="https://example.com/path/to/script.js"></script>
1211
</body>
1312
</html>

dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts

Lines changed: 19 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import type { Route } from '@playwright/test';
21
import { expect } from '@playwright/test';
3-
import type { Contexts, Event, SpanJSON } from '@sentry/types';
2+
import type { Event as SentryEvent } from '@sentry/types';
43

54
import { sentryTest } from '../../../../utils/fixtures';
65
import {
@@ -9,33 +8,24 @@ import {
98
shouldSkipTracingTest,
109
} from '../../../../utils/helpers';
1110

12-
type TransactionJSON = SpanJSON & {
13-
spans: SpanJSON[];
14-
contexts: Contexts;
15-
platform: string;
16-
type: string;
17-
};
18-
19-
const wait = (time: number) => new Promise(res => setTimeout(res, time));
20-
2111
sentryTest('should capture interaction transaction. @firefox', async ({ browserName, getLocalTestPath, page }) => {
2212
const supportedBrowsers = ['chromium', 'firefox'];
2313

2414
if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) {
2515
sentryTest.skip();
2616
}
27-
28-
await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` }));
29-
3017
const url = await getLocalTestPath({ testDir: __dirname });
3118

3219
await page.goto(url);
33-
await getFirstSentryEnvelopeRequest<Event>(page);
20+
await getFirstSentryEnvelopeRequest<SentryEvent>(page);
21+
22+
const envelopesPromise = getMultipleSentryEnvelopeRequests<SentryEvent>(page, 1);
3423

3524
await page.locator('[data-test-id=interaction-button]').click();
3625
await page.locator('.clicked[data-test-id=interaction-button]').isVisible();
3726

38-
const envelopes = await getMultipleSentryEnvelopeRequests<TransactionJSON>(page, 1);
27+
const envelopes = await envelopesPromise;
28+
3929
expect(envelopes).toHaveLength(1);
4030

4131
const eventData = envelopes[0];
@@ -64,18 +54,15 @@ sentryTest(
6454
sentryTest.skip();
6555
}
6656

67-
await page.route('**/path/to/script.js', (route: Route) =>
68-
route.fulfill({ path: `${__dirname}/assets/script.js` }),
69-
);
70-
7157
const url = await getLocalTestPath({ testDir: __dirname });
7258
await page.goto(url);
73-
await getFirstSentryEnvelopeRequest<Event>(page);
59+
await getFirstSentryEnvelopeRequest<SentryEvent>(page);
7460

7561
for (let i = 0; i < 4; i++) {
76-
await wait(100);
62+
const envelopePromise = getMultipleSentryEnvelopeRequests<SentryEvent>(page, 1);
63+
await page.waitForTimeout(1000);
7764
await page.locator('[data-test-id=interaction-button]').click();
78-
const envelope = await getMultipleSentryEnvelopeRequests<Event>(page, 1);
65+
const envelope = await envelopePromise;
7966
expect(envelope[0].spans).toHaveLength(1);
8067
}
8168
},
@@ -90,18 +77,16 @@ sentryTest(
9077
sentryTest.skip();
9178
}
9279

93-
await page.route('**/path/to/script.js', (route: Route) =>
94-
route.fulfill({ path: `${__dirname}/assets/script.js` }),
95-
);
96-
9780
const url = await getLocalTestPath({ testDir: __dirname });
9881

9982
await page.goto(url);
100-
await getFirstSentryEnvelopeRequest<Event>(page);
83+
await getFirstSentryEnvelopeRequest<SentryEvent>(page);
84+
85+
const envelopePromise = getMultipleSentryEnvelopeRequests<SentryEvent>(page, 1);
10186

10287
await page.locator('[data-test-id=annotated-button]').click();
10388

104-
const envelopes = await getMultipleSentryEnvelopeRequests<TransactionJSON>(page, 1);
89+
const envelopes = await envelopePromise;
10590
expect(envelopes).toHaveLength(1);
10691
const eventData = envelopes[0];
10792

@@ -122,21 +107,19 @@ sentryTest(
122107
sentryTest.skip();
123108
}
124109

125-
await page.route('**/path/to/script.js', (route: Route) =>
126-
route.fulfill({ path: `${__dirname}/assets/script.js` }),
127-
);
128-
129110
const url = await getLocalTestPath({ testDir: __dirname });
130111

131112
await page.goto(url);
132-
await getFirstSentryEnvelopeRequest<Event>(page);
113+
await getFirstSentryEnvelopeRequest<SentryEvent>(page);
114+
115+
const envelopesPromise = getMultipleSentryEnvelopeRequests<SentryEvent>(page, 1);
133116

134117
await page.locator('[data-test-id=styled-button]').click();
135118

136-
const envelopes = await getMultipleSentryEnvelopeRequests<TransactionJSON>(page, 1);
119+
const envelopes = await envelopesPromise;
137120
expect(envelopes).toHaveLength(1);
138-
const eventData = envelopes[0];
139121

122+
const eventData = envelopes[0];
140123
expect(eventData.spans).toHaveLength(1);
141124

142125
const interactionSpan = eventData.spans![0];
Lines changed: 15 additions & 0 deletions
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+
Sentry.browserTracingIntegration({
9+
idleTimeout: 1000,
10+
enableLongTask: false,
11+
enableInp: true,
12+
}),
13+
],
14+
tracesSampleRate: 1,
15+
});
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: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { expect } from '@playwright/test';
2+
import type { Event as SentryEvent, SpanJSON } from '@sentry/types';
3+
4+
import { sentryTest } from '../../../../utils/fixtures';
5+
import {
6+
getFirstSentryEnvelopeRequest,
7+
getMultipleSentryEnvelopeRequests,
8+
shouldSkipTracingTest,
9+
} from '../../../../utils/helpers';
10+
11+
sentryTest('should capture an INP click event span.', async ({ browserName, getLocalTestPath, page }) => {
12+
const supportedBrowsers = ['chromium'];
13+
14+
if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) {
15+
sentryTest.skip();
16+
}
17+
18+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
19+
return route.fulfill({
20+
status: 200,
21+
contentType: 'application/json',
22+
body: JSON.stringify({ id: 'test-id' }),
23+
});
24+
});
25+
26+
const url = await getLocalTestPath({ testDir: __dirname });
27+
28+
await page.goto(url);
29+
await getFirstSentryEnvelopeRequest<SentryEvent>(page); // wait for page load
30+
31+
const spanEnvelopesPromise = getMultipleSentryEnvelopeRequests<SpanJSON>(page, 1, {
32+
envelopeType: 'span',
33+
});
34+
35+
await page.locator('[data-test-id=normal-button]').click();
36+
await page.locator('.clicked[data-test-id=normal-button]').isVisible();
37+
38+
await page.waitForTimeout(500);
39+
40+
// Page hide to trigger INP
41+
await page.evaluate(() => {
42+
window.dispatchEvent(new Event('pagehide'));
43+
});
44+
45+
// Get the INP span envelope
46+
const spanEnvelopes = await spanEnvelopesPromise;
47+
48+
expect(spanEnvelopes).toHaveLength(1);
49+
expect(spanEnvelopes[0].op).toBe('ui.interaction.click');
50+
expect(spanEnvelopes[0].description).toBe('body > NormalButton');
51+
expect(spanEnvelopes[0].exclusive_time).toBeGreaterThan(0);
52+
expect(spanEnvelopes[0].measurements?.inp.value).toBeGreaterThan(0);
53+
expect(spanEnvelopes[0].measurements?.inp.unit).toBe('millisecond');
54+
});
55+
56+
sentryTest(
57+
'should choose the slowest interaction click event when INP is triggered.',
58+
async ({ browserName, getLocalTestPath, page }) => {
59+
const supportedBrowsers = ['chromium'];
60+
61+
if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) {
62+
sentryTest.skip();
63+
}
64+
65+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
66+
return route.fulfill({
67+
status: 200,
68+
contentType: 'application/json',
69+
body: JSON.stringify({ id: 'test-id' }),
70+
});
71+
});
72+
73+
const url = await getLocalTestPath({ testDir: __dirname });
74+
75+
await page.goto(url);
76+
await getFirstSentryEnvelopeRequest<SentryEvent>(page);
77+
78+
await page.locator('[data-test-id=normal-button]').click();
79+
await page.locator('.clicked[data-test-id=normal-button]').isVisible();
80+
81+
await page.waitForTimeout(500);
82+
83+
await page.locator('[data-test-id=slow-button]').click();
84+
await page.locator('.clicked[data-test-id=slow-button]').isVisible();
85+
86+
await page.waitForTimeout(500);
87+
88+
const spanEnvelopesPromise = getMultipleSentryEnvelopeRequests<SpanJSON>(page, 1, {
89+
envelopeType: 'span',
90+
});
91+
92+
// Page hide to trigger INP
93+
await page.evaluate(() => {
94+
window.dispatchEvent(new Event('pagehide'));
95+
});
96+
97+
// Get the INP span envelope
98+
const spanEnvelopes = await spanEnvelopesPromise;
99+
100+
expect(spanEnvelopes).toHaveLength(1);
101+
expect(spanEnvelopes[0].op).toBe('ui.interaction.click');
102+
expect(spanEnvelopes[0].description).toBe('body > SlowButton');
103+
expect(spanEnvelopes[0].exclusive_time).toBeGreaterThan(400);
104+
expect(spanEnvelopes[0].measurements?.inp.value).toBeGreaterThan(400);
105+
expect(spanEnvelopes[0].measurements?.inp.unit).toBe('millisecond');
106+
},
107+
);

packages/browser-utils/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export {
1111
startTrackingInteractions,
1212
startTrackingLongTasks,
1313
startTrackingWebVitals,
14+
startTrackingINP,
1415
} from './metrics/browserMetrics';
1516

1617
export { addClickKeypressInstrumentationHandler } from './instrument/dom';

packages/browser-utils/src/metrics/browserMetrics.ts

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
addTtfbInstrumentationHandler,
1515
} from './instrument';
1616
import { WINDOW } from './types';
17-
import { isMeasurementValue, startAndEndSpan } from './utils';
17+
import { getBrowserPerformanceAPI, isMeasurementValue, msToSec, startAndEndSpan } from './utils';
1818
import { getNavigationEntry } from './web-vitals/lib/getNavigationEntry';
1919
import { getVisibilityWatcher } from './web-vitals/lib/getVisibilityWatcher';
2020

@@ -58,19 +58,6 @@ interface NavigatorDeviceMemory {
5858

5959
const MAX_INT_AS_BYTES = 2147483647;
6060

61-
/**
62-
* Converts from milliseconds to seconds
63-
* @param time time in ms
64-
*/
65-
function msToSec(time: number): number {
66-
return time / 1000;
67-
}
68-
69-
function getBrowserPerformanceAPI(): Performance | undefined {
70-
// @ts-expect-error we want to make sure all of these are available, even if TS is sure they are
71-
return WINDOW && WINDOW.addEventListener && WINDOW.performance;
72-
}
73-
7461
let _performanceCursor: number = 0;
7562

7663
let _measurements: Measurements = {};
@@ -170,6 +157,8 @@ export function startTrackingInteractions(): void {
170157
});
171158
}
172159

160+
export { startTrackingINP } from './inp';
161+
173162
/** Starts tracking the Cumulative Layout Shift on the current page. */
174163
function _trackCLS(): () => void {
175164
return addClsInstrumentationHandler(({ metric }) => {
@@ -226,7 +215,7 @@ function _trackTtfb(): () => void {
226215
});
227216
}
228217

229-
/** Add performance related spans to a span */
218+
/** Add performance related spans to a transaction */
230219
export function addPerformanceEntries(span: Span): void {
231220
const performance = getBrowserPerformanceAPI();
232221
if (!performance || !WINDOW.performance.getEntries || !browserPerformanceTimeOrigin) {

0 commit comments

Comments
 (0)