Skip to content

Commit 47a3cc7

Browse files
feat(webvitals): Adds event entry names for INP handler. Also guard against empty metric value
Adds more interaction event entry names to the INP handler, and distinguish op between click, hover, drag, and press. Also adds a check to `metric.value` to drop any spans that would have empty exclusive time
1 parent aef8c98 commit 47a3cc7

File tree

5 files changed

+141
-8
lines changed
  • dev-packages/browser-integration-tests/suites/tracing
  • packages/tracing-internal/src/browser/metrics

5 files changed

+141
-8
lines changed

dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/init.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Sentry.init({
1111
_experiments: {
1212
enableInteractions: true,
1313
},
14+
enableInp: true,
1415
}),
1516
],
1617
tracesSampleRate: 1,

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

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Route } from '@playwright/test';
22
import { expect } from '@playwright/test';
3-
import type { Event, Span, SpanContext, Transaction } from '@sentry/types';
3+
import type { Event as SentryEvent, Measurements, Span, SpanContext, SpanJSON, Transaction } from '@sentry/types';
44

55
import { sentryTest } from '../../../../utils/fixtures';
66
import {
@@ -30,7 +30,7 @@ sentryTest('should capture interaction transaction. @firefox', async ({ browserN
3030
const url = await getLocalTestPath({ testDir: __dirname });
3131

3232
await page.goto(url);
33-
await getFirstSentryEnvelopeRequest<Event>(page);
33+
await getFirstSentryEnvelopeRequest<SentryEvent>(page);
3434

3535
await page.locator('[data-test-id=interaction-button]').click();
3636
await page.locator('.clicked[data-test-id=interaction-button]').isVisible();
@@ -70,12 +70,12 @@ sentryTest(
7070

7171
const url = await getLocalTestPath({ testDir: __dirname });
7272
await page.goto(url);
73-
await getFirstSentryEnvelopeRequest<Event>(page);
73+
await getFirstSentryEnvelopeRequest<SentryEvent>(page);
7474

7575
for (let i = 0; i < 4; i++) {
7676
await wait(100);
7777
await page.locator('[data-test-id=interaction-button]').click();
78-
const envelope = await getMultipleSentryEnvelopeRequests<Event>(page, 1);
78+
const envelope = await getMultipleSentryEnvelopeRequests<SentryEvent>(page, 1);
7979
expect(envelope[0].spans).toHaveLength(1);
8080
}
8181
},
@@ -97,7 +97,7 @@ sentryTest(
9797
const url = await getLocalTestPath({ testDir: __dirname });
9898

9999
await page.goto(url);
100-
await getFirstSentryEnvelopeRequest<Event>(page);
100+
await getFirstSentryEnvelopeRequest<SentryEvent>(page);
101101

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

@@ -112,3 +112,51 @@ sentryTest(
112112
expect(interactionSpan.description).toBe('body > AnnotatedButton');
113113
},
114114
);
115+
116+
sentryTest('should capture an INP click event span. @firefox', async ({ browserName, getLocalTestPath, page }) => {
117+
const supportedBrowsers = ['chromium', 'firefox'];
118+
119+
if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) {
120+
sentryTest.skip();
121+
}
122+
123+
await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` }));
124+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
125+
return route.fulfill({
126+
status: 200,
127+
contentType: 'application/json',
128+
body: JSON.stringify({ id: 'test-id' }),
129+
});
130+
});
131+
132+
const url = await getLocalTestPath({ testDir: __dirname });
133+
134+
await page.goto(url);
135+
await getFirstSentryEnvelopeRequest<SentryEvent>(page);
136+
137+
await page.locator('[data-test-id=interaction-button]').click();
138+
await page.locator('.clicked[data-test-id=interaction-button]').isVisible();
139+
140+
// Wait for the interaction transaction from the enableInteractions experiment
141+
await getMultipleSentryEnvelopeRequests<TransactionJSON>(page, 1);
142+
143+
const spanEnvelopesPromise = getMultipleSentryEnvelopeRequests<
144+
SpanJSON & { exclusive_time: number; measurements: Measurements }
145+
>(page, 1, {
146+
envelopeType: 'span',
147+
});
148+
// Page hide to trigger INP
149+
await page.evaluate(() => {
150+
window.dispatchEvent(new Event('pagehide'));
151+
});
152+
153+
// Get the INP span envelope
154+
const spanEnvelopes = await spanEnvelopesPromise;
155+
156+
expect(spanEnvelopes).toHaveLength(1);
157+
expect(spanEnvelopes[0].op).toBe('ui.interaction.click');
158+
expect(spanEnvelopes[0].description).toBe('body > button.clicked');
159+
expect(spanEnvelopes[0].exclusive_time).toBeGreaterThan(0);
160+
expect(spanEnvelopes[0].measurements.inp.value).toBeGreaterThan(0);
161+
expect(spanEnvelopes[0].measurements.inp.unit).toBe('millisecond');
162+
});

dev-packages/browser-integration-tests/suites/tracing/browsertracing/interactions/init.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Sentry.init({
1212
enableInteractions: true,
1313
enableLongTask: false,
1414
},
15+
enableInp: true,
1516
}),
1617
],
1718
tracesSampleRate: 1,

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

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Route } from '@playwright/test';
22
import { expect } from '@playwright/test';
3-
import type { SerializedEvent, Span, SpanContext, Transaction } from '@sentry/types';
3+
import type { Measurements, SerializedEvent, Span, SpanContext, SpanJSON, Transaction } from '@sentry/types';
44

55
import { sentryTest } from '../../../../utils/fixtures';
66
import {
@@ -112,3 +112,51 @@ sentryTest(
112112
expect(interactionSpan.description).toBe('body > AnnotatedButton');
113113
},
114114
);
115+
116+
sentryTest('should capture an INP click event span. @firefox', async ({ browserName, getLocalTestPath, page }) => {
117+
const supportedBrowsers = ['chromium', 'firefox'];
118+
119+
if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) {
120+
sentryTest.skip();
121+
}
122+
123+
await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` }));
124+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
125+
return route.fulfill({
126+
status: 200,
127+
contentType: 'application/json',
128+
body: JSON.stringify({ id: 'test-id' }),
129+
});
130+
});
131+
132+
const url = await getLocalTestPath({ testDir: __dirname });
133+
134+
await page.goto(url);
135+
await getFirstSentryEnvelopeRequest<Event>(page);
136+
137+
await page.locator('[data-test-id=interaction-button]').click();
138+
await page.locator('.clicked[data-test-id=interaction-button]').isVisible();
139+
140+
// Wait for the interaction transaction from the enableInteractions experiment
141+
await getMultipleSentryEnvelopeRequests<TransactionJSON>(page, 1);
142+
143+
const spanEnvelopesPromise = getMultipleSentryEnvelopeRequests<
144+
SpanJSON & { exclusive_time: number; measurements: Measurements }
145+
>(page, 1, {
146+
envelopeType: 'span',
147+
});
148+
// Page hide to trigger INP
149+
await page.evaluate(() => {
150+
window.dispatchEvent(new Event('pagehide'));
151+
});
152+
153+
// Get the INP span envelope
154+
const spanEnvelopes = await spanEnvelopesPromise;
155+
156+
expect(spanEnvelopes).toHaveLength(1);
157+
expect(spanEnvelopes[0].op).toBe('ui.interaction.click');
158+
expect(spanEnvelopes[0].description).toBe('body > button.clicked');
159+
expect(spanEnvelopes[0].exclusive_time).toBeGreaterThan(0);
160+
expect(spanEnvelopes[0].measurements.inp.value).toBeGreaterThan(0);
161+
expect(spanEnvelopes[0].measurements.inp.unit).toBe('millisecond');
162+
});

