Skip to content

Commit

Permalink
feat(replay): Capture slow clicks (experimental)
Browse files Browse the repository at this point in the history
  • Loading branch information
mydea committed May 9, 2023
1 parent 18bab90 commit 998906e
Show file tree
Hide file tree
Showing 9 changed files with 734 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { expect } from '@playwright/test';

import { sentryTest } from '../../../../utils/fixtures';
import { getCustomRecordingEvents, shouldSkipReplayTest, waitForReplayRequest } from '../../../../utils/replayHelpers';

sentryTest('click is ignored on div', async ({ getLocalTestUrl, page }) => {
if (shouldSkipReplayTest()) {
sentryTest.skip();
}

const reqPromise0 = waitForReplayRequest(page, 0);

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 getLocalTestUrl({ testDir: __dirname });

await page.goto(url);
await reqPromise0;

const reqPromise1 = waitForReplayRequest(page);

await page.click('#mutationDiv');

const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1);

expect(breadcrumbs).toEqual([
{
category: 'ui.click',
data: {
node: {
attributes: {
id: 'mutationDiv',
},
id: expect.any(Number),
tagName: 'div',
textContent: '******* ********',
},
nodeId: expect.any(Number),
},
message: 'body > div#mutationDiv',
timestamp: expect.any(Number),
type: 'default',
},
]);
});

sentryTest('click is ignored on ignoreSelectors', async ({ getLocalTestUrl, page }) => {
if (shouldSkipReplayTest()) {
sentryTest.skip();
}

const reqPromise0 = waitForReplayRequest(page, 0);

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 getLocalTestUrl({ testDir: __dirname });

await page.goto(url);
await reqPromise0;

const reqPromise1 = waitForReplayRequest(page);

await page.click('#mutationIgnoreButton');

const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1);

expect(breadcrumbs).toEqual([
{
category: 'ui.click',
data: {
node: {
attributes: {
class: 'ignore-class',
id: 'mutationIgnoreButton',
},
id: expect.any(Number),
tagName: 'button',
textContent: '******* ****** ****',
},
nodeId: expect.any(Number),
},
message: 'body > button#mutationIgnoreButton.ignore-class',
timestamp: expect.any(Number),
type: 'default',
},
]);
});
24 changes: 24 additions & 0 deletions packages/browser-integration-tests/suites/replay/slowClick/init.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;
window.Replay = new Sentry.Replay({
flushMinDelay: 500,
flushMaxDelay: 500,
_experiments: {
slowClicks: {
threshold: 300,
scrollThreshold: 300,
timeout: 2000,
ignoreSelectors: ['.ignore-class', '[ignore-attribute]'],
},
},
});

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
sampleRate: 0,
replaysSessionSampleRate: 1.0,
replaysOnErrorSampleRate: 0.0,

integrations: [window.Replay],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { expect } from '@playwright/test';

import { sentryTest } from '../../../../utils/fixtures';
import { getCustomRecordingEvents, shouldSkipReplayTest, waitForReplayRequest } from '../../../../utils/replayHelpers';

sentryTest('mutation after threshold results in slow click', async ({ getLocalTestUrl, page }) => {
if (shouldSkipReplayTest()) {
sentryTest.skip();
}

const reqPromise0 = waitForReplayRequest(page, 0);

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 getLocalTestUrl({ testDir: __dirname });

await page.goto(url);
await reqPromise0;

const reqPromise1 = waitForReplayRequest(page, (event, res) => {
const { breadcrumbs } = getCustomRecordingEvents(res);

return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.slowClickDetected');
});

await page.click('#mutationButton');

const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1);

const slowClickBreadcrumbs = breadcrumbs.filter(breadcrumb => breadcrumb.category === 'ui.slowClickDetected');

expect(slowClickBreadcrumbs).toEqual([
{
category: 'ui.slowClickDetected',
data: {
endReason: 'mutation',
node: {
attributes: {
id: 'mutationButton',
},
id: expect.any(Number),
tagName: 'button',
textContent: '******* ********',
},
nodeId: expect.any(Number),
timeAfterClickMs: expect.any(Number),
url: 'http://sentry-test.io/index.html',
},
message: 'body > button#mutationButton',
timestamp: expect.any(Number),
},
]);

expect(slowClickBreadcrumbs[0]?.data?.timeAfterClickMs).toBeGreaterThan(300);
expect(slowClickBreadcrumbs[0]?.data?.timeAfterClickMs).toBeLessThan(2000);
});

