Skip to content

Commit

Permalink
feat(webvitals): Adds event entry names for INP handler. Also guard a…
Browse files Browse the repository at this point in the history
…gainst 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
  • Loading branch information
edwardgou-sentry committed Mar 18, 2024
1 parent aef8c98 commit 47a3cc7
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 8 deletions.
Expand Up @@ -11,6 +11,7 @@ Sentry.init({
_experiments: {
enableInteractions: true,
},
enableInp: true,
}),
],
tracesSampleRate: 1,
Expand Down
@@ -1,6 +1,6 @@
import type { Route } from '@playwright/test';
import { expect } from '@playwright/test';
import type { Event, Span, SpanContext, Transaction } from '@sentry/types';
import type { Event as SentryEvent, Measurements, Span, SpanContext, SpanJSON, Transaction } from '@sentry/types';

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

await page.goto(url);
await getFirstSentryEnvelopeRequest<Event>(page);
await getFirstSentryEnvelopeRequest<SentryEvent>(page);

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

const url = await getLocalTestPath({ testDir: __dirname });
await page.goto(url);
await getFirstSentryEnvelopeRequest<Event>(page);
await getFirstSentryEnvelopeRequest<SentryEvent>(page);

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

await page.goto(url);
await getFirstSentryEnvelopeRequest<Event>(page);
await getFirstSentryEnvelopeRequest<SentryEvent>(page);

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

Expand All @@ -112,3 +112,51 @@ sentryTest(
expect(interactionSpan.description).toBe('body > AnnotatedButton');
},
);

sentryTest('should capture an INP click event span. @firefox', async ({ browserName, getLocalTestPath, page }) => {
const supportedBrowsers = ['chromium', 'firefox'];

if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) {
sentryTest.skip();
}

await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` }));
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
return route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ id: 'test-id' }),
});
});

const url = await getLocalTestPath({ testDir: __dirname });

await page.goto(url);
await getFirstSentryEnvelopeRequest<SentryEvent>(page);

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

// Wait for the interaction transaction from the enableInteractions experiment
await getMultipleSentryEnvelopeRequests<TransactionJSON>(page, 1);

const spanEnvelopesPromise = getMultipleSentryEnvelopeRequests<
SpanJSON & { exclusive_time: number; measurements: Measurements }
>(page, 1, {
envelopeType: 'span',
});
// Page hide to trigger INP
await page.evaluate(() => {
window.dispatchEvent(new Event('pagehide'));
});

// Get the INP span envelope
const spanEnvelopes = await spanEnvelopesPromise;

expect(spanEnvelopes).toHaveLength(1);
expect(spanEnvelopes[0].op).toBe('ui.interaction.click');
expect(spanEnvelopes[0].description).toBe('body > button.clicked');
expect(spanEnvelopes[0].exclusive_time).toBeGreaterThan(0);
expect(spanEnvelopes[0].measurements.inp.value).toBeGreaterThan(0);
expect(spanEnvelopes[0].measurements.inp.unit).toBe('millisecond');
});
Expand Up @@ -12,6 +12,7 @@ Sentry.init({
enableInteractions: true,
enableLongTask: false,
},
enableInp: true,
}),
],
tracesSampleRate: 1,
Expand Down
@@ -1,6 +1,6 @@
import type { Route } from '@playwright/test';
import { expect } from '@playwright/test';
import type { SerializedEvent, Span, SpanContext, Transaction } from '@sentry/types';
import type { Measurements, SerializedEvent, Span, SpanContext, SpanJSON, Transaction } from '@sentry/types';

import { sentryTest } from '../../../../utils/fixtures';
import {
Expand Down Expand Up @@ -112,3 +112,51 @@ sentryTest(
expect(interactionSpan.description).toBe('body > AnnotatedButton');
},
);

sentryTest('should capture an INP click event span. @firefox', async ({ browserName, getLocalTestPath, page }) => {
const supportedBrowsers = ['chromium', 'firefox'];

if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) {
sentryTest.skip();
}

await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` }));
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
return route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ id: 'test-id' }),
});
});

const url = await getLocalTestPath({ testDir: __dirname });

await page.goto(url);
await getFirstSentryEnvelopeRequest<Event>(page);

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

// Wait for the interaction transaction from the enableInteractions experiment
await getMultipleSentryEnvelopeRequests<TransactionJSON>(page, 1);

const spanEnvelopesPromise = getMultipleSentryEnvelopeRequests<
SpanJSON & { exclusive_time: number; measurements: Measurements }
>(page, 1, {
envelopeType: 'span',
});
// Page hide to trigger INP
await page.evaluate(() => {
window.dispatchEvent(new Event('pagehide'));
});

// Get the INP span envelope
const spanEnvelopes = await spanEnvelopesPromise;

expect(spanEnvelopes).toHaveLength(1);
expect(spanEnvelopes[0].op).toBe('ui.interaction.click');
expect(spanEnvelopes[0].description).toBe('body > button.clicked');
expect(spanEnvelopes[0].exclusive_time).toBeGreaterThan(0);
expect(spanEnvelopes[0].measurements.inp.value).toBeGreaterThan(0);
expect(spanEnvelopes[0].measurements.inp.unit).toBe('millisecond');
});
39 changes: 37 additions & 2 deletions packages/tracing-internal/src/browser/metrics/index.ts
Expand Up @@ -201,14 +201,49 @@ function _trackFID(): () => void {
});
}

const INP_ENTRY_MAP: Record<string, 'click' | 'hover' | 'drag' | 'press'> = {
click: 'click',
pointerdown: 'click',
pointerup: 'click',
mousedown: 'click',
mouseup: 'click',
touchstart: 'click',
touchend: 'click',
mouseover: 'hover',
mouseout: 'hover',
mouseenter: 'hover',
mouseleave: 'hover',
pointerover: 'hover',
pointerout: 'hover',
pointerenter: 'hover',
pointerleave: 'hover',
dragstart: 'drag',
dragend: 'drag',
drag: 'drag',
dragenter: 'drag',
dragleave: 'drag',
dragover: 'drag',
drop: 'drag',
keydown: 'press',
keyup: 'press',
keypress: 'press',
input: 'press',
};

/** Starts tracking the Interaction to Next Paint on the current page. */
function _trackINP(interactionIdtoRouteNameMapping: InteractionRouteNameMapping): () => void {
return addInpInstrumentationHandler(({ metric }) => {
const entry = metric.entries.find(e => e.name === 'click' || e.name === 'pointerdown');
if (metric.value === undefined) {
return;
}
const entry = metric.entries.find(
entry => entry.duration === metric.value && INP_ENTRY_MAP[entry.name] !== undefined,
);
const client = getClient();
if (!entry || !client) {
return;
}
const interactionType = INP_ENTRY_MAP[entry.name];
const options = client.getOptions();
/** Build the INP span, create an envelope from the span, and then send the envelope */
const startTime = msToSec((browserPerformanceTimeOrigin as number) + entry.startTime);
Expand All @@ -229,7 +264,7 @@ function _trackINP(interactionIdtoRouteNameMapping: InteractionRouteNameMapping)
const span = new Span({
startTimestamp: startTime,
endTimestamp: startTime + duration,
op: 'ui.interaction.click',
op: `ui.interaction.${interactionType}`,
name: htmlTreeAsString(entry.target),
attributes: {
release: options.release,
Expand Down

0 comments on commit 47a3cc7

Please sign in to comment.