Skip to content

Commit 9c41e52

Browse files
mydeas1gr1d
authored andcommitted
WIP INP in v8??
1 parent 3a45a3c commit 9c41e52

File tree

12 files changed

+280
-50
lines changed

12 files changed

+280
-50
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: 46 additions & 30 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 { Contexts, Event, SpanJSON } from '@sentry/types';
3+
import type { Contexts, Event as SentryEvent, Measurements, SpanJSON } 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

@@ -113,34 +113,50 @@ sentryTest(
113113
},
114114
);
115115

116-
sentryTest(
117-
'should use the element name for a clicked element when no component name',
118-
async ({ browserName, getLocalTestPath, page }) => {
119-
const supportedBrowsers = ['chromium', 'firefox'];
120-
121-
if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) {
122-
sentryTest.skip();
123-
}
124-
125-
await page.route('**/path/to/script.js', (route: Route) =>
126-
route.fulfill({ path: `${__dirname}/assets/script.js` }),
127-
);
116+
sentryTest('should capture an INP click event span. @firefox', async ({ browserName, getLocalTestPath, page }) => {
117+
const supportedBrowsers = ['chromium', 'firefox'];
128118

129-
const url = await getLocalTestPath({ testDir: __dirname });
119+
if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) {
120+
sentryTest.skip();
121+
}
130122

131-
await page.goto(url);
132-
await getFirstSentryEnvelopeRequest<Event>(page);
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+
});
133131

134-
await page.locator('[data-test-id=styled-button]').click();
132+
const url = await getLocalTestPath({ testDir: __dirname });
135133

136-
const envelopes = await getMultipleSentryEnvelopeRequests<TransactionJSON>(page, 1);
137-
expect(envelopes).toHaveLength(1);
138-
const eventData = envelopes[0];
134+
await page.goto(url);
135+
await getFirstSentryEnvelopeRequest<SentryEvent>(page);
139136

140-
expect(eventData.spans).toHaveLength(1);
137+
await page.locator('[data-test-id=interaction-button]').click();
138+
await page.locator('.clicked[data-test-id=interaction-button]').isVisible();
141139

