Skip to content

Commit f729509

Browse files
authored
test(browser): Add test for INP target name after navigation or DOM changes (#18033)
This test displays the current behavior of getting the element target name for INP when the DOM changes after clicking on a navigation link. When the DOM changes after clicking on an element, the element name from before the navigation is not captured: ```js description: '<unknown>', // FIXME: currently unable to get the target name when element is removed from DOM ```
1 parent 993303c commit f729509

File tree

4 files changed

+261
-0
lines changed

4 files changed

+261
-0
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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+
instrumentPageLoad: false,
13+
instrumentNavigation: false,
14+
}),
15+
],
16+
tracesSampleRate: 1,
17+
});
18+
19+
const client = Sentry.getClient();
20+
21+
// Force page load transaction name to a testable value
22+
Sentry.startBrowserTracingPageLoadSpan(client, {
23+
name: 'test-url',
24+
attributes: {
25+
[Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
26+
},
27+
});
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
const simulateNavigationKeepDOM = e => {
2+
const startTime = Date.now();
3+
4+
function getElapsed() {
5+
const time = Date.now();
6+
return time - startTime;
7+
}
8+
9+
while (getElapsed() < 100) {
10+
// Block UI for 100ms to simulate some processing work during navigation
11+
}
12+
13+
const contentDiv = document.getElementById('content');
14+
contentDiv.innerHTML = '<h1>Page 1</h1><p>Successfully navigated!</p>';
15+
16+
contentDiv.classList.add('navigated');
17+
};
18+
19+
const simulateNavigationChangeDOM = e => {
20+
const startTime = Date.now();
21+
22+
function getElapsed() {
23+
const time = Date.now();
24+
return time - startTime;
25+
}
26+
27+
while (getElapsed() < 100) {
28+
// Block UI for 100ms to simulate some processing work during navigation
29+
}
30+
31+
const navigationHTML =
32+
' <nav id="navigation">\n' +
33+
' <a href="#page1" data-test-id="nav-link-keepDOM" data-sentry-element="NavigationLink">Go to Page 1</a>\n' +
34+
' <a href="#page2" data-test-id="nav-link-changeDOM" data-sentry-element="NavigationLink">Go to Page 2</a>\n' +
35+
' </nav>';
36+
37+
const body = document.querySelector('body');
38+
body.innerHTML = `${navigationHTML}<div id="content"><h1>Page 2</h1><p>Successfully navigated!</p></div>`;
39+
40+
body.classList.add('navigated');
41+
};
42+
43+
document.querySelector('[data-test-id=nav-link-keepDOM]').addEventListener('click', simulateNavigationKeepDOM);
44+
document.querySelector('[data-test-id=nav-link-changeDOM]').addEventListener('click', simulateNavigationChangeDOM);
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
</head>
6+
<body>
7+
<nav id="navigation">
8+
<a href="#page1" data-test-id="nav-link-keepDOM" data-sentry-element="NavigationLink">Go to Page 1</a>
9+
<a href="#page2" data-test-id="nav-link-changeDOM" data-sentry-element="NavigationLink">Go to Page 2</a>
10+
</nav>
11+
<div id="content">
12+
<h1>Home Page</h1>
13+
<p>Click the navigation link to simulate a route change</p>
14+
</div>
15+
</body>
16+
</html>
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import { expect } from '@playwright/test';
2+
import type { Event as SentryEvent, SpanEnvelope } from '@sentry/core';
3+
import { sentryTest } from '../../../../utils/fixtures';
4+
import {
5+
getFirstSentryEnvelopeRequest,
6+
getMultipleSentryEnvelopeRequests,
7+
hidePage,
8+
properFullEnvelopeRequestParser,
9+
shouldSkipTracingTest,
10+
} from '../../../../utils/helpers';
11+
12+
const supportedBrowsers = ['chromium'];
13+
14+
sentryTest(
15+
'should capture INP with correct target name when navigation keeps DOM element',
16+
async ({ browserName, getLocalTestUrl, page }) => {
17+
if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) {
18+
sentryTest.skip();
19+
}
20+
21+
const url = await getLocalTestUrl({ testDir: __dirname });
22+
23+
await page.goto(url);
24+
await getFirstSentryEnvelopeRequest<SentryEvent>(page); // wait for page load
25+
26+
const spanEnvelopePromise = getMultipleSentryEnvelopeRequests<SpanEnvelope>(
27+
page,
28+
1,
29+
{ envelopeType: 'span' },
30+
properFullEnvelopeRequestParser,
31+
);
32+
33+
// Simulating route change (keeping <nav> in DOM)
34+
await page.locator('[data-test-id=nav-link-keepDOM]').click();
35+
await page.locator('.navigated').isVisible();
36+
37+
await page.waitForTimeout(500);
38+
39+
// Page hide to trigger INP
40+
await hidePage(page);
41+
42+
// Get the INP span envelope
43+
const spanEnvelope = (await spanEnvelopePromise)[0];
44+
45+
const spanEnvelopeHeaders = spanEnvelope[0];
46+
const spanEnvelopeItem = spanEnvelope[1][0][1];
47+
48+
const traceId = spanEnvelopeHeaders.trace!.trace_id;
49+
expect(traceId).toMatch(/[a-f0-9]{32}/);
50+
51+
expect(spanEnvelopeHeaders).toEqual({
52+
sent_at: expect.any(String),
53+
trace: {
54+
environment: 'production',
55+
public_key: 'public',
56+
sample_rate: '1',
57+
sampled: 'true',
58+
trace_id: traceId,
59+
sample_rand: expect.any(String),
60+
},
61+
});
62+
63+
const inpValue = spanEnvelopeItem.measurements?.inp.value;
64+
expect(inpValue).toBeGreaterThan(0);
65+
66+
expect(spanEnvelopeItem).toEqual({
67+
data: {
68+
'sentry.exclusive_time': inpValue,
69+
'sentry.op': 'ui.interaction.click',
70+
'sentry.origin': 'auto.http.browser.inp',
71+
'sentry.source': 'custom',
72+
transaction: 'test-url',
73+
'user_agent.original': expect.stringContaining('Chrome'),
74+
},
75+
measurements: {
76+
inp: {
77+
unit: 'millisecond',
78+
value: inpValue,
79+
},
80+
},
81+
description: 'body > nav#navigation > NavigationLink',
82+
exclusive_time: inpValue,
83+
op: 'ui.interaction.click',
84+
origin: 'auto.http.browser.inp',
85+
is_segment: true,
86+
segment_id: spanEnvelopeItem.span_id,
87+
span_id: expect.stringMatching(/[a-f0-9]{16}/),
88+
start_timestamp: expect.any(Number),
89+
timestamp: expect.any(Number),
90+
trace_id: traceId,
91+
});
92+
},
93+
);
94+
95+
sentryTest(
96+
'should capture INP with unknown target name when navigation removes element from DOM',
97+
async ({ browserName, getLocalTestUrl, page }) => {
98+
if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) {
99+
sentryTest.skip();
100+
}
101+
102+
const url = await getLocalTestUrl({ testDir: __dirname });
103+
104+
await page.goto(url);
105+
await getFirstSentryEnvelopeRequest<SentryEvent>(page); // wait for page load
106+
107+
const spanEnvelopePromise = getMultipleSentryEnvelopeRequests<SpanEnvelope>(
108+
page,
109+
1,
110+
{ envelopeType: 'span' },
111+
properFullEnvelopeRequestParser,
112+
);
113+
114+
// Simulating route change (also changing <nav> in DOM)
115+
await page.locator('[data-test-id=nav-link-changeDOM]').click();
116+
await page.locator('.navigated').isVisible();
117+
118+
await page.waitForTimeout(500);
119+
120+
// Page hide to trigger INP
121+
await hidePage(page);
122+
123+
// Get the INP span envelope
124+
const spanEnvelope = (await spanEnvelopePromise)[0];
125+
126+
const spanEnvelopeHeaders = spanEnvelope[0];
127+
const spanEnvelopeItem = spanEnvelope[1][0][1];
128+
129+
const traceId = spanEnvelopeHeaders.trace!.trace_id;
130+
expect(traceId).toMatch(/[a-f0-9]{32}/);
131+
132+
expect(spanEnvelopeHeaders).toEqual({
133+
sent_at: expect.any(String),
134+
trace: {
135+
environment: 'production',
136+
public_key: 'public',
137+
sample_rate: '1',
138+
sampled: 'true',
139+
trace_id: traceId,
140+
sample_rand: expect.any(String),
141+
},
142+
});
143+
144+
const inpValue = spanEnvelopeItem.measurements?.inp.value;
145+
expect(inpValue).toBeGreaterThan(0);
146+
147+
expect(spanEnvelopeItem).toEqual({
148+
data: {
149+
'sentry.exclusive_time': inpValue,
150+
'sentry.op': 'ui.interaction.click',
151+
'sentry.origin': 'auto.http.browser.inp',
152+
'sentry.source': 'custom',
153+
transaction: 'test-url',
154+
'user_agent.original': expect.stringContaining('Chrome'),
155+
},
156+
measurements: {
157+
inp: {
158+
unit: 'millisecond',
159+
value: inpValue,
160+
},
161+
},
162+
description: '<unknown>', // FIXME: currently unable to get the target name when element is removed from DOM
163+
exclusive_time: inpValue,
164+
op: 'ui.interaction.click',
165+
origin: 'auto.http.browser.inp',
166+
is_segment: true,
167+
segment_id: spanEnvelopeItem.span_id,
168+
span_id: expect.stringMatching(/[a-f0-9]{16}/),
169+
start_timestamp: expect.any(Number),
170+
timestamp: expect.any(Number),
171+
trace_id: traceId,
172+
});
173+
},
174+
);

0 commit comments

Comments
 (0)