diff --git a/src/__tests__/entrypoints/lazy-loaded-dead-clicks-autocapture.test.ts b/src/__tests__/entrypoints/lazy-loaded-dead-clicks-autocapture.test.ts index 95edc40f1..7588cb264 100644 --- a/src/__tests__/entrypoints/lazy-loaded-dead-clicks-autocapture.test.ts +++ b/src/__tests__/entrypoints/lazy-loaded-dead-clicks-autocapture.test.ts @@ -211,32 +211,35 @@ describe('LazyLoadedDeadClicksAutocapture', () => { lazyLoadedDeadClicksAutocapture['_checkClicks']() expect(lazyLoadedDeadClicksAutocapture['_clicks']).toHaveLength(0) - expect(fakeInstance.capture).toHaveBeenCalledWith('$dead_click', { - // faked system timestamp isn't moving so this is negative - $dead_click_absolute_delay_ms: -900, - $dead_click_absolute_timeout: false, - $dead_click_event_timestamp: 900, - $dead_click_last_mutation_timestamp: undefined, - $dead_click_last_scroll_timestamp: undefined, - $dead_click_mutation_delay_ms: undefined, - $dead_click_mutation_timeout: false, - $dead_click_scroll_delay_ms: undefined, - $dead_click_scroll_timeout: false, - $dead_click_selection_changed_delay_ms: 100, - $dead_click_selection_changed_timeout: true, - timestamp: 900, - $ce_version: 1, - $el_text: 'text', - $elements: [ - { - $el_text: 'text', - nth_child: 2, - nth_of_type: 1, - tag_name: 'body', - }, - ], - $event_type: 'click', - }) + expect(fakeInstance.capture).toHaveBeenCalledWith( + '$dead_click', + { + // faked system timestamp isn't moving so this is negative + $dead_click_absolute_delay_ms: -900, + $dead_click_absolute_timeout: false, + $dead_click_event_timestamp: 900, + $dead_click_last_mutation_timestamp: undefined, + $dead_click_last_scroll_timestamp: undefined, + $dead_click_mutation_delay_ms: undefined, + $dead_click_mutation_timeout: false, + $dead_click_scroll_delay_ms: undefined, + $dead_click_scroll_timeout: false, + $dead_click_selection_changed_delay_ms: 100, + $dead_click_selection_changed_timeout: true, + $ce_version: 1, + $el_text: 'text', + $elements: [ + { + $el_text: 'text', + nth_child: 2, + nth_of_type: 1, + tag_name: 'body', + }, + ], + $event_type: 'click', + }, + { timestamp: new Date(900) } + ) }) it('click followed by a mutation after threshold, dead click', () => { @@ -250,32 +253,35 @@ describe('LazyLoadedDeadClicksAutocapture', () => { lazyLoadedDeadClicksAutocapture['_checkClicks']() expect(lazyLoadedDeadClicksAutocapture['_clicks']).toHaveLength(0) - expect(fakeInstance.capture).toHaveBeenCalledWith('$dead_click', { - // faked system timestamp isn't moving so this is negative - $dead_click_absolute_delay_ms: -900, - $dead_click_absolute_timeout: false, - $dead_click_event_timestamp: 900, - $dead_click_last_mutation_timestamp: 3401, - $dead_click_last_scroll_timestamp: undefined, - $dead_click_mutation_delay_ms: 2501, - $dead_click_mutation_timeout: true, - $dead_click_scroll_delay_ms: undefined, - $dead_click_scroll_timeout: false, - $dead_click_selection_changed_delay_ms: undefined, - $dead_click_selection_changed_timeout: false, - timestamp: 900, - $ce_version: 1, - $el_text: 'text', - $elements: [ - { - $el_text: 'text', - nth_child: 2, - nth_of_type: 1, - tag_name: 'body', - }, - ], - $event_type: 'click', - }) + expect(fakeInstance.capture).toHaveBeenCalledWith( + '$dead_click', + { + // faked system timestamp isn't moving so this is negative + $dead_click_absolute_delay_ms: -900, + $dead_click_absolute_timeout: false, + $dead_click_event_timestamp: 900, + $dead_click_last_mutation_timestamp: 3401, + $dead_click_last_scroll_timestamp: undefined, + $dead_click_mutation_delay_ms: 2501, + $dead_click_mutation_timeout: true, + $dead_click_scroll_delay_ms: undefined, + $dead_click_scroll_timeout: false, + $dead_click_selection_changed_delay_ms: undefined, + $dead_click_selection_changed_timeout: false, + $ce_version: 1, + $el_text: 'text', + $elements: [ + { + $el_text: 'text', + nth_child: 2, + nth_of_type: 1, + tag_name: 'body', + }, + ], + $event_type: 'click', + }, + { timestamp: new Date(900) } + ) }) it('click followed by a scroll after threshold, dead click', () => { @@ -290,31 +296,34 @@ describe('LazyLoadedDeadClicksAutocapture', () => { lazyLoadedDeadClicksAutocapture['_checkClicks']() expect(lazyLoadedDeadClicksAutocapture['_clicks']).toHaveLength(0) - expect(fakeInstance.capture).toHaveBeenCalledWith('$dead_click', { - // faked system timestamp isn't moving so this is negative - $dead_click_absolute_delay_ms: -900, - $dead_click_absolute_timeout: false, - $dead_click_event_timestamp: 900, - $dead_click_last_mutation_timestamp: undefined, - $dead_click_mutation_delay_ms: undefined, - $dead_click_mutation_timeout: false, - $dead_click_scroll_delay_ms: 2501, - $dead_click_scroll_timeout: true, - $dead_click_selection_changed_delay_ms: undefined, - $dead_click_selection_changed_timeout: false, - $ce_version: 1, - $el_text: 'text', - $elements: [ - { - $el_text: 'text', - nth_child: 2, - nth_of_type: 1, - tag_name: 'body', - }, - ], - $event_type: 'click', - timestamp: 900, - }) + expect(fakeInstance.capture).toHaveBeenCalledWith( + '$dead_click', + { + // faked system timestamp isn't moving so this is negative + $dead_click_absolute_delay_ms: -900, + $dead_click_absolute_timeout: false, + $dead_click_event_timestamp: 900, + $dead_click_last_mutation_timestamp: undefined, + $dead_click_mutation_delay_ms: undefined, + $dead_click_mutation_timeout: false, + $dead_click_scroll_delay_ms: 2501, + $dead_click_scroll_timeout: true, + $dead_click_selection_changed_delay_ms: undefined, + $dead_click_selection_changed_timeout: false, + $ce_version: 1, + $el_text: 'text', + $elements: [ + { + $el_text: 'text', + nth_child: 2, + nth_of_type: 1, + tag_name: 'body', + }, + ], + $event_type: 'click', + }, + { timestamp: new Date(900) } + ) }) it('click followed by nothing for too long, dead click', () => { @@ -325,35 +334,38 @@ describe('LazyLoadedDeadClicksAutocapture', () => { }) lazyLoadedDeadClicksAutocapture['_lastMutation'] = undefined - jest.setSystemTime(2501 + 900) + jest.setSystemTime(3001 + 900) lazyLoadedDeadClicksAutocapture['_checkClicks']() expect(lazyLoadedDeadClicksAutocapture['_clicks']).toHaveLength(0) - expect(fakeInstance.capture).toHaveBeenCalledWith('$dead_click', { - $dead_click_absolute_delay_ms: 2501, - $dead_click_absolute_timeout: true, - $dead_click_event_timestamp: 900, - $dead_click_last_mutation_timestamp: undefined, - $dead_click_last_scroll_timestamp: undefined, - $dead_click_mutation_delay_ms: undefined, - $dead_click_mutation_timeout: false, - $dead_click_scroll_delay_ms: undefined, - $dead_click_scroll_timeout: false, - $dead_click_selection_changed_delay_ms: undefined, - $dead_click_selection_changed_timeout: false, - $ce_version: 1, - $el_text: 'text', - $elements: [ - { - $el_text: 'text', - nth_child: 2, - nth_of_type: 1, - tag_name: 'body', - }, - ], - $event_type: 'click', - timestamp: 900, - }) + expect(fakeInstance.capture).toHaveBeenCalledWith( + '$dead_click', + { + $dead_click_absolute_delay_ms: 3001, + $dead_click_absolute_timeout: true, + $dead_click_event_timestamp: 900, + $dead_click_last_mutation_timestamp: undefined, + $dead_click_last_scroll_timestamp: undefined, + $dead_click_mutation_delay_ms: undefined, + $dead_click_mutation_timeout: false, + $dead_click_scroll_delay_ms: undefined, + $dead_click_scroll_timeout: false, + $dead_click_selection_changed_delay_ms: undefined, + $dead_click_selection_changed_timeout: false, + $ce_version: 1, + $el_text: 'text', + $elements: [ + { + $el_text: 'text', + nth_child: 2, + nth_of_type: 1, + tag_name: 'body', + }, + ], + $event_type: 'click', + }, + { timestamp: new Date(900) } + ) }) it('click not followed by anything within threshold, rescheduled for next check', () => { diff --git a/src/autocapture-utils.ts b/src/autocapture-utils.ts index c2e776f0b..00ca3883f 100644 --- a/src/autocapture-utils.ts +++ b/src/autocapture-utils.ts @@ -4,6 +4,7 @@ import { each, entries, includes, trim } from './utils' import { isArray, isNullish, isString, isUndefined } from './utils/type-utils' import { logger } from './utils/logger' import { window } from './utils/globals' +import { isDocumentFragment, isElementNode, isTag, isTextNode } from './utils/element-utils' export function splitClassString(s: string): string[] { return s ? trim(s).split(/\s+/) : [] @@ -94,47 +95,6 @@ export function getEventTarget(e: Event): Element | null { } } -/* - * Check whether an element has nodeType Node.ELEMENT_NODE - * @param {Element} el - element to check - * @returns {boolean} whether el is of the correct nodeType - */ -export function isElementNode(el: Node | Element | undefined | null): el is Element { - return !!el && el.nodeType === 1 // Node.ELEMENT_NODE - use integer constant for browser portability -} - -/* - * Check whether an element is of a given tag type. - * Due to potential reference discrepancies (such as the webcomponents.js polyfill), - * we want to match tagNames instead of specific references because something like - * element === document.body won't always work because element might not be a native - * element. - * @param {Element} el - element to check - * @param {string} tag - tag name (e.g., "div") - * @returns {boolean} whether el is of the given tag type - */ -export function isTag(el: Element | undefined | null, tag: string): el is HTMLElement { - return !!el && !!el.tagName && el.tagName.toLowerCase() === tag.toLowerCase() -} - -/* - * Check whether an element has nodeType Node.TEXT_NODE - * @param {Element} el - element to check - * @returns {boolean} whether el is of the correct nodeType - */ -export function isTextNode(el: Element | undefined | null): el is HTMLElement { - return !!el && el.nodeType === 3 // Node.TEXT_NODE - use integer constant for browser portability -} - -/* - * Check whether an element has nodeType Node.DOCUMENT_FRAGMENT_NODE - * @param {Element} el - element to check - * @returns {boolean} whether el is of the correct nodeType - */ -export function isDocumentFragment(el: Element | ParentNode | undefined | null): el is DocumentFragment { - return !!el && el.nodeType === 11 // Node.DOCUMENT_FRAGMENT_NODE - use integer constant for browser portability -} - export const autocaptureCompatibleElements = ['a', 'button', 'form', 'input', 'select', 'textarea', 'label'] /* diff --git a/src/autocapture.ts b/src/autocapture.ts index 96262452d..8fc25d580 100644 --- a/src/autocapture.ts +++ b/src/autocapture.ts @@ -7,11 +7,7 @@ import { getEventTarget, getSafeText, isAngularStyleAttr, - isDocumentFragment, - isElementNode, isSensitiveElement, - isTag, - isTextNode, makeSafeText, shouldCaptureDomEvent, shouldCaptureElement, @@ -27,6 +23,7 @@ import { isBoolean, isFunction, isNull, isObject } from './utils/type-utils' import { logger } from './utils/logger' import { document, window } from './utils/globals' import { convertToURL } from './utils/request-utils' +import { isDocumentFragment, isElementNode, isTag, isTextNode } from './utils/element-utils' const COPY_AUTOCAPTURE_EVENT = '$copy_autocapture' diff --git a/src/entrypoints/dead-clicks-autocapture.ts b/src/entrypoints/dead-clicks-autocapture.ts index 80da3d6c7..f60c33177 100644 --- a/src/entrypoints/dead-clicks-autocapture.ts +++ b/src/entrypoints/dead-clicks-autocapture.ts @@ -1,10 +1,10 @@ import { assignableWindow, LazyLoadedDeadClicksAutocaptureInterface } from '../utils/globals' import { PostHog } from '../posthog-core' import { isNull, isNumber, isUndefined } from '../utils/type-utils' -import { autocaptureCompatibleElements, getEventTarget, isElementNode, isTag } from '../autocapture-utils' +import { autocaptureCompatibleElements, getEventTarget } from '../autocapture-utils' import { DeadClicksAutoCaptureConfig, Properties } from '../types' import { autocapturePropertiesForElement } from '../autocapture' -import { isElementInToolbar } from '../utils/element-utils' +import { isElementInToolbar, isElementNode, isTag } from '../utils/element-utils' const DEFAULT_CONFIG: Required = { element_attribute_ignorelist: [], @@ -205,7 +205,9 @@ class LazyLoadedDeadClicksAutocapture implements LazyLoadedDeadClicksAutocapture this._config.selection_change_threshold_ms ) const mutationTimeout = checkTimeout(click.mutationDelayMs, this._config.mutation_threshold_ms) - const absoluteTimeout = checkTimeout(click.absoluteDelayMs, 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 + const absoluteTimeout = checkTimeout(click.absoluteDelayMs, this._config.mutation_threshold_ms * 1.1) const hadScroll = isNumber(click.scrollDelayMs) && click.scrollDelayMs < this._config.scroll_threshold_ms const hadMutation = @@ -244,22 +246,27 @@ class LazyLoadedDeadClicksAutocapture implements LazyLoadedDeadClicksAutocapture private _captureDeadClick(click: Click, properties: Properties) { // TODO need to check safe and captur-able as with autocapture // TODO autocaputure config - this.instance.capture('$dead_click', { - ...properties, - ...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, - }).props, - $dead_click_scroll_delay_ms: click.scrollDelayMs, - $dead_click_mutation_delay_ms: click.mutationDelayMs, - $dead_click_absolute_delay_ms: click.absoluteDelayMs, - $dead_click_selection_changed_delay_ms: click.selectionChangedDelayMs, - timestamp: click.timestamp, - }) + this.instance.capture( + '$dead_click', + { + ...properties, + ...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, + }).props, + $dead_click_scroll_delay_ms: click.scrollDelayMs, + $dead_click_mutation_delay_ms: click.mutationDelayMs, + $dead_click_absolute_delay_ms: click.absoluteDelayMs, + $dead_click_selection_changed_delay_ms: click.selectionChangedDelayMs, + }, + { + timestamp: new Date(click.timestamp), + } + ) } } diff --git a/src/heatmaps.ts b/src/heatmaps.ts index 6f9ac31c8..ca6b99d5a 100644 --- a/src/heatmaps.ts +++ b/src/heatmaps.ts @@ -4,11 +4,11 @@ import { DecideResponse, Properties } from './types' import { PostHog } from './posthog-core' import { document, window } from './utils/globals' -import { getEventTarget, getParentElement, isElementNode, isTag } from './autocapture-utils' +import { getEventTarget, getParentElement } from './autocapture-utils' import { HEATMAPS_ENABLED_SERVER_SIDE } from './constants' import { isEmptyObject, isObject, isUndefined } from './utils/type-utils' import { logger } from './utils/logger' -import { isElementInToolbar } from './utils/element-utils' +import { isElementInToolbar, isElementNode, isTag } from './utils/element-utils' const DEFAULT_FLUSH_INTERVAL = 5000 const HEATMAPS = 'heatmaps' diff --git a/src/utils/element-utils.ts b/src/utils/element-utils.ts index 02cf7eaca..9a9b43768 100644 --- a/src/utils/element-utils.ts +++ b/src/utils/element-utils.ts @@ -4,3 +4,44 @@ export function isElementInToolbar(el: Element): boolean { // NOTE: .closest is not supported in IE11 hence the operator check return el.id === TOOLBAR_ID || !!el.closest?.('#' + TOOLBAR_ID) } + +/* + * Check whether an element has nodeType Node.ELEMENT_NODE + * @param {Element} el - element to check + * @returns {boolean} whether el is of the correct nodeType + */ +export function isElementNode(el: Node | Element | undefined | null): el is Element { + return !!el && el.nodeType === 1 // Node.ELEMENT_NODE - use integer constant for browser portability +} + +/* + * Check whether an element is of a given tag type. + * Due to potential reference discrepancies (such as the webcomponents.js polyfill), + * we want to match tagNames instead of specific references because something like + * element === document.body won't always work because element might not be a native + * element. + * @param {Element} el - element to check + * @param {string} tag - tag name (e.g., "div") + * @returns {boolean} whether el is of the given tag type + */ +export function isTag(el: Element | undefined | null, tag: string): el is HTMLElement { + return !!el && !!el.tagName && el.tagName.toLowerCase() === tag.toLowerCase() +} + +/* + * Check whether an element has nodeType Node.TEXT_NODE + * @param {Element} el - element to check + * @returns {boolean} whether el is of the correct nodeType + */ +export function isTextNode(el: Element | undefined | null): el is HTMLElement { + return !!el && el.nodeType === 3 // Node.TEXT_NODE - use integer constant for browser portability +} + +/* + * Check whether an element has nodeType Node.DOCUMENT_FRAGMENT_NODE + * @param {Element} el - element to check + * @returns {boolean} whether el is of the correct nodeType + */ +export function isDocumentFragment(el: Element | ParentNode | undefined | null): el is DocumentFragment { + return !!el && el.nodeType === 11 // Node.DOCUMENT_FRAGMENT_NODE - use integer constant for browser portability +}