142-
const interactionSpan = eventData.spans![0];
143-
expect(interactionSpan.op).toBe('ui.interaction.click');
144-
expect(interactionSpan.description).toBe('body > StyledButton');
145-
},
146-
);
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/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) {
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import {
2+
SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME,
3+
SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT,
4+
SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE,
5+
SentrySpan,
6+
createSpanEnvelope,
7+
getActiveSpan,
8+
getClient,
9+
getCurrentScope,
10+
getRootSpan,
11+
sampleSpan,
12+
spanIsSampled,
13+
spanToJSON,
14+
} from '@sentry/core';
15+
import type { Integration, SpanAttributes } from '@sentry/types';
16+
import { browserPerformanceTimeOrigin, dropUndefinedKeys, htmlTreeAsString, logger } from '@sentry/utils';
17+
import { DEBUG_BUILD } from '../debug-build';
18+
import { addInpInstrumentationHandler } from './instrument';
19+
import { getBrowserPerformanceAPI, msToSec } from './utils';
20+
21+
/**
22+
* Start tracking INP webvital events.
23+
*/
24+
export function startTrackingINP(): () => void {
25+
const performance = getBrowserPerformanceAPI();
26+
if (performance && browserPerformanceTimeOrigin) {
27+
const inpCallback = _trackINP();
28+
29+
return (): void => {
30+
inpCallback();
31+
};
32+
}
33+
34+
return () => undefined;
35+
}
36+
37+
const INP_ENTRY_MAP: Record<string, 'click' | 'hover' | 'drag' | 'press'> = {
38+
click: 'click',
39+
pointerdown: 'click',
40+
pointerup: 'click',
41+
mousedown: 'click',
42+
mouseup: 'click',
43+
touchstart: 'click',
44+
touchend: 'click',
45+
mouseover: 'hover',
46+
mouseout: 'hover',
47+
mouseenter: 'hover',
48+
mouseleave: 'hover',
49+
pointerover: 'hover',
50+
pointerout: 'hover',
51+
pointerenter: 'hover',
52+
pointerleave: 'hover',
53+
dragstart: 'drag',
54+
dragend: 'drag',
55+
drag: 'drag',
56+
dragenter: 'drag',
57+
dragleave: 'drag',
58+
dragover: 'drag',
59+
drop: 'drag',
60+
keydown: 'press',
61+
keyup: 'press',
62+
keypress: 'press',
63+
input: 'press',
64+
};
65+
66+
/** Starts tracking the Interaction to Next Paint on the current page. */
67+
function _trackINP(): () => void {
68+
return addInpInstrumentationHandler(({ metric }) => {
69+
const client = getClient();
70+
if (!client || metric.value == undefined) {
71+
return;
72+
}
73+
74+
const entry = metric.entries.find(entry => entry.duration === metric.value && INP_ENTRY_MAP[entry.name]);
75+
76+
if (!entry) {
77+
return;
78+
}
79+
80+
const interactionType = INP_ENTRY_MAP[entry.name];
81+
82+
const options = client.getOptions();
83+
/** Build the INP span, create an envelope from the span, and then send the envelope */
84+
const startTime = msToSec((browserPerformanceTimeOrigin as number) + entry.startTime);
85+
const duration = msToSec(metric.value);
86+
const scope = getCurrentScope();
87+
const activeSpan = getActiveSpan();
88+
const rootSpan = activeSpan ? getRootSpan(activeSpan) : undefined;
89+
90+
const routeName = rootSpan ? spanToJSON(rootSpan).description : undefined;
91+
const user = scope.getUser();
92+
93+
// We need to get the replay, user, and activeTransaction from the current scope
94+
// so that we can associate replay id, profile id, and a user display to the span
95+
const replay = client.getIntegrationByName<Integration & { getReplayId: () => string }>('Replay');
96+
97+
const replayId = replay && replay.getReplayId();
98+
99+
const userDisplay = user !== undefined ? user.email || user.id || user.ip_address : undefined;
100+
const profileId = scope.getScopeData().contexts?.profile?.profile_id as string | undefined;
101+
102+
const name = htmlTreeAsString(entry.target);
103+
const parentSampled = activeSpan ? spanIsSampled(activeSpan) : undefined;
104+
const attributes: SpanAttributes = dropUndefinedKeys({
105+
release: options.release,
106+
environment: options.environment,
107+
transaction: routeName,
108+
[SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME]: metric.value,
109+
user: userDisplay || undefined,
110+
profile_id: profileId || undefined,
111+
replay_id: replayId || undefined,
112+
});
113+
114+
/** Check to see if the span should be sampled */
115+
const [sampled] = sampleSpan(options, {
116+
name,
117+
parentSampled,
118+
attributes,
119+
transactionContext: {
120+
name,
121+
parentSampled,
122+
},
123+
});
124+
125+
// Nothing to do
126+
if (!sampled) {
127+
return;
128+
}
129+
130+
const span = new SentrySpan({
131+
startTimestamp: startTime,
132+
endTimestamp: startTime + duration,
133+
op: `ui.interaction.${interactionType}`,
134+
name,
135+
attributes,
136+
});
137+
138+
span.addEvent('inp', {
139+
[SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT]: 'millisecond',
140+
[SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE]: metric.value,
141+
});
142+
143+
const envelope = span ? createSpanEnvelope([span]) : undefined;
144+
const transport = client && client.getTransport();
145+
if (transport && envelope) {
146+
transport.send(envelope).then(null, reason => {
147+
DEBUG_BUILD && logger.error('Error while sending interaction:', reason);
148+
});
149+
}
150+
return;
151+
});
152+
}

0 commit comments

Comments
 (0)