packages/tracing-internal/src/browser/metrics/index.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,14 +201,49 @@ function _trackFID(): () => void {
201201
});
202202
}
203203

204+
const INP_ENTRY_MAP: Record<string, 'click' | 'hover' | 'drag' | 'press'> = {
205+
click: 'click',
206+
pointerdown: 'click',
207+
pointerup: 'click',
208+
mousedown: 'click',
209+
mouseup: 'click',
210+
touchstart: 'click',
211+
touchend: 'click',
212+
mouseover: 'hover',
213+
mouseout: 'hover',
214+
mouseenter: 'hover',
215+
mouseleave: 'hover',
216+
pointerover: 'hover',
217+
pointerout: 'hover',
218+
pointerenter: 'hover',
219+
pointerleave: 'hover',
220+
dragstart: 'drag',
221+
dragend: 'drag',
222+
drag: 'drag',
223+
dragenter: 'drag',
224+
dragleave: 'drag',
225+
dragover: 'drag',
226+
drop: 'drag',
227+
keydown: 'press',
228+
keyup: 'press',
229+
keypress: 'press',
230+
input: 'press',
231+
};
232+
204233
/** Starts tracking the Interaction to Next Paint on the current page. */
205234
function _trackINP(interactionIdtoRouteNameMapping: InteractionRouteNameMapping): () => void {
206235
return addInpInstrumentationHandler(({ metric }) => {
207-
const entry = metric.entries.find(e => e.name === 'click' || e.name === 'pointerdown');
236+
if (metric.value === undefined) {
237+
return;
238+
}
239+
const entry = metric.entries.find(
240+
entry => entry.duration === metric.value && INP_ENTRY_MAP[entry.name] !== undefined,
241+
);
208242
const client = getClient();
209243
if (!entry || !client) {
210244
return;
211245
}
246+
const interactionType = INP_ENTRY_MAP[entry.name];
212247
const options = client.getOptions();
213248
/** Build the INP span, create an envelope from the span, and then send the envelope */
214249
const startTime = msToSec((browserPerformanceTimeOrigin as number) + entry.startTime);
@@ -229,7 +264,7 @@ function _trackINP(interactionIdtoRouteNameMapping: InteractionRouteNameMapping)
229264
const span = new Span({
230265
startTimestamp: startTime,
231266
endTimestamp: startTime + duration,
232-
op: 'ui.interaction.click',
267+
op: `ui.interaction.${interactionType}`,
233268
name: htmlTreeAsString(entry.target),
234269
attributes: {
235270
release: options.release,

0 commit comments

Comments
 (0)