Skip to content

Commit bb67a11

Browse files
authored
fix(utils): Dereference DOM events after they have servered their purpose (#9224)
1 parent bde088e commit bb67a11

File tree

2 files changed

+20
-32
lines changed

2 files changed

+20
-32
lines changed

packages/utils/src/instrument.ts

Lines changed: 19 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import type {
1212
import { isString } from './is';
1313
import type { ConsoleLevel } from './logger';
1414
import { CONSOLE_LEVELS, logger, originalConsoleMethods } from './logger';
15-
import { fill } from './object';
15+
import { addNonEnumerableProperty, fill } from './object';
1616
import { getFunctionName } from './stacktrace';
1717
import { supportsHistory, supportsNativeFetch } from './supports';
1818
import { getGlobalObject, GLOBAL_OBJ } from './worldwide';
@@ -400,31 +400,24 @@ function instrumentHistory(): void {
400400
fill(WINDOW.history, 'replaceState', historyReplacementFunction);
401401
}
402402

403-
const debounceDuration = 1000;
403+
const DEBOUNCE_DURATION = 1000;
404404
let debounceTimerID: number | undefined;
405405
let lastCapturedEvent: Event | undefined;
406406

407407
/**
408-
* Decide whether the current event should finish the debounce of previously captured one.
409-
* @param previous previously captured event
410-
* @param current event to be captured
408+
* Check whether two DOM events are similar to eachother. For example, two click events on the same button.
411409
*/
412-
function shouldShortcircuitPreviousDebounce(previous: Event | undefined, current: Event): boolean {
413-
// If there was no previous event, it should always be swapped for the new one.
414-
if (!previous) {
415-
return true;
416-
}
417-
410+
function areSimilarDomEvents(a: Event, b: Event): boolean {
418411
// If both events have different type, then user definitely performed two separate actions. e.g. click + keypress.
419-
if (previous.type !== current.type) {
420-
return true;
412+
if (a.type !== b.type) {
413+
return false;
421414
}
422415

423416
try {
424417
// If both events have the same type, it's still possible that actions were performed on different targets.
425418
// e.g. 2 clicks on different buttons.
426-
if (previous.target !== current.target) {
427-
return true;
419+
if (a.target !== b.target) {
420+
return false;
428421
}
429422
} catch (e) {
430423
// just accessing `target` property can throw an exception in some rare circumstances
@@ -434,7 +427,7 @@ function shouldShortcircuitPreviousDebounce(previous: Event | undefined, current
434427
// If both events have the same type _and_ same `target` (an element which triggered an event, _not necessarily_
435428
// to which an event listener was attached), we treat them as the same action, as we want to capture
436429
// only one breadcrumb. e.g. multiple clicks on the same button, or typing inside a user input box.
437-
return false;
430+
return true;
438431
}
439432

440433
/**
@@ -475,11 +468,11 @@ function shouldSkipDOMEvent(event: Event): boolean {
475468
* @hidden
476469
*/
477470
function makeDOMEventHandler(handler: Function, globalListener: boolean = false): (event: Event) => void {
478-
return (event: Event): void => {
471+
return (event: Event & { _sentryCaptured?: true }): void => {
479472
// It's possible this handler might trigger multiple times for the same
480473
// event (e.g. event propagation through node ancestors).
481474
// Ignore if we've already captured that event.
482-
if (!event || lastCapturedEvent === event) {
475+
if (!event || event['_sentryCaptured']) {
483476
return;
484477
}
485478

@@ -488,20 +481,15 @@ function makeDOMEventHandler(handler: Function, globalListener: boolean = false)
488481
return;
489482
}
490483

484+
// Mark event as "seen"
485+
addNonEnumerableProperty(event, '_sentryCaptured', true);
486+
491487
const name = event.type === 'keypress' ? 'input' : event.type;
492488

493-
// If there is no debounce timer, it means that we can safely capture the new event and store it for future comparisons.
494-
if (debounceTimerID === undefined) {
495-
handler({
496-
event: event,
497-
name,
498-
global: globalListener,
499-
});
500-
lastCapturedEvent = event;
501-
}
502-
// If there is a debounce awaiting, see if the new event is different enough to treat it as a unique one.
489+
// If there is no last captured event, it means that we can safely capture the new event and store it for future comparisons.
490+
// If there is a last captured event, see if the new event is different enough to treat it as a unique one.
503491
// If that's the case, emit the previous event and store locally the newly-captured DOM event.
504-
else if (shouldShortcircuitPreviousDebounce(lastCapturedEvent, event)) {
492+
if (lastCapturedEvent === undefined || !areSimilarDomEvents(lastCapturedEvent, event)) {
505493
handler({
506494
event: event,
507495
name,
@@ -513,8 +501,8 @@ function makeDOMEventHandler(handler: Function, globalListener: boolean = false)
513501
// Start a new debounce timer that will prevent us from capturing multiple events that should be grouped together.
514502
clearTimeout(debounceTimerID);
515503
debounceTimerID = WINDOW.setTimeout(() => {
516-
debounceTimerID = undefined;
517-
}, debounceDuration);
504+
lastCapturedEvent = undefined;
505+
}, DEBOUNCE_DURATION);
518506
};
519507
}
520508

packages/utils/src/object.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export function fill(source: { [key: string]: any }, name: string, replacementFa
4242
* @param name The name of the property to be set
4343
* @param value The value to which to set the property
4444
*/
45-
export function addNonEnumerableProperty(obj: { [key: string]: unknown }, name: string, value: unknown): void {
45+
export function addNonEnumerableProperty(obj: object, name: string, value: unknown): void {
4646
try {
4747
Object.defineProperty(obj, name, {
4848
// enumerable: false, // the default, so we can save on bundle size by not explicitly setting it

0 commit comments

Comments
 (0)