Skip to content

Commit 231a5f0

Browse files
committed
feat(replay): Capture slow clicks (experimental)
1 parent 48ef411 commit 231a5f0

File tree

10 files changed

+928
-4
lines changed

10 files changed

+928
-4
lines changed
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { expect } from '@playwright/test';
2+
3+
import { sentryTest } from '../../../../utils/fixtures';
4+
import { getCustomRecordingEvents, shouldSkipReplayTest, waitForReplayRequest } from '../../../../utils/replayHelpers';
5+
6+
[
7+
{
8+
id: 'link',
9+
slowClick: true,
10+
},
11+
{
12+
id: 'linkExternal',
13+
slowClick: false,
14+
},
15+
{
16+
id: 'linkDownload',
17+
slowClick: false,
18+
},
19+
{
20+
id: 'inputButton',
21+
slowClick: true,
22+
},
23+
{
24+
id: 'inputSubmit',
25+
slowClick: true,
26+
},
27+
{
28+
id: 'inputText',
29+
slowClick: false,
30+
},
31+
].forEach(({ id, slowClick }) => {
32+
if (slowClick) {
33+
sentryTest(`slow click is captured for ${id}`, async ({ getLocalTestUrl, page }) => {
34+
if (shouldSkipReplayTest()) {
35+
sentryTest.skip();
36+
}
37+
38+
const reqPromise0 = waitForReplayRequest(page, 0);
39+
40+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
41+
return route.fulfill({
42+
status: 200,
43+
contentType: 'application/json',
44+
body: JSON.stringify({ id: 'test-id' }),
45+
});
46+
});
47+
48+
const url = await getLocalTestUrl({ testDir: __dirname });
49+
50+
await page.goto(url);
51+
await reqPromise0;
52+
53+
const reqPromise1 = waitForReplayRequest(page, (event, res) => {
54+
const { breadcrumbs } = getCustomRecordingEvents(res);
55+
56+
return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.slowClickDetected');
57+
});
58+
59+
await page.click(`#${id}`);
60+
61+
const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1);
62+
63+
const slowClickBreadcrumbs = breadcrumbs.filter(breadcrumb => breadcrumb.category === 'ui.slowClickDetected');
64+
65+
expect(slowClickBreadcrumbs).toEqual([
66+
{
67+
category: 'ui.slowClickDetected',
68+
data: {
69+
endReason: 'timeout',
70+
node: {
71+
attributes: expect.objectContaining({
72+
id,
73+
}),
74+
id: expect.any(Number),
75+
tagName: expect.any(String),
76+
textContent: expect.any(String),
77+
},
78+
nodeId: expect.any(Number),
79+
timeAfterClickMs: expect.any(Number),
80+
url: expect.any(String),
81+
},
82+
message: expect.any(String),
83+
timestamp: expect.any(Number),
84+
},
85+
]);
86+
});
87+
} else {
88+
sentryTest(`slow click is not captured for ${id}`, async ({ getLocalTestUrl, page }) => {
89+
if (shouldSkipReplayTest()) {
90+
sentryTest.skip();
91+
}
92+
93+
const reqPromise0 = waitForReplayRequest(page, 0);
94+
95+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
96+
return route.fulfill({
97+
status: 200,
98+
contentType: 'application/json',
99+
body: JSON.stringify({ id: 'test-id' }),
100+
});
101+
});
102+
103+
const url = await getLocalTestUrl({ testDir: __dirname });
104+
105+
await page.goto(url);
106+
await reqPromise0;
107+
108+
const reqPromise1 = waitForReplayRequest(page, (event, res) => {
109+
const { breadcrumbs } = getCustomRecordingEvents(res);
110+
111+
return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.click');
112+
});
113+
114+
await page.click(`#${id}`);
115+
116+
const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1);
117+
118+
expect(breadcrumbs).toEqual([
119+
{
120+
category: 'ui.click',
121+
data: {
122+
node: {
123+
attributes: expect.objectContaining({
124+
id,
125+
}),
126+
id: expect.any(Number),
127+
tagName: expect.any(String),
128+
textContent: expect.any(String),
129+
},
130+
nodeId: expect.any(Number),
131+
},
132+
message: expect.any(String),
133+
timestamp: expect.any(Number),
134+
type: 'default',
135+
},
136+
]);
137+
});
138+
}
139+
});
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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, (event, res) => {
27+
const { breadcrumbs } = getCustomRecordingEvents(res);
28+
29+
return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.click');
30+
});
31+
32+
await page.click('#mutationDiv');
33+
34+
const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1);
35+
36+
expect(breadcrumbs).toEqual([
37+
{
38+
category: 'ui.click',
39+
data: {
40+
node: {
41+
attributes: {
42+
id: 'mutationDiv',
43+
},
44+
id: expect.any(Number),
45+
tagName: 'div',
46+
textContent: '******* ********',
47+
},
48+
nodeId: expect.any(Number),
49+
},
50+
message: 'body > div#mutationDiv',
51+
timestamp: expect.any(Number),
52+
type: 'default',
53+
},
54+
]);
55+
});
56+
57+
sentryTest('click is ignored on ignoreSelectors', async ({ getLocalTestUrl, page }) => {
58+
if (shouldSkipReplayTest()) {
59+
sentryTest.skip();
60+
}
61+
62+
const reqPromise0 = waitForReplayRequest(page, 0);
63+
64+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
65+
return route.fulfill({
66+
status: 200,
67+
contentType: 'application/json',
68+
body: JSON.stringify({ id: 'test-id' }),
69+
});
70+
});
71+
72+
const url = await getLocalTestUrl({ testDir: __dirname });
73+
74+
await page.goto(url);
75+
await reqPromise0;
76+
77+
const reqPromise1 = waitForReplayRequest(page, (event, res) => {
78+
const { breadcrumbs } = getCustomRecordingEvents(res);
79+
80+
return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.click');
81+
});
82+
83+
await page.click('#mutationIgnoreButton');
84+
85+
const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1);
86+
87+
expect(breadcrumbs).toEqual([
88+
{
89+
category: 'ui.click',
90+
data: {
91+
node: {
92+
attributes: {
93+
class: 'ignore-class',
94+
id: 'mutationIgnoreButton',
95+
},
96+
id: expect.any(Number),
97+
tagName: 'button',
98+
textContent: '******* ****** ****',
99+
},
100+
nodeId: expect.any(Number),
101+
},
102+
message: 'body > button#mutationIgnoreButton.ignore-class',
103+
timestamp: expect.any(Number),
104+
type: 'default',
105+
},
106+
]);
107+
});
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+
});

0 commit comments

Comments
 (0)