Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;

Sentry.init({
dsn: 'https://[email protected]/1337',
integrations: [
Sentry.browserTracingIntegration({
idleTimeout: 1000,
enableLongTask: false,
enableInp: true,
instrumentPageLoad: false,
instrumentNavigation: false,
}),
],
tracesSampleRate: 1,
});

const client = Sentry.getClient();

// Force page load transaction name to a testable value
Sentry.startBrowserTracingPageLoadSpan(client, {
name: 'test-url',
attributes: {
[Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
const simulateNavigationKeepDOM = e => {
const startTime = Date.now();

function getElapsed() {
const time = Date.now();
return time - startTime;
}

while (getElapsed() < 100) {
// Block UI for 100ms to simulate some processing work during navigation
}

const contentDiv = document.getElementById('content');
contentDiv.innerHTML = '<h1>Page 1</h1><p>Successfully navigated!</p>';

contentDiv.classList.add('navigated');
};

const simulateNavigationChangeDOM = e => {
const startTime = Date.now();

function getElapsed() {
const time = Date.now();
return time - startTime;
}

while (getElapsed() < 100) {
// Block UI for 100ms to simulate some processing work during navigation
}

const navigationHTML =
' <nav id="navigation">\n' +
' <a href="#page1" data-test-id="nav-link-keepDOM" data-sentry-element="NavigationLink">Go to Page 1</a>\n' +
' <a href="#page2" data-test-id="nav-link-changeDOM" data-sentry-element="NavigationLink">Go to Page 2</a>\n' +
' </nav>';

const body = document.querySelector('body');
body.innerHTML = `${navigationHTML}<div id="content"><h1>Page 2</h1><p>Successfully navigated!</p></div>`;

body.classList.add('navigated');
};

document.querySelector('[data-test-id=nav-link-keepDOM]').addEventListener('click', simulateNavigationKeepDOM);
document.querySelector('[data-test-id=nav-link-changeDOM]').addEventListener('click', simulateNavigationChangeDOM);
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<nav id="navigation">
<a href="#page1" data-test-id="nav-link-keepDOM" data-sentry-element="NavigationLink">Go to Page 1</a>
<a href="#page2" data-test-id="nav-link-changeDOM" data-sentry-element="NavigationLink">Go to Page 2</a>
</nav>
<div id="content">
<h1>Home Page</h1>
<p>Click the navigation link to simulate a route change</p>
</div>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { expect } from '@playwright/test';
import type { Event as SentryEvent, SpanEnvelope } from '@sentry/core';
import { sentryTest } from '../../../../utils/fixtures';
import {
getFirstSentryEnvelopeRequest,
getMultipleSentryEnvelopeRequests,
hidePage,
properFullEnvelopeRequestParser,
shouldSkipTracingTest,
} from '../../../../utils/helpers';

const supportedBrowsers = ['chromium'];

sentryTest(
'should capture INP with correct target name when navigation keeps DOM element',
async ({ browserName, getLocalTestUrl, page }) => {
if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) {
sentryTest.skip();
}

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

await page.goto(url);
await getFirstSentryEnvelopeRequest<SentryEvent>(page); // wait for page load

const spanEnvelopePromise = getMultipleSentryEnvelopeRequests<SpanEnvelope>(
page,
1,
{ envelopeType: 'span' },
properFullEnvelopeRequestParser,
);

// Simulating route change (keeping <nav> in DOM)
await page.locator('[data-test-id=nav-link-keepDOM]').click();
await page.locator('.navigated').isVisible();

await page.waitForTimeout(500);

// Page hide to trigger INP
await hidePage(page);

// Get the INP span envelope
const spanEnvelope = (await spanEnvelopePromise)[0];

const spanEnvelopeHeaders = spanEnvelope[0];
const spanEnvelopeItem = spanEnvelope[1][0][1];

const traceId = spanEnvelopeHeaders.trace!.trace_id;
expect(traceId).toMatch(/[a-f0-9]{32}/);

expect(spanEnvelopeHeaders).toEqual({
sent_at: expect.any(String),
trace: {
environment: 'production',
public_key: 'public',
sample_rate: '1',
sampled: 'true',
trace_id: traceId,
sample_rand: expect.any(String),
},
});

const inpValue = spanEnvelopeItem.measurements?.inp.value;
expect(inpValue).toBeGreaterThan(0);

expect(spanEnvelopeItem).toEqual({
data: {
'sentry.exclusive_time': inpValue,
'sentry.op': 'ui.interaction.click',
'sentry.origin': 'auto.http.browser.inp',
'sentry.source': 'custom',
transaction: 'test-url',
'user_agent.original': expect.stringContaining('Chrome'),

This comment was marked as outdated.

},
measurements: {
inp: {
unit: 'millisecond',
value: inpValue,
},
},
description: 'body > nav#navigation > NavigationLink',
exclusive_time: inpValue,
op: 'ui.interaction.click',
origin: 'auto.http.browser.inp',
is_segment: true,
segment_id: spanEnvelopeItem.span_id,
span_id: expect.stringMatching(/[a-f0-9]{16}/),
start_timestamp: expect.any(Number),
timestamp: expect.any(Number),
trace_id: traceId,
});
},
);

sentryTest(
'should capture INP with unknown target name when navigation removes element from DOM',
Comment on lines +95 to +96
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ultra-l: Maybe we can also add a FIXME comment here?

async ({ browserName, getLocalTestUrl, page }) => {
if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) {
sentryTest.skip();
}

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

await page.goto(url);
await getFirstSentryEnvelopeRequest<SentryEvent>(page); // wait for page load

const spanEnvelopePromise = getMultipleSentryEnvelopeRequests<SpanEnvelope>(
page,
1,
{ envelopeType: 'span' },
properFullEnvelopeRequestParser,
);

// Simulating route change (also changing <nav> in DOM)
await page.locator('[data-test-id=nav-link-changeDOM]').click();
await page.locator('.navigated').isVisible();

await page.waitForTimeout(500);

// Page hide to trigger INP
await hidePage(page);

// Get the INP span envelope
const spanEnvelope = (await spanEnvelopePromise)[0];

const spanEnvelopeHeaders = spanEnvelope[0];
const spanEnvelopeItem = spanEnvelope[1][0][1];

const traceId = spanEnvelopeHeaders.trace!.trace_id;
expect(traceId).toMatch(/[a-f0-9]{32}/);

expect(spanEnvelopeHeaders).toEqual({
sent_at: expect.any(String),
trace: {
environment: 'production',
public_key: 'public',
sample_rate: '1',
sampled: 'true',
trace_id: traceId,
sample_rand: expect.any(String),
},
});

const inpValue = spanEnvelopeItem.measurements?.inp.value;
expect(inpValue).toBeGreaterThan(0);

expect(spanEnvelopeItem).toEqual({
data: {
'sentry.exclusive_time': inpValue,
'sentry.op': 'ui.interaction.click',
'sentry.origin': 'auto.http.browser.inp',
'sentry.source': 'custom',
transaction: 'test-url',
'user_agent.original': expect.stringContaining('Chrome'),
},
measurements: {
inp: {
unit: 'millisecond',
value: inpValue,
},
},
description: '<unknown>', // FIXME: currently unable to get the target name when element is removed from DOM
exclusive_time: inpValue,
op: 'ui.interaction.click',
origin: 'auto.http.browser.inp',
is_segment: true,
segment_id: spanEnvelopeItem.span_id,
span_id: expect.stringMatching(/[a-f0-9]{16}/),
start_timestamp: expect.any(Number),
timestamp: expect.any(Number),
trace_id: traceId,
});
},
);