UNPKG

posthog-js

Version:

Posthog-js allows you to automatically capture usage and send events to PostHog.

794 lines • 50.3 kB
"use strict";
var __assign = (this && this.__assign) || function () {
    __assign = Object.assign || function(t) {
        for (var s, i = 1, n = arguments.length; i < n; i++) {
            s = arguments[i];
            for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
                t[p] = s[p];
        }
        return t;
    };
    return __assign.apply(this, arguments);
};
var __values = (this && this.__values) || function(o) {
    var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
    if (m) return m.call(o);
    if (o && typeof o.length === "number") return {
        next: function () {
            if (o && i >= o.length) o = void 0;
            return { value: o && o[i++], done: !o };
        }
    };
    throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
};
Object.defineProperty(exports, "__esModule", { value: true });
var globals_1 = require("@posthog/browser-common/utils/globals");
var globals_2 = require("../utils/globals");
var core_1 = require("@posthog/core");
var autocapture_utils_1 = require("@posthog/browser-common/utils/autocapture-utils");
var autocapture_1 = require("../autocapture");
var element_utils_1 = require("@posthog/browser-common/utils/element-utils");
var prototype_utils_1 = require("@posthog/browser-common/utils/prototype-utils");
var general_utils_1 = require("@posthog/browser-common/utils/general-utils");
function asCandidate(event, extra) {
    var eventTarget = (0, autocapture_utils_1.getEventTarget)(event);
    if (eventTarget) {
        return __assign({ node: eventTarget, originalEvent: event, timestamp: Date.now() }, extra);
    }
    return null;
}
// Firefox denies property access on a node from another origin or from a realm that was torn
// down, and both a composed path and a mutation record can hand us one. A node we cannot read
// is a node we cannot inspect, so answer no instead of letting the denial escape.
function isInspectableElement(node) {
    return !!(0, core_1.trySafe)(function () { return (0, element_utils_1.isElementNode)(node); });
}
function swipeDirection(dx, dy) {
    if (Math.abs(dx) >= Math.abs(dy)) {
        return dx >= 0 ? 'right' : 'left';
    }
    return dy >= 0 ? 'down' : 'up';
}
// the event name doubles as the property prefix ($dead_click_* / $dead_swipe_*)
function deadEventName(candidate) {
    return candidate.type === 'swipe' ? '$dead_swipe' : '$dead_click';
}
// surfaces that respond to a gesture without anything our observers can see — a canvas or
// WebGL repaint is not a DOM mutation, and native media controls live in a closed UA shadow
// root — so a swipe over them can never be judged dead
var UNOBSERVABLE_SURFACE_SELECTOR = 'canvas,video,audio,embed,object';
// a click within this window of a visibility or window focus/blur change is treated as the click
// that woke/focused the tab (or opened a new tab/window) and suppressed rather than flagged dead.
// wider than the other thresholds because a real "tab back, then click the page" gesture has a
// human-scale gap (refocus, move the mouse, click); still short enough that a genuinely dead click
// a while after refocusing is caught.
var LIVENESS_SUPPRESSION_MS = 1000;
function hasModifierKey(event) {
    return event.ctrlKey || event.metaKey || event.altKey || event.shiftKey;
}
function checkTimeout(value, thresholdMs) {
    return (0, core_1.isNumber)(value) && value >= thresholdMs;
}
// a liveness transition only counts if it fired within the suppression window on one side of the
// click. this is the single definition of "in window" — it gates every recorded delay, so once a
// delay lands on a candidate `_checkClicks` can trust it is already in range.
function livenessDelayInWindow(delay, windowMs) {
    if (windowMs === void 0) { windowMs = LIVENESS_SUPPRESSION_MS; }
    return delay >= 0 && delay < windowMs ? delay : undefined;
}
// a liveness signal that fired shortly BEFORE the click. read once when the candidate is queued;
// only a change inside the suppression window counts, so a long-ago transition cannot affect it.
function priorLivenessDelay(clickTimestamp, lastSeenAt, windowMs) {
    if (windowMs === void 0) { windowMs = LIVENESS_SUPPRESSION_MS; }
    return (0, core_1.isNumber)(lastSeenAt) ? livenessDelayInWindow(clickTimestamp - lastSeenAt, windowMs) : undefined;
}
// How dead-click detection works
// ================================
// A click (or swipe) is queued as a candidate, then re-examined ~1s later in `_checkClicks`. It is
// reported as a `$dead_click` only if NO liveness/suppression signal fired within its applicable
// window AND at least one timeout signal fired. In short: something-happened-fast wins
// (alive), otherwise nothing-happened-in-time loses (dead).
//
// Gate signals (checked in `_ignore` before the click is ever queued — never even a candidate):
//   - element is inside the PostHog toolbar
//   - same node clicked again within 1s (dedupe of repeated clicks)
//   - target is the <html> node or not an element
//   - target matches `css_selector_ignorelist`
//   - a modifier key is held (ctrl/meta/alt/shift) unless `capture_clicks_with_modifier_keys`
//   - (clicks only) target is an anchor — a legitimate activation; swipes are still candidates
//
// Liveness / suppression signals (any one, within its window => alive, dropped).
// These say "the click did something", so they only ever suppress; none can mark a click dead:
//   - mutation:   a DOM mutation           < mutation_threshold_ms (default 2500)
//   - scroll:     the page/an element scrolled < scroll_threshold_ms (default 100)
//   - selection:  selecting/unselecting a range or moving an editable caret during the matching
//                 mouse gesture, or < selection_change_threshold_ms (default 100) either side of the click
//   - visibility: a visibilitychange (either direction — the tab going hidden because the click
//                 opened a new tab, or becoming visible as the click woke/focused it)
//                 < LIVENESS_SUPPRESSION_MS on either side of the click
//   - focus:      a window focus/blur (a click that opened a new window/popup may only surface as
//                 the current window losing focus) < LIVENESS_SUPPRESSION_MS on either side
// selection/visibility/focus are recorded on each candidate, not read from a single shared
// timestamp when the click is checked. A pre-click selection is stored only inside its window;
// post-click changes keep the closest delay. For visibility/focus this also matters because
// hiding/blurring the tab can suspend the check until a later transition overwrites it.
//
// Timeout signals (a click with no liveness signal is dead if any one fired). Note visibility and
// focus are deliberately absent here — they are liveness-only and never mark a click dead:
//   - mutation timeout:  a mutation, but only after mutation_threshold_ms
//   - scroll timeout:    a scroll, but only after scroll_threshold_ms
//   - selection timeout: a selectionchange, but only after selection_change_threshold_ms
//   - absolute timeout:  nothing at all within mutation_threshold_ms * 1.1 (the catch-all backstop)
var LazyLoadedDeadClicksAutocapture = /** @class */ (function () {
    function LazyLoadedDeadClicksAutocapture(instance, config) {
        var _this = this;
        this.instance = instance;
        this._observedRoots = new WeakSet();
        this._hadSelection = false;
        this._clicks = [];
        this._deadSwipesCaptured = 0;
        this._defaultConfig = function (defaultOnCapture) { return ({
            element_attribute_ignorelist: [],
            scroll_threshold_ms: 100,
            selection_change_threshold_ms: 100,
            mutation_threshold_ms: 2500,
            capture_clicks_with_modifier_keys: false,
            capture_dead_swipes: true,
            swipe_threshold_px: 30,
            max_dead_swipes_per_page_load: 10,
            __onCapture: defaultOnCapture,
        }); };
        this._clearMouseSelection = function () {
            clearTimeout(_this._mouseSelectionTimer);
            _this._mouseSelectionTimer = undefined;
            _this._mouseSelection = undefined;
        };
        this._onMouseDown = function (event) {
            _this._observeGesturePath(event);
            _this._clearMouseSelection();
            if (event.button === 0) {
                var gesture = {
                    path: event.composedPath(),
                    trusted: event.isTrusted,
                    selectionChanged: false,
                    selectionInitiallyRelated: false,
                };
                var selection = globals_1.document === null || globals_1.document === void 0 ? void 0 : globals_1.document.getSelection();
                gesture.selectionInitiallyRelated =
                    (selection === null || selection === void 0 ? void 0 : selection.type) === 'Range' && _this._selectionTouchesGesture(selection, gesture);
                _this._mouseSelection = gesture;
            }
        };
        this._onMouseOut = function (event) {
            // A release outside the document may never reach our mouseup listener.
            if ((0, core_1.isNull)(event.relatedTarget)) {
                _this._clearMouseSelection();
            }
        };
        this._onMouseUp = function (event) {
            var e_1, _a;
            var gesture = _this._mouseSelection;
            var mouseEvent = event;
            if (!gesture || mouseEvent.button !== 0 || event.isTrusted !== gesture.trusted) {
                return;
            }
            var path = event.composedPath();
            try {
                // Native clicks target the nearest common ancestor of the press and release targets.
                // Composed paths preserve this relationship through accessible shadow boundaries.
                for (var _b = __values(gesture.path), _c = _b.next(); !_c.done; _c = _b.next()) {
                    var node = _c.value;
                    if (isInspectableElement(node) && path.indexOf(node) !== -1) {
                        gesture.clickTarget = node;
                        break;
                    }
                }
            }
            catch (e_1_1) { e_1 = { error: e_1_1 }; }
            finally {
                try {
                    if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
                }
                finally { if (e_1) throw e_1.error; }
            }
            gesture.clickDetail = mouseEvent.detail;
            // Mouseup and its native click are dispatched together. If no click follows, discard
            // the released gesture before a later activation can inherit its selection activity.
            _this._mouseSelectionTimer = globals_2.assignableWindow.setTimeout(_this._clearMouseSelection, 0);
        };
        this._onClick = function (event) {
            // a click without a preceding mousedown, e.g. a keyboard activation, reaches us here first
            _this._observeGesturePath(event);
            var mouseEvent = event;
            var click = asCandidate(mouseEvent, { type: 'click' });
            var gesture = _this._mouseSelection;
            if (click &&
                gesture &&
                mouseEvent.button === 0 &&
                mouseEvent.detail > 0 &&
                mouseEvent.detail === gesture.clickDetail &&
                event.isTrusted === gesture.trusted &&
                click.node === gesture.clickTarget) {
                click.selectionChangedDuringGesture =
                    gesture.selectionChanged && _this._config.selection_change_threshold_ms > 0;
                _this._clearMouseSelection();
            }
            if (!(0, core_1.isNull)(click) && !_this._ignore(click)) {
                _this._queueCandidate(click);
            }
            _this._scheduleCheck();
        };
        this._onScroll = function () {
            var candidateNow = Date.now();
            _this._lastScroll = candidateNow;
            // very naive throttle
            if (candidateNow % 50 === 0) {
                // we can see many scrolls between scheduled checks,
                // so we update scroll delay as we see them
                // to avoid false positives
                _this._clicks.forEach(function (click) {
                    if ((0, core_1.isUndefined)(click.scrollDelayMs)) {
                        click.scrollDelayMs = candidateNow - click.timestamp;
                    }
                });
            }
        };
        this._onSelectionChange = function (event) {
            var _a;
            var selection = globals_1.document === null || globals_1.document === void 0 ? void 0 : globals_1.document.getSelection();
            var hadSelection = _this._hadSelection;
            // isCollapsed can be true for a range inside a shadow root; type reflects the actual selection.
            _this._hadSelection = (selection === null || selection === void 0 ? void 0 : selection.type) === 'Range';
            var focusNode = selection === null || selection === void 0 ? void 0 : selection.focusNode;
            var focusElement = focusNode && ((0, element_utils_1.isElementNode)(focusNode) ? focusNode : focusNode.parentElement);
            // Selection endpoints may be retargeted outside an editor's shadow root.
            var documentActiveElement = globals_1.document === null || globals_1.document === void 0 ? void 0 : globals_1.document.activeElement;
            var activeElement = documentActiveElement;
            while ((_a = activeElement === null || activeElement === void 0 ? void 0 : activeElement.shadowRoot) === null || _a === void 0 ? void 0 : _a.activeElement) {
                activeElement = activeElement.shadowRoot.activeElement;
            }
            var isEditing = (0, autocapture_utils_1.isTextSelectionTarget)((0, autocapture_utils_1.getEventTarget)(event)) ||
                (0, autocapture_utils_1.isTextSelectionTarget)(focusElement !== null && focusElement !== void 0 ? focusElement : null) ||
                (0, autocapture_utils_1.isTextSelectionTarget)(activeElement !== null && activeElement !== void 0 ? activeElement : null);
            // A caret reported outside focused content may belong to a closed shadow root.
            // Preserve that activity when its actual target cannot be inspected.
            var hasRetargetedCaret = (selection === null || selection === void 0 ? void 0 : selection.type) === 'Caret' &&
                activeElement &&
                !activeElement.shadowRoot &&
                focusNode &&
                focusNode !== documentActiveElement &&
                focusNode.contains(documentActiveElement !== null && documentActiveElement !== void 0 ? documentActiveElement : null);
            // Ordinary clicks can move an invisible caret on known non-editable content.
            if (!hadSelection && !_this._hadSelection && !isEditing && !hasRetargetedCaret) {
                return;
            }
            var firedAt = Date.now();
            _this._lastSelectionChanged = firedAt;
            var gesture = _this._mouseSelection;
            // An opaque closed-root caret is only a possible editor. Keep its existing timed
            // fallback, but do not extend that uncertainty across a long press on inert content.
            if (gesture && (hadSelection || _this._hadSelection || isEditing)) {
                var eventTarget = (0, autocapture_utils_1.getEventTarget)(event);
                var editor = void 0;
                if ((0, autocapture_utils_1.isTextSelectionTarget)(eventTarget)) {
                    editor = eventTarget;
                }
                else if ((selection === null || selection === void 0 ? void 0 : selection.type) === 'Caret') {
                    if ((0, autocapture_utils_1.isTextSelectionTarget)(activeElement !== null && activeElement !== void 0 ? activeElement : null) || hasRetargetedCaret) {
                        editor = activeElement;
                    }
                    else if (isEditing) {
                        editor = focusElement;
                    }
                }
                // Endpoint-less clearing must belong to the range present at press time; an
                // unrelated range removed elsewhere in the document must not suppress this click.
                var related = editor
                    ? _this._selectionIsInGesture(editor, gesture)
                    : selection && (focusNode || selection.anchorNode)
                        ? _this._selectionTouchesGesture(selection, gesture)
                        : hadSelection && !_this._hadSelection && gesture.selectionInitiallyRelated;
                gesture.selectionChanged = gesture.selectionChanged || related;
            }
            // Keep the closest selection change after each candidate. A nearby change suppresses the
            // candidate; a later one retains the existing post-click timeout behavior.
            _this._clicks.forEach(function (candidate) {
                var delay = firedAt - candidate.timestamp;
                if (delay >= 0 &&
                    ((0, core_1.isUndefined)(candidate.selectionChangedDelayMs) || delay < candidate.selectionChangedDelayMs)) {
                    candidate.selectionChangedDelayMs = delay;
                }
            });
        };
        this._onVisibilityChange = function () {
            if ((globals_1.document === null || globals_1.document === void 0 ? void 0 : globals_1.document.visibilityState) === 'hidden') {
                _this._clearMouseSelection();
            }
            // record both directions: a tab going _hidden_ right after a click (the click opened a new
            // tab) is as much a liveness signal as it becoming visible (the click that woke the tab).
            // stamp queued candidates now, before a hidden tab can suspend `_checkClicks`.
            var firedAt = Date.now();
            _this._lastVisibilityChange = firedAt;
            _this._recordLivenessSignal('visibilityChangedDelayMs', firedAt);
        };
        this._onFocusChange = function () {
            var firedAt = Date.now();
            _this._lastFocusChange = firedAt;
            _this._recordLivenessSignal('focusChangedDelayMs', firedAt);
        };
        this._onTouchStart = function (event) {
            _this._observeGesturePath(event);
            var touches = event.touches;
            // only single-finger gestures are swipes; a second finger (pinch/zoom) is not,
            // so a multi-touch start clears any tracked origin rather than measuring against it
            var touch = (touches === null || touches === void 0 ? void 0 : touches.length) === 1 ? touches[0] : undefined;
            _this._touchStart = touch ? { x: touch.clientX, y: touch.clientY, timestamp: Date.now() } : undefined;
        };
        this._onTouchCancel = function () {
            _this._touchStart = undefined;
        };
        this._onTouchEnd = function (event) {
            var _a, _b, _c;
            var start = _this._touchStart;
            _this._touchStart = undefined;
            if ((0, core_1.isUndefined)(start) || _this._deadSwipesCaptured >= _this._config.max_dead_swipes_per_page_load) {
                return;
            }
            var touchEvent = event;
            var touch = (_a = touchEvent.changedTouches) === null || _a === void 0 ? void 0 : _a[0];
            if (!touch) {
                return;
            }
            var dx = touch.clientX - start.x;
            var dy = touch.clientY - start.y;
            var distanceSq = dx * dx + dy * dy;
            // compare squared distances to avoid a sqrt on every touch; only the actual swipes need it
            var threshold = _this._config.swipe_threshold_px;
            if (distanceSq < threshold * threshold) {
                // a short movement is a tap or a jitter, not a swipe
                return;
            }
            // a gesture that already did something while the finger was down — dragging scrolled
            // the page, a finger-following carousel mutated the DOM, a text selection grew — is not
            // dead, so it never becomes a candidate. surviving candidates keep the touchend
            // timestamp: the thresholds in _checkClicks measure how quickly the page responds
            // after the finger lifts, just as they measure the response to a click
            var gestureCausedActivity = [_this._lastScroll, _this._lastMutation, _this._lastSelectionChanged].some(function (lastActivityAt) { return (0, core_1.isNumber)(lastActivityAt) && lastActivityAt >= start.timestamp; });
            if (gestureCausedActivity) {
                return;
            }
            // hit-test the stack under where the finger landed rather than the candidate's
            // ancestors: canvas apps usually receive their touches through transparent overlay
            // elements stacked on top of the canvas, so the surface is beneath, not above.
            // this runs at most once per dead-candidate gesture, when the page is provably idle
            // (the activity gate above just passed), so a forced layout flush is unlikely
            if (_this._pageHasUnobservableSurfaces()) {
                var stack = (_c = (_b = globals_1.document === null || globals_1.document === void 0 ? void 0 : globals_1.document.elementsFromPoint) === null || _b === void 0 ? void 0 : _b.call(globals_1.document, start.x, start.y)) !== null && _c !== void 0 ? _c : [];
                if (stack.some(function (el) { var _a; return (_a = el.matches) === null || _a === void 0 ? void 0 : _a.call(el, UNOBSERVABLE_SURFACE_SELECTOR); })) {
                    return;
                }
            }
            var swipe = asCandidate(touchEvent, {
                type: 'swipe',
                swipeDirection: swipeDirection(dx, dy),
                swipeDistancePx: Math.round(Math.sqrt(distanceSq)),
            });
            if (!(0, core_1.isNull)(swipe) && !_this._ignore(swipe)) {
                _this._queueCandidate(swipe);
            }
            _this._scheduleCheck();
        };
        this._config = this._asRequiredConfig(config);
        this._onCapture = this._config.__onCapture;
    }
    LazyLoadedDeadClicksAutocapture.prototype._asRequiredConfig = function (providedConfig) {
        var _a, _b, _c, _d, _e, _f, _g, _h;
        var defaultConfig = this._defaultConfig((providedConfig === null || providedConfig === void 0 ? void 0 : providedConfig.__onCapture) || this._captureDeadClick.bind(this));
        return {
            element_attribute_ignorelist: (_a = providedConfig === null || providedConfig === void 0 ? void 0 : providedConfig.element_attribute_ignorelist) !== null && _a !== void 0 ? _a : defaultConfig.element_attribute_ignorelist,
            scroll_threshold_ms: (_b = providedConfig === null || providedConfig === void 0 ? void 0 : providedConfig.scroll_threshold_ms) !== null && _b !== void 0 ? _b : defaultConfig.scroll_threshold_ms,
            selection_change_threshold_ms: (_c = providedConfig === null || providedConfig === void 0 ? void 0 : providedConfig.selection_change_threshold_ms) !== null && _c !== void 0 ? _c : defaultConfig.selection_change_threshold_ms,
            mutation_threshold_ms: (_d = providedConfig === null || providedConfig === void 0 ? void 0 : providedConfig.mutation_threshold_ms) !== null && _d !== void 0 ? _d : defaultConfig.mutation_threshold_ms,
            capture_clicks_with_modifier_keys: (_e = providedConfig === null || providedConfig === void 0 ? void 0 : providedConfig.capture_clicks_with_modifier_keys) !== null && _e !== void 0 ? _e : defaultConfig.capture_clicks_with_modifier_keys,
            capture_dead_swipes: (_f = providedConfig === null || providedConfig === void 0 ? void 0 : providedConfig.capture_dead_swipes) !== null && _f !== void 0 ? _f : defaultConfig.capture_dead_swipes,
            swipe_threshold_px: (_g = providedConfig === null || providedConfig === void 0 ? void 0 : providedConfig.swipe_threshold_px) !== null && _g !== void 0 ? _g : defaultConfig.swipe_threshold_px,
            max_dead_swipes_per_page_load: (_h = providedConfig === null || providedConfig === void 0 ? void 0 : providedConfig.max_dead_swipes_per_page_load) !== null && _h !== void 0 ? _h : defaultConfig.max_dead_swipes_per_page_load,
            css_selector_ignorelist: providedConfig === null || providedConfig === void 0 ? void 0 : providedConfig.css_selector_ignorelist,
            __onCapture: defaultConfig.__onCapture,
        };
    };
    LazyLoadedDeadClicksAutocapture.prototype.start = function (observerTarget) {
        this._startClickObserver();
        this._startScrollObserver();
        this._startSelectionChangedObserver();
        this._startVisibilityChangeObserver();
        this._startFocusChangeObserver();
        this._startMutationObserver(observerTarget);
        if (this._config.capture_dead_swipes) {
            this._startSwipeObserver();
        }
    };
    LazyLoadedDeadClicksAutocapture.prototype._startMutationObserver = function (observerTarget) {
        var _this = this;
        if (!this._mutationObserver) {
            var NativeMutationObserver = (0, prototype_utils_1.getNativeMutationObserverImplementation)(globals_2.assignableWindow);
            this._mutationObserver = new NativeMutationObserver(function (mutations) {
                _this._onMutation(mutations);
            });
            this._observeRoot(observerTarget);
        }
    };
    // An observed subtree stops at a shadow boundary, so a click that re-renders inside a shadow
    // root looks like nothing happened. Each root we want changes from needs its own observe call.
    LazyLoadedDeadClicksAutocapture.prototype._observeRoot = function (root) {
        if (!root || !this._mutationObserver) {
            return;
        }
        if (this._observedRoots.has(root)) {
            return;
        }
        this._mutationObserver.observe(root, {
            attributes: true,
            characterData: true,
            childList: true,
            subtree: true,
        });
        this._observedRoots.add(root);
        this._observeShadowRoots(root);
    };
    // Observe every open shadow root this node hosts or contains. Nested roots are covered
    // because each root observed here is scanned in turn. Every scanned element is added to
    // `scanned`, when given, so a caller can skip nodes an earlier scan already covered.
    LazyLoadedDeadClicksAutocapture.prototype._observeShadowRoots = function (node, scanned) {
        var _a, _b, _c;
        if ((0, element_utils_1.isElementNode)(node) && node.shadowRoot) {
            this._observeRoot(node.shadowRoot);
        }
        var descendants = (_c = (_b = (_a = node).querySelectorAll) === null || _b === void 0 ? void 0 : _b.call(_a, '*')) !== null && _c !== void 0 ? _c : [];
        for (var i = 0; i < descendants.length; i++) {
            scanned === null || scanned === void 0 ? void 0 : scanned.add(descendants[i]);
            this._observeRoot(descendants[i].shadowRoot);
        }
    };
    // A gesture inside an open shadow root exposes that root in its composed path, so we can
    // watch the root before the application's own handlers render into it.
    LazyLoadedDeadClicksAutocapture.prototype._observeGesturePath = function (event) {
        var _a, _b;
        var path = (_b = (_a = event.composedPath) === null || _a === void 0 ? void 0 : _a.call(event)) !== null && _b !== void 0 ? _b : [];
        for (var i = 0; i < path.length; i++) {
            var node = path[i];
            if (isInspectableElement(node)) {
                this._observeRoot(node.shadowRoot);
            }
        }
    };
    LazyLoadedDeadClicksAutocapture.prototype.stop = function () {
        var _a;
        (_a = this._mutationObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
        this._mutationObserver = undefined;
        this._observedRoots = new WeakSet();
        globals_2.assignableWindow.removeEventListener('click', this._onClick, { capture: true });
        globals_2.assignableWindow.removeEventListener('mousedown', this._onMouseDown, { capture: true });
        globals_2.assignableWindow.removeEventListener('mouseup', this._onMouseUp, { capture: true });
        globals_2.assignableWindow.removeEventListener('mouseout', this._onMouseOut, { capture: true });
        globals_2.assignableWindow.removeEventListener('dragstart', this._clearMouseSelection, { capture: true });
        globals_2.assignableWindow.removeEventListener('pointercancel', this._clearMouseSelection, { capture: true });
        globals_2.assignableWindow.removeEventListener('blur', this._clearMouseSelection);
        this._clearMouseSelection();
        globals_2.assignableWindow.removeEventListener('scroll', this._onScroll, { capture: true });
        globals_1.document === null || globals_1.document === void 0 ? void 0 : globals_1.document.removeEventListener('selectionchange', this._onSelectionChange);
        globals_2.assignableWindow.removeEventListener('touchstart', this._onTouchStart, { capture: true });
        globals_2.assignableWindow.removeEventListener('touchend', this._onTouchEnd, { capture: true });
        globals_2.assignableWindow.removeEventListener('touchcancel', this._onTouchCancel, { capture: true });
        globals_1.document === null || globals_1.document === void 0 ? void 0 : globals_1.document.removeEventListener('visibilitychange', this._onVisibilityChange);
        globals_2.assignableWindow.removeEventListener('blur', this._onFocusChange);
        globals_2.assignableWindow.removeEventListener('focus', this._onFocusChange);
        // so a gesture in flight when we stopped can't pair with a touchend after a restart
        this._touchStart = undefined;
    };
    LazyLoadedDeadClicksAutocapture.prototype._onMutation = function (mutations) {
        var e_2, _a;
        var _b;
        // every scan in this batch reads the DOM as it is now, so a node an earlier scan visited
        // needs no scan of its own
        var scanned = new Set();
        try {
            for (var mutations_1 = __values(mutations), mutations_1_1 = mutations_1.next(); !mutations_1_1.done; mutations_1_1 = mutations_1.next()) {
                var mutation = mutations_1_1.value;
                // one record we cannot read must not end the batch: every record behind it would lose
                // its sign of life and the timeout backstop would report a false dead click
                try {
                    // a root observed directly keeps reporting after its host leaves the page, and a change
                    // off the page is no sign of life. only an explicit `false` is skipped, since older
                    // browsers lack `isConnected`
                    if (((_b = mutation.target) === null || _b === void 0 ? void 0 : _b.isConnected) === false) {
                        continue;
                    }
                    this._lastMutation = Date.now();
                    // added content can bring a shadow root of its own, which the observer that reported
                    // the addition cannot see into
                    var addedNodes = mutation.addedNodes;
                    for (var i = 0; i < addedNodes.length; i++) {
                        var node = addedNodes[i];
                        if (isInspectableElement(node) && !scanned.has(node)) {
                            this._observeShadowRoots(node, scanned);
                        }
                    }
                }
                catch (_c) { }
            }
        }
        catch (e_2_1) { e_2 = { error: e_2_1 }; }
        finally {
            try {
                if (mutations_1_1 && !mutations_1_1.done && (_a = mutations_1.return)) _a.call(mutations_1);
            }
            finally { if (e_2) throw e_2.error; }
        }
    };
    LazyLoadedDeadClicksAutocapture.prototype._startClickObserver = function () {
        // Queue the click before application handlers can mutate the DOM and deliver mutation callbacks.
        (0, general_utils_1.addEventListener)(globals_2.assignableWindow, 'click', this._onClick, { capture: true });
        (0, general_utils_1.addEventListener)(globals_2.assignableWindow, 'mousedown', this._onMouseDown, { capture: true });
        (0, general_utils_1.addEventListener)(globals_2.assignableWindow, 'mouseup', this._onMouseUp, { capture: true });
        (0, general_utils_1.addEventListener)(globals_2.assignableWindow, 'mouseout', this._onMouseOut, { capture: true });
        (0, general_utils_1.addEventListener)(globals_2.assignableWindow, 'dragstart', this._clearMouseSelection, { capture: true });
        (0, general_utils_1.addEventListener)(globals_2.assignableWindow, 'pointercancel', this._clearMouseSelection, { capture: true });
        (0, general_utils_1.addEventListener)(globals_2.assignableWindow, 'blur', this._clearMouseSelection);
    };
    // Only store a pre-candidate selection delay when it is already inside the suppression window,
    // so it can never feed the post-click selection timeout. Keep the candidate queued until the
    // check so it can still deduplicate another click on the same node.
    LazyLoadedDeadClicksAutocapture.prototype._queueCandidate = function (candidate) {
        candidate.selectionChangedDelayMs = priorLivenessDelay(candidate.timestamp, this._lastSelectionChanged, this._config.selection_change_threshold_ms);
        candidate.visibilityChangedDelayMs = priorLivenessDelay(candidate.timestamp, this._lastVisibilityChange);
        candidate.focusChangedDelayMs = priorLivenessDelay(candidate.timestamp, this._lastFocusChange);
        this._clicks.push(candidate);
    };
    LazyLoadedDeadClicksAutocapture.prototype._scheduleCheck = function () {
        var _this = this;
        if (this._clicks.length && (0, core_1.isUndefined)(this._checkClickTimer)) {
            this._checkClickTimer = globals_2.assignableWindow.setTimeout(function () {
                _this._checkClicks();
            }, 1000);
        }
    };
    // `capture: true` is required to get scroll events for other scrollable elements
    // on the page, not just the window
    // see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#usecapture
    //
    // `passive: true` is used to tell the browser that the scroll event handler will not call `preventDefault()`
    // This allows the browser to optimize scrolling performance by not waiting for our handling of the scroll event
    // see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#passive
    LazyLoadedDeadClicksAutocapture.prototype._startScrollObserver = function () {
        (0, general_utils_1.addEventListener)(globals_2.assignableWindow, 'scroll', this._onScroll, { capture: true });
    };
    LazyLoadedDeadClicksAutocapture.prototype._startSelectionChangedObserver = function () {
        var _a;
        // A selection may already exist when the lazy-loaded detector starts.
        this._hadSelection = ((_a = globals_1.document === null || globals_1.document === void 0 ? void 0 : globals_1.document.getSelection()) === null || _a === void 0 ? void 0 : _a.type) === 'Range';
        // Document selections fire selectionchange directly on document and the event does not bubble.
        (0, general_utils_1.addEventListener)(globals_1.document, 'selectionchange', this._onSelectionChange);
    };
    LazyLoadedDeadClicksAutocapture.prototype._selectionIsInGesture = function (node, gesture) {
        var target = gesture.path[0];
        try {
            while (node) {
                if (gesture.path.indexOf(node) !== -1 || ((0, element_utils_1.isElementNode)(target) && target.contains(node))) {
                    return true;
                }
                // Firefox can expose selection endpoints behind a closed root while mouse events
                // expose only its host. Compare both representations without traversing other content.
                node = node.getRootNode().host;
            }
        }
        catch (_a) {
            // an endpoint we cannot read is an endpoint we cannot match to the gesture
        }
        return false;
    };
    LazyLoadedDeadClicksAutocapture.prototype._selectionTouchesGesture = function (selection, gesture) {
        var _a;
        var target = gesture.path[0];
        var range = selection.rangeCount > 0 ? selection.getRangeAt(0) : undefined;
        var sameRoot = !!range && (0, core_1.trySafe)(function () { return range.startContainer.getRootNode() === target.getRootNode(); });
        if (range && sameRoot && (0, element_utils_1.isElementNode)(target)) {
            if (!range.collapsed) {
                return range.intersectsNode(target);
            }
            // A shadow selection can be represented as a collapsed boundary immediately before
            // its host. Resolve that boundary rather than associating its whole parent container.
            var node = (0, element_utils_1.isElementNode)(range.startContainer)
                ? ((_a = range.startContainer.childNodes[range.startOffset]) !== null && _a !== void 0 ? _a : range.startContainer)
                : range.startContainer;
            return this._selectionIsInGesture(node, gesture);
        }
        return (this._selectionIsInGesture(selection.focusNode, gesture) ||
            this._selectionIsInGesture(selection.anchorNode, gesture));
    };
    LazyLoadedDeadClicksAutocapture.prototype._startVisibilityChangeObserver = function () {
        (0, general_utils_1.addEventListener)(globals_1.document, 'visibilitychange', this._onVisibilityChange);
    };
    // a click that opens a new window/popup may leave the current tab visible, so its only trace is
    // the window losing focus. focus/blur are liveness signals too — they suppress a dead click, and
    // like visibility they never cause one.
    LazyLoadedDeadClicksAutocapture.prototype._startFocusChangeObserver = function () {
        (0, general_utils_1.addEventListener)(globals_2.assignableWindow, 'blur', this._onFocusChange);
        (0, general_utils_1.addEventListener)(globals_2.assignableWindow, 'focus', this._onFocusChange);
    };
    // stamp each queued candidate with how long AFTER its click this liveness signal fired, the
    // moment the event arrives. recording it immediately — rather than reading a single shared
    // timestamp when `_checkClicks` finally runs — is what makes it robust: a hidden tab suspends
    // `_checkClicks`, and by the time it resumes a later transition (the tab coming back) would have
    // overwritten the click-correlated one. keeps the closest transition, only within the window.
    LazyLoadedDeadClicksAutocapture.prototype._recordLivenessSignal = function (field, firedAt) {
        this._clicks.forEach(function (click) {
            var delay = livenessDelayInWindow(firedAt - click.timestamp);
            if ((0, core_1.isNumber)(delay) && ((0, core_1.isUndefined)(click[field]) || delay < click[field])) {
                click[field] = delay;
            }
        });
    };
    // `capture: true` mirrors the scroll observer so we see gestures on nested scrollable
    // elements too. `passive: true` tells the browser we won't call `preventDefault()`.
    LazyLoadedDeadClicksAutocapture.prototype._startSwipeObserver = function () {
        (0, general_utils_1.addEventListener)(globals_2.assignableWindow, 'touchstart', this._onTouchStart, { capture: true, passive: true });
        (0, general_utils_1.addEventListener)(globals_2.assignableWindow, 'touchend', this._onTouchEnd, { capture: true, passive: true });
        // a cancelled gesture (system back-swipe, incoming call, etc.) never fires touchend,
        // so clear the start point to avoid measuring the next gesture from a stale origin
        (0, general_utils_1.addEventListener)(globals_2.assignableWindow, 'touchcancel', this._onTouchCancel, { capture: true, passive: true });
    };
    // whether the page contains surfaces whose response to a gesture we cannot observe —
    // cached, recomputed lazily and only when the DOM has mutated since the last look
    LazyLoadedDeadClicksAutocapture.prototype._pageHasUnobservableSurfaces = function () {
        var domChangedSinceCheck = (0, core_1.isUndefined)(this._surfacesCheckedAt) ||
            ((0, core_1.isNumber)(this._lastMutation) && this._lastMutation > this._surfacesCheckedAt);
        if ((0, core_1.isUndefined)(this._hasUnobservableSurfaces) || domChangedSinceCheck) {
            this._surfacesCheckedAt = Date.now();
            this._hasUnobservableSurfaces = !!(globals_1.document === null || globals_1.document === void 0 ? void 0 : globals_1.document.querySelector(UNOBSERVABLE_SURFACE_SELECTOR));
        }
        return !!this._hasUnobservableSurfaces;
    };
    LazyLoadedDeadClicksAutocapture.prototype._ignore = function (candidate) {
        if (!candidate) {
            return true;
        }
        // swipes run only the shared gates; clicks layer their own intent gates on top
        return candidate.type === 'swipe' ? this._ignoreCommon(candidate) : this._ignoreClick(candidate);
    };
    // gates that apply to every dead candidate, whatever the gesture
    LazyLoadedDeadClicksAutocapture.prototype._ignoreCommon = function (candidate) {
        if ((0, element_utils_1.isElementInToolbar)(candidate.node)) {
            return true;
        }
        var alreadySeenInLastSecond = this._clicks.some(function (c) {
            return (c.type === candidate.type &&
                c.node === candidate.node &&
                Math.abs(c.timestamp - candidate.timestamp) < 1000);
        });
        if (alreadySeenInLastSecond) {
            return true;
        }
        if ((0, element_utils_1.isTag)(candidate.node, 'html') || !(0, element_utils_1.isElementNode)(candidate.node)) {
            return true;
        }
        return !(0, autocapture_utils_1.shouldCaptureDeadClick)(candidate.node, {
            css_selector_ignorelist: this._config.css_selector_ignorelist,
        });
    };
    // click-only gates, layered on top of the shared ones
    LazyLoadedDeadClicksAutocapture.prototype._ignoreClick = function (candidate) {
        // clicks with modifier keys (open in new tab, etc.) are intentional; touch swipes have no modifiers
        if (!this._config.capture_clicks_with_modifier_keys && hasModifierKey(candidate.originalEvent)) {
            return true;
        }
        if (this._ignoreCommon(candidate)) {
            return true;
        }
        // an anchor is a legitimate click activation so we skip it, but a swipe that fails to
        // navigate is the signal we want — which is why anchors are skipped for clicks only.
        // reaching here means _ignoreCommon confirmed an element node, so this is safe to call
        return (0, autocapture_utils_1.shouldSkipDeadClick)(candidate.node);
    };
    LazyLoadedDeadClicksAutocapture.prototype._checkClicks = function () {
        var e_3, _a, _b;
        var _c;
        if (!this._clicks.length) {
            return;
        }
        clearTimeout(this._checkClickTimer);
        this._checkClickTimer = undefined;
        var clicksToCheck = this._clicks;
        this._clicks = [];
        try {
            for (var clicksToCheck_1 = __values(clicksToCheck), clicksToCheck_1_1 = clicksToCheck_1.next(); !clicksToCheck_1_1.done; clicksToCheck_1_1 = clicksToCheck_1.next()) {
                var click = clicksToCheck_1_1.value;
                click.mutationDelayMs =
                    (_c = click.mutationDelayMs) !== null && _c !== void 0 ? _c : (this._lastMutation && click.timestamp <= this._lastMutation
                        ? this._lastMutation - click.timestamp
                        : undefined);
                click.absoluteDelayMs = Date.now() - click.timestamp;
                // Selection/visibility/focus delays are recorded on each candidate as their events fire.
                // The pre-candidate directions are handled in `_queueCandidate`. Nothing uses a shared
                // timestamp here, so a later event cannot overwrite the change related to this candidate.
                var scrollTimeout = checkTimeout(click.scrollDelayMs, this._config.scroll_threshold_ms);
                var selectionChangedTimeout = checkTimeout(click.selectionChangedDelayMs, this._config.selection_change_threshold_ms);
                var mutationTimeout = checkTimeout(click.mutationDelayMs, this._config.mutation_threshold_ms);
                // we want to timeout eventually even if nothing else catches it...
                // we leave a little longer than the maximum threshold to give the other checks a chance to catch it
                var absoluteTimeout = checkTimeout(click.absoluteDelayMs, this._config.mutation_threshold_ms * 1.1);
                var hadScroll = (0, core_1.isNumber)(click.scrollDelayMs) && click.scrollDelayMs < this._config.scroll_threshold_ms;
                var hadMutation = (0, core_1.isNumber)(click.mutationDelayMs) && click.mutationDelayMs < this._config.mutation_threshold_ms;
                var hadSelectionChange = click.selectionChangedDuringGesture ||
                    ((0, core_1.isNumber)(click.selectionChangedDelayMs) &&
                        click.selectionChangedDelayMs < this._config.selection_change_threshold_ms);
                // visibility/focus delays are only ever recorded when already inside the suppression
                // window (see `livenessDelayInWindow`), so their presence alone means "suppress"
                var hadVisibilityChange = (0, core_1.isNumber)(click.visibilityChangedDelayMs);
                var hadFocusChange = (0, core_1.isNumber)(click.focusChangedDelayMs);
                if (hadScroll || hadMutation || hadSelectionChange || hadVisibilityChange || hadFocusChange) {
                    continue;
                }
                if (scrollTimeout || mutationTimeout || absoluteTimeout || selectionChangedTimeout) {
                    var prefix = deadEventName(click);
                    if (prefix === '$dead_swipe') {
                        if (this._deadSwipesCaptured >= this._config.max_dead_swipes_per_page_load) {
                            // the page-load budget for dead swipes is spent — drop, don't requeue
                            continue;
                        }
                        this._deadSwipesCaptured++;
                    }
                    this._onCapture(click, (_b = {},
                        _b["".concat(prefix, "_last_mutation_timestamp")] = this._lastMutation,
                        _b["".concat(prefix, "_event_timestamp")] = click.timestamp,
                        _b["".concat(prefix, "_scroll_timeout")] = scrollTimeout,
                        _b["".concat(prefix, "_mutation_timeout")] = mutationTimeout,
                        _b["".concat(prefix, "_absolute_timeout")] = absoluteTimeout,
                        _b["".concat(prefix, "_selection_changed_timeout")] = selectionChangedTimeout,
                        // visibility changes only ever suppress a dead click, never cause one; kept in the
                        // payload (always false) so the event shape is unchanged for existing consumers
                        _b["".concat(prefix, "_visibility_changed_timeout")] = false,
                        _b));
                }
                else if (click.absoluteDelayMs < this._config.mutation_threshold_ms) {
                    // keep waiting until next check
                    this._clicks.push(click);
                }
            }
        }
        catch (e_3_1) { e_3 = { error: e_3_1 }; }
        finally {
            try {
                if (clicksToCheck_1_1 && !clicksToCheck_1_1.done && (_a = clicksToCheck_1.return)) _a.call(clicksToCheck_1);
            }
            finally { if (e_3) throw e_3.error; }
        }
        this._scheduleCheck();
    };
    LazyLoadedDeadClicksAutocapture.prototype._captureDeadClick = function (click, properties) {
        var _a;
        // TODO need to check safe and captur-able as with autocapture
        // TODO autocaputure config
        var prefix = deadEventName(click);
        this.instance.capture(prefix, __assign(__assign(__assign({}, properties), (0, autocapture_1.autocapturePropertiesForElement)(click.node, {
            e: click.originalEvent,
            maskAllElementAttributes: this.instance.config.mask_all_element_attributes,
            maskAllText: this.instance.config.mask_all_text,
            elementAttributeIgnoreList: this._config.element_attribute_ignorelist,
            // TRICKY: it appears that we were moving to elementsChainAsString, but the UI still depends on elements, so :shrug:
            elementsChainAsString: false,
            disableCaptureUrlHashes: this.instance.config.disable_capture_url_hashes,
        }).props), (_a = {}, _a["".concat(prefix, "_scroll_delay_ms")] = click.scrollDelayMs, _a["".concat(prefix, "_mutation_delay_ms")] = click.mutationDelayMs, _a["".concat(prefix, "_absolute_delay_ms")] = click.absoluteDelayMs, _a["".concat(prefix, "_selection_changed_delay_ms")] = click.selectionChangedDelayMs, _a["".concat(prefix, "_visibility_changed_delay_ms")] = click.visibilityChangedDelayMs, _a["".concat(prefix, "_focus_changed_delay_ms")] = click.focusChangedDelayMs, 
        // undefined for clicks (stripped on serialization), like the delay fields above
        _a.$dead_swipe_direction = click.swipeDirection, _a.$dead_swipe_distance_px = click.swipeDistancePx, _a)), {
            timestamp: new Date(click.timestamp),
        });
    };
    return LazyLoadedDeadClicksAutocapture;
}());
globals_2.assignableWindow.__PosthogExtensions__ = globals_2.assignableWindow.__PosthogExtensions__ || {};
globals_2.assignableWindow.__PosthogExtensions__.initDeadClicksAutocapture = function (ph, config) {
    return new LazyLoadedDeadClicksAutocapture(ph, config);
};
exports.default = LazyLoadedDeadClicksAutocapture;
//# sourceMappingURL=dead-clicks-autocapture.js.map