Skip to content

Commit d47112b

Browse files
committed
WIP: capture slow click
1 parent 194130e commit d47112b

File tree

2 files changed

+102
-0
lines changed

2 files changed

+102
-0
lines changed

packages/replay/src/coreHandlers/handleDom.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { htmlTreeAsString } from '@sentry/utils';
55

66
import type { ReplayContainer } from '../types';
77
import { createBreadcrumb } from '../util/createBreadcrumb';
8+
import { detectSlowClick } from './handleSlowClick';
89
import { addBreadcrumbEvent } from './util/addBreadcrumbEvent';
910
import { getAttributesToRecord } from './util/getAttributesToRecord';
1011

@@ -26,6 +27,11 @@ export const handleDomListener: (replay: ReplayContainer) => (handlerData: DomHa
2627
return;
2728
}
2829

30+
const isClick = handlerData.name === 'click';
31+
if (isClick) {
32+
detectSlowClick(replay, result as Breadcrumb & { timestamp: number }, getTargetNode(handlerData) as HTMLElement);
33+
}
34+
2935
addBreadcrumbEvent(replay, result);
3036
};
3137

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import type { Breadcrumb } from '@sentry/types';
2+
3+
import { WINDOW } from '../constants';
4+
import type { ReplayContainer } from '../types';
5+
import { addBreadcrumbEvent } from './util/addBreadcrumbEvent';
6+
7+
const SLOW_CLICK_THRESHOLD = 500; // in ms
8+
const SLOW_CLICK_TIMEOUT = 5_000; // in ms
9+
const SLOW_CLICK_DISABLE_SELECTOR = '[data-sentry-disable-slowclick],.data-sentry-disable-slowclick';
10+
11+
type ClickBreadcrumb = Breadcrumb & {
12+
timestamp: number;
13+
};
14+
15+
/**
16+
* Detect a slow click on a button/a tag,
17+
* and potentially create a corresponding breadcrumb.
18+
*/
19+
export function detectSlowClick(replay: ReplayContainer, clickBreadcrumb: ClickBreadcrumb, node: HTMLElement): void {
20+
if (
21+
(node.tagName !== 'BUTTON' && node.tagName !== 'A') ||
22+
// TODO FN: make this configurable?
23+
node.matches(SLOW_CLICK_DISABLE_SELECTOR)
24+
) {
25+
return;
26+
}
27+
28+
/*
29+
We consider a slow click a click on a button/a, which does not trigger one of:
30+
- DOM mutation
31+
- URL change
32+
- Scroll
33+
Within 500ms.
34+
*/
35+
36+
const _cleanup: (() => void)[] = [];
37+
38+
function cleanup(): void {
39+
_cleanup.forEach(fn => fn());
40+
}
41+
42+
// After timeout time, def. consider this a slow click, and stop watching for mutations
43+
const timeout = setTimeout(() => {
44+
handleSlowClick(replay, clickBreadcrumb);
45+
cleanup();
46+
}, SLOW_CLICK_TIMEOUT);
47+
48+
const handler = (): void => {
49+
maybeHandleSlowClick(replay, clickBreadcrumb);
50+
cleanup();
51+
};
52+
53+
const obs = new MutationObserver(handler);
54+
55+
obs.observe(WINDOW.document.documentElement, {
56+
attributes: true,
57+
characterData: true,
58+
childList: true,
59+
subtree: true,
60+
});
61+
62+
WINDOW.addEventListener('scroll', handler);
63+
WINDOW.addEventListener('popstate', handler);
64+
65+
_cleanup.push(() => clearTimeout(timeout));
66+
_cleanup.push(() => obs.disconnect());
67+
_cleanup.push(() => WINDOW.removeEventListener('scroll', handler));
68+
_cleanup.push(() => WINDOW.removeEventListener('popstate', handler));
69+
}
70+
71+
function maybeHandleSlowClick(replay: ReplayContainer, clickBreadcrumb: ClickBreadcrumb): void {
72+
const now = Date.now();
73+
const timeAfterClickMs = now - clickBreadcrumb.timestamp * 1000;
74+
75+
if (timeAfterClickMs > SLOW_CLICK_THRESHOLD) {
76+
handleSlowClick(replay, clickBreadcrumb, timeAfterClickMs);
77+
}
78+
}
79+
80+
function handleSlowClick(replay: ReplayContainer, clickBreadcrumb: ClickBreadcrumb, _timeAfterClickMs?: number): void {
81+
const timeAfterClickMs = _timeAfterClickMs ? _timeAfterClickMs : Date.now() - clickBreadcrumb.timestamp * 1000;
82+
83+
const breadcrumb = {
84+
message: clickBreadcrumb.message,
85+
timestamp: clickBreadcrumb.timestamp,
86+
category: 'ui.slowClickDetected',
87+
data: {
88+
...clickBreadcrumb.data,
89+
url: WINDOW.location.href,
90+
// TODO FN: add parametrized route, when possible
91+
timeAfterClickMs: Math.min(timeAfterClickMs, SLOW_CLICK_TIMEOUT),
92+
},
93+
};
94+
95+
addBreadcrumbEvent(replay, breadcrumb);
96+
}

0 commit comments

Comments
 (0)