123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239 |
- Object.defineProperty(exports, '__esModule', { value: true });
- const misc = require('../misc.js');
- const object = require('../object.js');
- const worldwide = require('../worldwide.js');
- const _handlers = require('./_handlers.js');
- const WINDOW = worldwide.GLOBAL_OBJ ;
- const DEBOUNCE_DURATION = 1000;
- let debounceTimerID;
- let lastCapturedEventType;
- let lastCapturedEventTargetId;
- /**
- * Add an instrumentation handler for when a click or a keypress happens.
- *
- * Use at your own risk, this might break without changelog notice, only used internally.
- * @hidden
- */
- function addClickKeypressInstrumentationHandler(handler) {
- const type = 'dom';
- _handlers.addHandler(type, handler);
- _handlers.maybeInstrument(type, instrumentDOM);
- }
- /** Exported for tests only. */
- function instrumentDOM() {
- if (!WINDOW.document) {
- return;
- }
- // Make it so that any click or keypress that is unhandled / bubbled up all the way to the document triggers our dom
- // handlers. (Normally we have only one, which captures a breadcrumb for each click or keypress.) Do this before
- // we instrument `addEventListener` so that we don't end up attaching this handler twice.
- const triggerDOMHandler = _handlers.triggerHandlers.bind(null, 'dom');
- const globalDOMEventHandler = makeDOMEventHandler(triggerDOMHandler, true);
- WINDOW.document.addEventListener('click', globalDOMEventHandler, false);
- WINDOW.document.addEventListener('keypress', globalDOMEventHandler, false);
- // After hooking into click and keypress events bubbled up to `document`, we also hook into user-handled
- // clicks & keypresses, by adding an event listener of our own to any element to which they add a listener. That
- // way, whenever one of their handlers is triggered, ours will be, too. (This is needed because their handler
- // could potentially prevent the event from bubbling up to our global listeners. This way, our handler are still
- // guaranteed to fire at least once.)
- ['EventTarget', 'Node'].forEach((target) => {
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
- const proto = (WINDOW )[target] && (WINDOW )[target].prototype;
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, no-prototype-builtins
- if (!proto || !proto.hasOwnProperty || !proto.hasOwnProperty('addEventListener')) {
- return;
- }
- object.fill(proto, 'addEventListener', function (originalAddEventListener) {
- return function (
- type,
- listener,
- options,
- ) {
- if (type === 'click' || type == 'keypress') {
- try {
- const el = this ;
- const handlers = (el.__sentry_instrumentation_handlers__ = el.__sentry_instrumentation_handlers__ || {});
- const handlerForType = (handlers[type] = handlers[type] || { refCount: 0 });
- if (!handlerForType.handler) {
- const handler = makeDOMEventHandler(triggerDOMHandler);
- handlerForType.handler = handler;
- originalAddEventListener.call(this, type, handler, options);
- }
- handlerForType.refCount++;
- } catch (e) {
- // Accessing dom properties is always fragile.
- // Also allows us to skip `addEventListenrs` calls with no proper `this` context.
- }
- }
- return originalAddEventListener.call(this, type, listener, options);
- };
- });
- object.fill(
- proto,
- 'removeEventListener',
- function (originalRemoveEventListener) {
- return function (
- type,
- listener,
- options,
- ) {
- if (type === 'click' || type == 'keypress') {
- try {
- const el = this ;
- const handlers = el.__sentry_instrumentation_handlers__ || {};
- const handlerForType = handlers[type];
- if (handlerForType) {
- handlerForType.refCount--;
- // If there are no longer any custom handlers of the current type on this element, we can remove ours, too.
- if (handlerForType.refCount <= 0) {
- originalRemoveEventListener.call(this, type, handlerForType.handler, options);
- handlerForType.handler = undefined;
- delete handlers[type]; // eslint-disable-line @typescript-eslint/no-dynamic-delete
- }
- // If there are no longer any custom handlers of any type on this element, cleanup everything.
- if (Object.keys(handlers).length === 0) {
- delete el.__sentry_instrumentation_handlers__;
- }
- }
- } catch (e) {
- // Accessing dom properties is always fragile.
- // Also allows us to skip `addEventListenrs` calls with no proper `this` context.
- }
- }
- return originalRemoveEventListener.call(this, type, listener, options);
- };
- },
- );
- });
- }
- /**
- * Check whether the event is similar to the last captured one. For example, two click events on the same button.
- */
- function isSimilarToLastCapturedEvent(event) {
- // If both events have different type, then user definitely performed two separate actions. e.g. click + keypress.
- if (event.type !== lastCapturedEventType) {
- return false;
- }
- try {
- // If both events have the same type, it's still possible that actions were performed on different targets.
- // e.g. 2 clicks on different buttons.
- if (!event.target || (event.target )._sentryId !== lastCapturedEventTargetId) {
- return false;
- }
- } catch (e) {
- // just accessing `target` property can throw an exception in some rare circumstances
- // see: https://github.com/getsentry/sentry-javascript/issues/838
- }
- // If both events have the same type _and_ same `target` (an element which triggered an event, _not necessarily_
- // to which an event listener was attached), we treat them as the same action, as we want to capture
- // only one breadcrumb. e.g. multiple clicks on the same button, or typing inside a user input box.
- return true;
- }
- /**
- * Decide whether an event should be captured.
- * @param event event to be captured
- */
- function shouldSkipDOMEvent(eventType, target) {
- // We are only interested in filtering `keypress` events for now.
- if (eventType !== 'keypress') {
- return false;
- }
- if (!target || !target.tagName) {
- return true;
- }
- // Only consider keypress events on actual input elements. This will disregard keypresses targeting body
- // e.g.tabbing through elements, hotkeys, etc.
- if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
- return false;
- }
- return true;
- }
- /**
- * Wraps addEventListener to capture UI breadcrumbs
- */
- function makeDOMEventHandler(
- handler,
- globalListener = false,
- ) {
- return (event) => {
- // It's possible this handler might trigger multiple times for the same
- // event (e.g. event propagation through node ancestors).
- // Ignore if we've already captured that event.
- if (!event || event['_sentryCaptured']) {
- return;
- }
- const target = getEventTarget(event);
- // We always want to skip _some_ events.
- if (shouldSkipDOMEvent(event.type, target)) {
- return;
- }
- // Mark event as "seen"
- object.addNonEnumerableProperty(event, '_sentryCaptured', true);
- if (target && !target._sentryId) {
- // Add UUID to event target so we can identify if
- object.addNonEnumerableProperty(target, '_sentryId', misc.uuid4());
- }
- const name = event.type === 'keypress' ? 'input' : event.type;
- // If there is no last captured event, it means that we can safely capture the new event and store it for future comparisons.
- // If there is a last captured event, see if the new event is different enough to treat it as a unique one.
- // If that's the case, emit the previous event and store locally the newly-captured DOM event.
- if (!isSimilarToLastCapturedEvent(event)) {
- const handlerData = { event, name, global: globalListener };
- handler(handlerData);
- lastCapturedEventType = event.type;
- lastCapturedEventTargetId = target ? target._sentryId : undefined;
- }
- // Start a new debounce timer that will prevent us from capturing multiple events that should be grouped together.
- clearTimeout(debounceTimerID);
- debounceTimerID = WINDOW.setTimeout(() => {
- lastCapturedEventTargetId = undefined;
- lastCapturedEventType = undefined;
- }, DEBOUNCE_DURATION);
- };
- }
- function getEventTarget(event) {
- try {
- return event.target ;
- } catch (e) {
- // just accessing `target` property can throw an exception in some rare circumstances
- // see: https://github.com/getsentry/sentry-javascript/issues/838
- return null;
- }
- }
- exports.addClickKeypressInstrumentationHandler = addClickKeypressInstrumentationHandler;
- exports.instrumentDOM = instrumentDOM;
- //# sourceMappingURL=dom.js.map
|