Skip to content

Commit 998906e

Browse files
committed
feat(replay): Capture slow clicks (experimental)
1 parent 18bab90 commit 998906e

File tree

9 files changed

+734
-4
lines changed

9 files changed

+734
-4
lines changed
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { expect } from '@playwright/test';
2+
3+
import { sentryTest } from '../../../../utils/fixtures';
4+
import { getCustomRecordingEvents, shouldSkipReplayTest, waitForReplayRequest } from '../../../../utils/replayHelpers';
5+
6+
sentryTest('click is ignored on div', async ({ getLocalTestUrl, page }) => {
7+
if (shouldSkipReplayTest()) {
8+
sentryTest.skip();
9+
}
10+
11+
const reqPromise0 = waitForReplayRequest(page, 0);
12+
13+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
14+
return route.fulfill({
15+
status: 200,
16+
contentType: 'application/json',
17+
body: JSON.stringify({ id: 'test-id' }),
18+
});
19+
});
20+
21+
const url = await getLocalTestUrl({ testDir: __dirname });
22+
23+
await page.goto(url);
24+
await reqPromise0;
25+
26+
const reqPromise1 = waitForReplayRequest(page);
27+
28+
await page.click('#mutationDiv');
29+
30+
const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1);
31+
32+
expect(breadcrumbs).toEqual([
33+
{
34+
category: 'ui.click',
35+
data: {
36+
node: {
37+
attributes: {
38+
id: 'mutationDiv',
39+
},
40+
id: expect.any(Number),
41+
tagName: 'div',
42+
textContent: '******* ********',
43+
},
44+
nodeId: expect.any(Number),
45+
},
46+
message: 'body > div#mutationDiv',
47+
timestamp: expect.any(Number),
48+
type: 'default',
49+
},
50+
]);
51+
});
52+
53+
sentryTest('click is ignored on ignoreSelectors', async ({ getLocalTestUrl, page }) => {
54+
if (shouldSkipReplayTest()) {
55+
sentryTest.skip();
56+
}
57+
58+
const reqPromise0 = waitForReplayRequest(page, 0);
59+
60+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
61+
return route.fulfill({
62+
status: 200,
63+
contentType: 'application/json',
64+
body: JSON.stringify({ id: 'test-id' }),
65+
});
66+
});
67+
68+
const url = await getLocalTestUrl({ testDir: __dirname });
69+
70+
await page.goto(url);
71+
await reqPromise0;
72+
73+
const reqPromise1 = waitForReplayRequest(page);
74+
75+
await page.click('#mutationIgnoreButton');
76+
77+
const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1);
78+
79+
expect(breadcrumbs).toEqual([
80+
{
81+
category: 'ui.click',
82+
data: {
83+
node: {
84+
attributes: {
85+
class: 'ignore-class',
86+
id: 'mutationIgnoreButton',
87+
},
88+
id: expect.any(Number),
89+
tagName: 'button',
90+
textContent: '******* ****** ****',
91+
},
92+
nodeId: expect.any(Number),
93+
},
94+
message: 'body > button#mutationIgnoreButton.ignore-class',
95+
timestamp: expect.any(Number),
96+
type: 'default',
97+
},
98+
]);
99+
});
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
window.Replay = new Sentry.Replay({
5+
flushMinDelay: 500,
6+
flushMaxDelay: 500,
7+
_experiments: {
8+
slowClicks: {
9+
threshold: 300,
10+
scrollThreshold: 300,
11+
timeout: 2000,
12+
ignoreSelectors: ['.ignore-class', '[ignore-attribute]'],
13+
},
14+
},
15+
});
16+
17+
Sentry.init({
18+
dsn: 'https://[email protected]/1337',
19+
sampleRate: 0,
20+
replaysSessionSampleRate: 1.0,
21+
replaysOnErrorSampleRate: 0.0,
22+
23+
integrations: [window.Replay],
24+
});
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { expect } from '@playwright/test';
2+
3+
import { sentryTest } from '../../../../utils/fixtures';
4+
import { getCustomRecordingEvents, shouldSkipReplayTest, waitForReplayRequest } from '../../../../utils/replayHelpers';
5+
6+
sentryTest('mutation after threshold results in slow click', async ({ getLocalTestUrl, page }) => {
7+
if (shouldSkipReplayTest()) {
8+
sentryTest.skip();
9+
}
10+
11+
const reqPromise0 = waitForReplayRequest(page, 0);
12+
13+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
14+
return route.fulfill({
15+
status: 200,
16+
contentType: 'application/json',
17+
body: JSON.stringify({ id: 'test-id' }),
18+
});
19+
});
20+
21+
const url = await getLocalTestUrl({ testDir: __dirname });
22+
23+
await page.goto(url);
24+
await reqPromise0;
25+
26+
const reqPromise1 = waitForReplayRequest(page, (event, res) => {
27+
const { breadcrumbs } = getCustomRecordingEvents(res);
28+
29+
return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.slowClickDetected');
30+
});
31+
32+
await page.click('#mutationButton');
33+
34+
const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1);
35+
36+
const slowClickBreadcrumbs = breadcrumbs.filter(breadcrumb => breadcrumb.category === 'ui.slowClickDetected');
37+
38+
expect(slowClickBreadcrumbs).toEqual([
39+
{
40+
category: 'ui.slowClickDetected',
41+
data: {
42+
endReason: 'mutation',
43+
node: {
44+
attributes: {
45+
id: 'mutationButton',
46+
},
47+
id: expect.any(Number),
48+
tagName: 'button',
49+
textContent: '******* ********',
50+
},
51+
nodeId: expect.any(Number),
52+
timeAfterClickMs: expect.any(Number),
53+
url: 'http://sentry-test.io/index.html',
54+
},
55+
message: 'body > button#mutationButton',
56+
timestamp: expect.any(Number),
57+
},
58+
]);
59+
60+
expect(slowClickBreadcrumbs[0]?.data?.timeAfterClickMs).toBeGreaterThan(300);
61+
expect(slowClickBreadcrumbs[0]?.data?.timeAfterClickMs).toBeLessThan(2000);
62+
});
63+
64+
sentryTest('immediate mutation does not trigger slow click', async ({ getLocalTestUrl, page }) => {
65+
if (shouldSkipReplayTest()) {
66+
sentryTest.skip();
67+
}
68+
69+
const reqPromise0 = waitForReplayRequest(page, 0);
70+
71+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
72+
return route.fulfill({
73+
status: 200,
74+
contentType: 'application/json',
75+
body: JSON.stringify({ id: 'test-id' }),
76+
});
77+
});
78+
79+
const url = await getLocalTestUrl({ testDir: __dirname });
80+
81+
await page.goto(url);
82+
await reqPromise0;
83+
84+
const reqPromise1 = waitForReplayRequest(page);
85+
86+
await page.click('#mutationButtonImmediately');
87+
88+
const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1);
89+
90+
expect(breadcrumbs).toEqual([
91+
{
92+
category: 'ui.click',
93+
data: {
94+
node: {
95+
attributes: {
96+
id: 'mutationButtonImmediately',
97+
},
98+
id: expect.any(Number),
99+
tagName: 'button',
100+
textContent: '******* ******** ***********',
101+
},
102+
nodeId: expect.any(Number),
103+
},
104+
message: 'body > button#mutationButtonImmediately',
105+
timestamp: expect.any(Number),
106+
type: 'default',
107+
},
108+
]);
109+
});
110+
111+
sentryTest('inline click handler does not trigger slow click', async ({ getLocalTestUrl, page }) => {
112+
if (shouldSkipReplayTest()) {
113+
sentryTest.skip();
114+
}
115+
116+
const reqPromise0 = waitForReplayRequest(page, 0);
117+
118+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
119+
return route.fulfill({
120+
status: 200,
121+
contentType: 'application/json',
122+
body: JSON.stringify({ id: 'test-id' }),
123+
});
124+
});
125+
126+
const url = await getLocalTestUrl({ testDir: __dirname });
127+
128+
await page.goto(url);
129+
await reqPromise0;
130+
131+
const reqPromise1 = waitForReplayRequest(page);
132+
133+
await page.click('#mutationButtonInline');
134+
135+
const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1);
136+
137+
expect(breadcrumbs).toEqual([
138+
{
139+
category: 'ui.click',
140+
data: {
141+
node: {
142+
attributes: {
143+
id: 'mutationButtonInline',
144+
},
145+
id: expect.any(Number),
146+
tagName: 'button',
147+
textContent: '******* ******** ***********',
148+
},
149+
nodeId: expect.any(Number),
150+
},
151+
message: 'body > button#mutationButtonInline',
152+
timestamp: expect.any(Number),
153+
type: 'default',
154+
},
155+
]);
156+
});
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { expect } from '@playwright/test';
2+
3+
import { sentryTest } from '../../../../utils/fixtures';
4+
import { getCustomRecordingEvents, shouldSkipReplayTest, waitForReplayRequest } from '../../../../utils/replayHelpers';
5+
6+
sentryTest('immediate scroll does not trigger slow click', async ({ getLocalTestUrl, page }) => {
7+
if (shouldSkipReplayTest()) {
8+
sentryTest.skip();
9+
}
10+
11+
const reqPromise0 = waitForReplayRequest(page, 0);
12+
13+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
14+
return route.fulfill({
15+
status: 200,
16+
contentType: 'application/json',
17+
body: JSON.stringify({ id: 'test-id' }),
18+
});
19+
});
20+
21+
const url = await getLocalTestUrl({ testDir: __dirname });
22+
23+
await page.goto(url);
24+
await reqPromise0;
25+
26+
const reqPromise1 = waitForReplayRequest(page);
27+
28+
await page.click('#scrollButton');
29+
30+
const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1);
31+
32+
expect(breadcrumbs).toEqual([
33+
{
34+
category: 'ui.click',
35+
data: {
36+
node: {
37+
attributes: {
38+
id: 'scrollButton',
39+
},
40+
id: expect.any(Number),
41+
tagName: 'button',
42+
textContent: '******* ******',
43+
},
44+
nodeId: expect.any(Number),
45+
},
46+
message: 'body > button#scrollButton',
47+
timestamp: expect.any(Number),
48+
type: 'default',
49+
},
50+
]);
51+
});
52+
53+
sentryTest('late scroll triggers slow click', async ({ getLocalTestUrl, page }) => {
54+
if (shouldSkipReplayTest()) {
55+
sentryTest.skip();
56+
}
57+
58+
const reqPromise0 = waitForReplayRequest(page, 0);
59+
60+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
61+
return route.fulfill({
62+
status: 200,
63+
contentType: 'application/json',
64+
body: JSON.stringify({ id: 'test-id' }),
65+
});
66+
});
67+
68+
const url = await getLocalTestUrl({ testDir: __dirname });
69+
70+
await page.goto(url);
71+
await reqPromise0;
72+
73+
const reqPromise1 = waitForReplayRequest(page, (event, res) => {
74+
const { breadcrumbs } = getCustomRecordingEvents(res);
75+
76+
return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.slowClickDetected');
77+
});
78+
79+
await page.click('#scrollLateButton');
80+
81+
const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1);
82+
83+
const slowClickBreadcrumbs = breadcrumbs.filter(breadcrumb => breadcrumb.category === 'ui.slowClickDetected');
84+
85+
expect(slowClickBreadcrumbs).toEqual([
86+
{
87+
category: 'ui.slowClickDetected',
88+
data: {
89+
endReason: 'timeout',
90+
node: {
91+
attributes: {
92+
id: 'scrollLateButton',
93+
},
94+
id: expect.any(Number),
95+
tagName: 'button',
96+
textContent: '******* ****** ****',
97+
},
98+
nodeId: expect.any(Number),
99+
timeAfterClickMs: expect.any(Number),
100+
url: 'http://sentry-test.io/index.html',
101+
},
102+
message: 'body > button#scrollLateButton',
103+
timestamp: expect.any(Number),
104+
},
105+
]);
106+
});

0 commit comments

Comments
 (0)