1
+ import { IncrementalSource , MouseInteractions , record } from '@sentry-internal/rrweb' ;
1
2
import type { Breadcrumb } from '@sentry/types' ;
2
3
3
4
import { WINDOW } from '../constants' ;
4
5
import type {
6
+ RecordingEvent ,
5
7
ReplayClickDetector ,
6
8
ReplayContainer ,
7
9
ReplayMultiClickFrame ,
8
10
ReplaySlowClickFrame ,
9
11
SlowClickConfig ,
10
12
} from '../types' ;
13
+ import { ReplayEventTypeIncrementalSnapshot } from '../types' ;
11
14
import { timestampToS } from '../util/timestamp' ;
12
15
import { addBreadcrumbEvent } from './util/addBreadcrumbEvent' ;
13
- import { getClickTargetNode } from './util/domUtils' ;
16
+ import { getClosestInteractive } from './util/domUtils' ;
14
17
import { onWindowOpen } from './util/onWindowOpen' ;
15
18
16
19
type ClickBreadcrumb = Breadcrumb & {
@@ -26,6 +29,16 @@ interface Click {
26
29
node : HTMLElement ;
27
30
}
28
31
32
+ type IncrementalRecordingEvent = RecordingEvent & {
33
+ type : typeof ReplayEventTypeIncrementalSnapshot ;
34
+ data : { source : IncrementalSource } ;
35
+ } ;
36
+
37
+ type IncrementalMouseInteractionRecordingEvent = IncrementalRecordingEvent & {
38
+ type : typeof ReplayEventTypeIncrementalSnapshot ;
39
+ data : { type : MouseInteractions ; id : number } ;
40
+ } ;
41
+
29
42
/** Handle a click. */
30
43
export function handleClick ( clickDetector : ReplayClickDetector , clickBreadcrumb : Breadcrumb , node : HTMLElement ) : void {
31
44
clickDetector . handleClick ( clickBreadcrumb , node ) ;
@@ -70,48 +83,14 @@ export class ClickDetector implements ReplayClickDetector {
70
83
71
84
/** Register click detection handlers on mutation or scroll. */
72
85
public addListeners ( ) : void {
73
- const mutationHandler = ( ) : void => {
74
- this . _lastMutation = nowInSeconds ( ) ;
75
- } ;
76
-
77
- const scrollHandler = ( ) : void => {
78
- this . _lastScroll = nowInSeconds ( ) ;
79
- } ;
80
-
81
86
const cleanupWindowOpen = onWindowOpen ( ( ) => {
82
87
// Treat window.open as mutation
83
88
this . _lastMutation = nowInSeconds ( ) ;
84
89
} ) ;
85
90
86
- const clickHandler = ( event : MouseEvent ) : void => {
87
- if ( ! event . target ) {
88
- return ;
89
- }
90
-
91
- const node = getClickTargetNode ( event ) ;
92
- if ( node ) {
93
- this . _handleMultiClick ( node as HTMLElement ) ;
94
- }
95
- } ;
96
-
97
- const obs = new MutationObserver ( mutationHandler ) ;
98
-
99
- obs . observe ( WINDOW . document . documentElement , {
100
- attributes : true ,
101
- characterData : true ,
102
- childList : true ,
103
- subtree : true ,
104
- } ) ;
105
-
106
- WINDOW . addEventListener ( 'scroll' , scrollHandler , { passive : true } ) ;
107
- WINDOW . addEventListener ( 'click' , clickHandler , { passive : true } ) ;
108
-
109
91
this . _teardown = ( ) => {
110
- WINDOW . removeEventListener ( 'scroll' , scrollHandler ) ;
111
- WINDOW . removeEventListener ( 'click' , clickHandler ) ;
112
92
cleanupWindowOpen ( ) ;
113
93
114
- obs . disconnect ( ) ;
115
94
this . _clicks = [ ] ;
116
95
this . _lastMutation = 0 ;
117
96
this . _lastScroll = 0 ;
@@ -129,7 +108,7 @@ export class ClickDetector implements ReplayClickDetector {
129
108
}
130
109
}
131
110
132
- /** Handle a click */
111
+ /** @inheritDoc */
133
112
public handleClick ( breadcrumb : Breadcrumb , node : HTMLElement ) : void {
134
113
if ( ignoreElement ( node , this . _ignoreSelector ) || ! isClickBreadcrumb ( breadcrumb ) ) {
135
114
return ;
@@ -158,6 +137,22 @@ export class ClickDetector implements ReplayClickDetector {
158
137
}
159
138
}
160
139
140
+ /** @inheritDoc */
141
+ public registerMutation ( timestamp = Date . now ( ) ) : void {
142
+ this . _lastMutation = timestampToS ( timestamp ) ;
143
+ }
144
+
145
+ /** @inheritDoc */
146
+ public registerScroll ( timestamp = Date . now ( ) ) : void {
147
+ this . _lastScroll = timestampToS ( timestamp ) ;
148
+ }
149
+
150
+ /** @inheritDoc */
151
+ public registerClick ( element : HTMLElement ) : void {
152
+ const node = getClosestInteractive ( element ) ;
153
+ this . _handleMultiClick ( node as HTMLElement ) ;
154
+ }
155
+
161
156
/** Count multiple clicks on elements. */
162
157
private _handleMultiClick ( node : HTMLElement ) : void {
163
158
this . _getClicks ( node ) . forEach ( click => {
@@ -311,3 +306,50 @@ function isClickBreadcrumb(breadcrumb: Breadcrumb): breadcrumb is ClickBreadcrum
311
306
function nowInSeconds ( ) : number {
312
307
return Date . now ( ) / 1000 ;
313
308
}
309
+
310
+ /** Update the click detector based on a recording event of rrweb. */
311
+ export function updateClickDetectorForRecordingEvent ( clickDetector : ReplayClickDetector , event : RecordingEvent ) : void {
312
+ try {
313
+ // note: We only consider incremental snapshots here
314
+ // This means that any full snapshot is ignored for mutation detection - the reason is that we simply cannot know if a mutation happened here.
315
+ // E.g. think that we are buffering, an error happens and we take a full snapshot because we switched to session mode -
316
+ // in this scenario, we would not know if a dead click happened because of the error, which is a key dead click scenario.
317
+ // Instead, by ignoring full snapshots, we have the risk that we generate a false positive
318
+ // (if a mutation _did_ happen but was "swallowed" by the full snapshot)
319
+ // But this should be more unlikely as we'd generally capture the incremental snapshot right away
320
+
321
+ if ( ! isIncrementalEvent ( event ) ) {
322
+ return ;
323
+ }
324
+
325
+ const { source } = event . data ;
326
+ if ( source === IncrementalSource . Mutation ) {
327
+ clickDetector . registerMutation ( event . timestamp ) ;
328
+ }
329
+
330
+ if ( source === IncrementalSource . Scroll ) {
331
+ clickDetector . registerScroll ( event . timestamp ) ;
332
+ }
333
+
334
+ if ( isIncrementalMouseInteraction ( event ) ) {
335
+ const { type, id } = event . data ;
336
+ const node = record . mirror . getNode ( id ) ;
337
+
338
+ if ( node instanceof HTMLElement && type === MouseInteractions . Click ) {
339
+ clickDetector . registerClick ( node ) ;
340
+ }
341
+ }
342
+ } catch {
343
+ // ignore errors here, e.g. if accessing something that does not exist
344
+ }
345
+ }
346
+
347
+ function isIncrementalEvent ( event : RecordingEvent ) : event is IncrementalRecordingEvent {
348
+ return event . type === ReplayEventTypeIncrementalSnapshot ;
349
+ }
350
+
351
+ function isIncrementalMouseInteraction (
352
+ event : IncrementalRecordingEvent ,
353
+ ) : event is IncrementalMouseInteractionRecordingEvent {
354
+ return event . data . source === IncrementalSource . MouseInteraction ;
355
+ }
0 commit comments