sentryTest('immediate mutation does not trigger slow click', async ({ getLocalTestUrl, page }) => {
if (shouldSkipReplayTest()) {
sentryTest.skip();
}

const reqPromise0 = waitForReplayRequest(page, 0);

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 getLocalTestUrl({ testDir: __dirname });

await page.goto(url);
await reqPromise0;

const reqPromise1 = waitForReplayRequest(page);

await page.click('#mutationButtonImmediately');

const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1);

expect(breadcrumbs).toEqual([
{
category: 'ui.click',
data: {
node: {
attributes: {
id: 'mutationButtonImmediately',
},
id: expect.any(Number),
tagName: 'button',
textContent: '******* ******** ***********',
},
nodeId: expect.any(Number),
},
message: 'body > button#mutationButtonImmediately',
timestamp: expect.any(Number),
type: 'default',
},
]);
});

sentryTest('inline click handler does not trigger slow click', async ({ getLocalTestUrl, page }) => {
if (shouldSkipReplayTest()) {
sentryTest.skip();
}

const reqPromise0 = waitForReplayRequest(page, 0);

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 getLocalTestUrl({ testDir: __dirname });

await page.goto(url);
await reqPromise0;

const reqPromise1 = waitForReplayRequest(page);

await page.click('#mutationButtonInline');

const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1);

expect(breadcrumbs).toEqual([
{
category: 'ui.click',
data: {
node: {
attributes: {
id: 'mutationButtonInline',
},
id: expect.any(Number),
tagName: 'button',
textContent: '******* ******** ***********',
},
nodeId: expect.any(Number),
},
message: 'body > button#mutationButtonInline',
timestamp: expect.any(Number),
type: 'default',
},
]);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { expect } from '@playwright/test';

import { sentryTest } from '../../../../utils/fixtures';
import { getCustomRecordingEvents, shouldSkipReplayTest, waitForReplayRequest } from '../../../../utils/replayHelpers';

sentryTest('immediate scroll does not trigger slow click', async ({ getLocalTestUrl, page }) => {
if (shouldSkipReplayTest()) {
sentryTest.skip();
}

const reqPromise0 = waitForReplayRequest(page, 0);

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 getLocalTestUrl({ testDir: __dirname });

await page.goto(url);
await reqPromise0;

const reqPromise1 = waitForReplayRequest(page);

await page.click('#scrollButton');

const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1);

expect(breadcrumbs).toEqual([
{
category: 'ui.click',
data: {
node: {
attributes: {
id: 'scrollButton',
},
id: expect.any(Number),
tagName: 'button',
textContent: '******* ******',
},
nodeId: expect.any(Number),
},
message: 'body > button#scrollButton',
timestamp: expect.any(Number),
type: 'default',
},
]);
});

sentryTest('late scroll triggers slow click', async ({ getLocalTestUrl, page }) => {
if (shouldSkipReplayTest()) {
sentryTest.skip();
}

const reqPromise0 = waitForReplayRequest(page, 0);

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 getLocalTestUrl({ testDir: __dirname });

await page.goto(url);
await reqPromise0;

const reqPromise1 = waitForReplayRequest(page, (event, res) => {
const { breadcrumbs } = getCustomRecordingEvents(res);

return breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.slowClickDetected');
});

await page.click('#scrollLateButton');

const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1);

const slowClickBreadcrumbs = breadcrumbs.filter(breadcrumb => breadcrumb.category === 'ui.slowClickDetected');

expect(slowClickBreadcrumbs).toEqual([
{
category: 'ui.slowClickDetected',
data: {
endReason: 'timeout',
node: {
attributes: {
id: 'scrollLateButton',
},
id: expect.any(Number),
tagName: 'button',
textContent: '******* ****** ****',
},
nodeId: expect.any(Number),
timeAfterClickMs: expect.any(Number),
url: 'http://sentry-test.io/index.html',
},
message: 'body > button#scrollLateButton',
timestamp: expect.any(Number),
},
]);
});

0 comments on commit 998906e

Please sign in to comment.