1+ import { IncrementalSource , MouseInteractions , record } from '@sentry-internal/rrweb' ;
12import type { Breadcrumb } from '@sentry/types' ;
23
34import { WINDOW } from '../constants' ;
45import type {
6+ RecordingEvent ,
57 ReplayClickDetector ,
68 ReplayContainer ,
79 ReplayMultiClickFrame ,
810 ReplaySlowClickFrame ,
911 SlowClickConfig ,
1012} from '../types' ;
13+ import { ReplayEventTypeIncrementalSnapshot } from '../types' ;
1114import { timestampToS } from '../util/timestamp' ;
1215import { addBreadcrumbEvent } from './util/addBreadcrumbEvent' ;
13- import { getClickTargetNode } from './util/domUtils' ;
16+ import { getClosestInteractive } from './util/domUtils' ;
1417import { onWindowOpen } from './util/onWindowOpen' ;
1518
1619type ClickBreadcrumb = Breadcrumb & {
@@ -26,6 +29,16 @@ interface Click {
2629 node : HTMLElement ;
2730}
2831
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+
2942/** Handle a click. */
3043export function handleClick ( clickDetector : ReplayClickDetector , clickBreadcrumb : Breadcrumb , node : HTMLElement ) : void {
3144 clickDetector . handleClick ( clickBreadcrumb , node ) ;
@@ -70,48 +83,14 @@ export class ClickDetector implements ReplayClickDetector {
7083
7184 /** Register click detection handlers on mutation or scroll. */
7285 public addListeners ( ) : void {
73- const mutationHandler = ( ) : void => {
74- this . _lastMutation = nowInSeconds ( ) ;
75- } ;
76-
77- const scrollHandler = ( ) : void => {
78- this . _lastScroll = nowInSeconds ( ) ;
79- } ;
80-
8186 const cleanupWindowOpen = onWindowOpen ( ( ) => {
8287 // Treat window.open as mutation
8388 this . _lastMutation = nowInSeconds ( ) ;
8489 } ) ;
8590
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-
10991 this . _teardown = ( ) => {
110- WINDOW . removeEventListener ( 'scroll' , scrollHandler ) ;
111- WINDOW . removeEventListener ( 'click' , clickHandler ) ;
11292 cleanupWindowOpen ( ) ;
11393
114- obs . disconnect ( ) ;
11594 this . _clicks = [ ] ;
11695 this . _lastMutation = 0 ;
11796 this . _lastScroll = 0 ;
@@ -129,7 +108,7 @@ export class ClickDetector implements ReplayClickDetector {
129108 }
130109 }
131110
132- /** Handle a click */
111+ /** @inheritDoc */
133112 public handleClick ( breadcrumb : Breadcrumb , node : HTMLElement ) : void {
134113 if ( ignoreElement ( node , this . _ignoreSelector ) || ! isClickBreadcrumb ( breadcrumb ) ) {
135114 return ;
@@ -158,6 +137,22 @@ export class ClickDetector implements ReplayClickDetector {
158137 }
159138 }
160139
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+
161156 /** Count multiple clicks on elements. */
162157 private _handleMultiClick ( node : HTMLElement ) : void {
163158 this . _getClicks ( node ) . forEach ( click => {
@@ -311,3 +306,50 @@ function isClickBreadcrumb(breadcrumb: Breadcrumb): breadcrumb is ClickBreadcrum
311306function nowInSeconds ( ) : number {
312307 return Date . now ( ) / 1000 ;
313308}
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