UNPKG

posthog-js

Version:

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

466 lines • 23.7 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 __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Autocapture = void 0; exports.getAugmentPropertiesFromElement = getAugmentPropertiesFromElement; exports.previousElementSibling = previousElementSibling; exports.getDefaultProperties = getDefaultProperties; exports.getPropertiesFromElement = getPropertiesFromElement; exports.autocapturePropertiesForElement = autocapturePropertiesForElement; var general_utils_1 = require("@posthog/browser-common/utils/general-utils"); var autocapture_utils_1 = require("@posthog/browser-common/utils/autocapture-utils"); var rageclick_1 = __importDefault(require("./extensions/rageclick")); var constants_1 = require("./constants"); var extension_tokens_1 = require("./extension-tokens"); var core_1 = require("@posthog/core"); var logger_1 = require("@posthog/browser-common/utils/logger"); var globals_1 = require("@posthog/browser-common/utils/globals"); var request_utils_1 = require("@posthog/browser-common/utils/request-utils"); var element_utils_1 = require("@posthog/browser-common/utils/element-utils"); var core_2 = require("@posthog/core"); var COPY_AUTOCAPTURE_EVENT = '$copy_autocapture'; var logger = (0, logger_1.createLogger)('[AutoCapture]'); function limitText(length, text) { if (text.length > length) { return text.slice(0, length) + '...'; } return text; } function getAugmentPropertiesFromElement(elem) { var shouldCaptureEl = (0, autocapture_utils_1.shouldCaptureElement)(elem); if (!shouldCaptureEl) { return {}; } var props = {}; (0, general_utils_1.each)(elem.attributes, function (attr) { if (attr.name && attr.name.indexOf('data-ph-capture-attribute') === 0) { var propertyKey = attr.name.replace('data-ph-capture-attribute-', ''); var propertyValue = attr.value; if (propertyKey && propertyValue && (0, autocapture_utils_1.shouldCaptureValue)(propertyValue)) { props[propertyKey] = propertyValue; } } }); return props; } function previousElementSibling(el) { if (el.previousElementSibling) { return el.previousElementSibling; } var _el = el; do { _el = _el.previousSibling; // resolves to ChildNode->Node, which is Element's parent class } while (_el && !(0, element_utils_1.isElementNode)(_el)); return _el; } function getDefaultProperties(eventType) { return { $event_type: eventType, $ce_version: 1, }; } function getPropertiesFromElement(elem, maskAllAttributes, maskText, elementAttributeIgnorelist, disableCaptureUrlHashes) { if (disableCaptureUrlHashes === void 0) { disableCaptureUrlHashes = false; } var tag_name = elem.tagName.toLowerCase(); var props = { tag_name: tag_name, }; if (autocapture_utils_1.autocaptureCompatibleElements.indexOf(tag_name) > -1 && !maskText) { if (tag_name.toLowerCase() === 'a' || tag_name.toLowerCase() === 'button') { props['$el_text'] = limitText(1024, (0, autocapture_utils_1.getDirectAndNestedSpanText)(elem)); } else { props['$el_text'] = limitText(1024, (0, autocapture_utils_1.getSafeText)(elem)); } } var classes = (0, autocapture_utils_1.getClassNames)(elem); if (classes.length > 0) props['classes'] = classes.filter(function (c) { return c !== ''; }); // capture the deny list here because this not-a-class class makes it tricky to use this.config in the function below (0, general_utils_1.each)(elem.attributes, function (attr) { // Only capture attributes we know are safe if ((0, autocapture_utils_1.isSensitiveElement)(elem) && ['name', 'id', 'class', 'aria-label'].indexOf(attr.name) === -1) return; if (elementAttributeIgnorelist === null || elementAttributeIgnorelist === void 0 ? void 0 : elementAttributeIgnorelist.includes(attr.name)) return; if (!maskAllAttributes && (0, autocapture_utils_1.shouldCaptureValue)(attr.value) && !(0, autocapture_utils_1.isAngularStyleAttr)(attr.name)) { var value = attr.value; if (attr.name === 'class') { // html attributes can _technically_ contain linebreaks, // but we're very intolerant of them in the class string, // so we strip them. value = (0, autocapture_utils_1.splitClassString)(value).join(' '); } props['attr__' + attr.name] = limitText(1024, attr.name === 'href' && disableCaptureUrlHashes ? (0, core_1.stripUrlHash)(value) : value); } }); var nthChild = 1; var nthOfType = 1; var currentElem = elem; while ((currentElem = previousElementSibling(currentElem))) { nthChild++; if (currentElem.tagName === elem.tagName) { nthOfType++; } } props['nth_child'] = nthChild; props['nth_of_type'] = nthOfType; return props; } function autocapturePropertiesForElement(target, _a) { var _b, _c, _d, _e; var e = _a.e, maskAllElementAttributes = _a.maskAllElementAttributes, maskAllText = _a.maskAllText, elementAttributeIgnoreList = _a.elementAttributeIgnoreList, elementsChainAsString = _a.elementsChainAsString, disableCaptureUrlHashes = _a.disableCaptureUrlHashes; if (!(0, element_utils_1.isElementNode)(target)) { return { props: {} }; } var targetElementList = [target]; var seen = new Set([target]); var curEl = target; while (curEl.parentNode && !(0, element_utils_1.isTag)(curEl, 'body')) { // bail out of abnormally deep or cyclic (patched parentNode) ancestor chains if (targetElementList.length >= autocapture_utils_1.MAX_DOM_ANCESTOR_DEPTH) { break; } if ((0, element_utils_1.isShadowRoot)(curEl.parentNode)) { var host = curEl.parentNode.host; if (seen.has(host)) { break; } seen.add(host); targetElementList.push(host); curEl = host; continue; } if (!(0, element_utils_1.isElementNode)(curEl.parentNode)) { break; } if (seen.has(curEl.parentNode)) { break; } seen.add(curEl.parentNode); targetElementList.push(curEl.parentNode); curEl = curEl.parentNode; } var elementsJson = []; var autocaptureAugmentProperties = {}; var href = false; var explicitNoCapture = false; (0, general_utils_1.each)(targetElementList, function (el) { var shouldCaptureEl = (0, autocapture_utils_1.shouldCaptureElement)(el); // if the element or a parent element is an anchor tag // include the href as a property if ((0, element_utils_1.isTag)(el, 'a')) { var hrefAttr = el.getAttribute('href'); href = shouldCaptureEl && !!hrefAttr && (0, autocapture_utils_1.shouldCaptureValue)(hrefAttr) ? disableCaptureUrlHashes ? (0, core_1.stripUrlHash)(hrefAttr) : hrefAttr : false; } // allow users to programmatically prevent capturing of elements by adding class 'ph-no-capture' var classes = (0, autocapture_utils_1.getClassNames)(el); if ((0, core_2.includes)(classes, 'ph-no-capture')) { explicitNoCapture = true; } elementsJson.push(getPropertiesFromElement(el, maskAllElementAttributes, maskAllText, elementAttributeIgnoreList, disableCaptureUrlHashes)); var augmentProperties = getAugmentPropertiesFromElement(el); (0, general_utils_1.each)(augmentProperties, function (value, key) { // Elements are ordered from the target to its ancestors, so retain the closest value. if (!Object.prototype.hasOwnProperty.call(autocaptureAugmentProperties, key)) { autocaptureAugmentProperties[key] = value; } }); }); if (explicitNoCapture) { return { props: {}, explicitNoCapture: explicitNoCapture }; } if (!maskAllText) { // if the element is a button or anchor tag get the span text from any // children and include it as/with the text property on the parent element if ((0, element_utils_1.isTag)(target, 'a') || (0, element_utils_1.isTag)(target, 'button')) { elementsJson[0]['$el_text'] = (0, autocapture_utils_1.getDirectAndNestedSpanText)(target); } else { elementsJson[0]['$el_text'] = (0, autocapture_utils_1.getSafeText)(target); } } var externalHref; if (href) { elementsJson[0]['attr__href'] = href; var hrefHost = (_b = (0, request_utils_1.convertToURL)(href)) === null || _b === void 0 ? void 0 : _b.host; var locationHost = (_c = globals_1.window === null || globals_1.window === void 0 ? void 0 : globals_1.window.location) === null || _c === void 0 ? void 0 : _c.host; if (hrefHost && locationHost && hrefHost !== locationHost) { externalHref = href; } } var props = (0, general_utils_1.extend)(getDefaultProperties(e.type), // Sending "$elements" is deprecated. Only one client on US cloud uses this. !elementsChainAsString ? { $elements: elementsJson } : {}, // Always send $elements_chain, as it's needed downstream in site app filtering { $elements_chain: (0, autocapture_utils_1.getElementsChainString)(elementsJson) }, ((_d = elementsJson[0]) === null || _d === void 0 ? void 0 : _d['$el_text']) ? { $el_text: (_e = elementsJson[0]) === null || _e === void 0 ? void 0 : _e['$el_text'] } : {}, externalHref && e.type === 'click' ? { $external_click_url: externalHref } : {}, autocaptureAugmentProperties); return { props: props }; } var Autocapture = /** @class */ (function () { function Autocapture(_configSource) { this._configSource = _configSource; this.name = extension_tokens_1.AutocaptureExtension; this._initialized = false; this._isDisabledServerSide = null; this._hasReceivedConfigResponse = false; this._elementsChainAsString = false; this._config = { enabled: false, rageclick: false, maskAllElementAttributes: false, maskAllText: false, disableCaptureUrlHashes: false, remoteRequestsDisabled: false, }; this._disposed = false; this._configSource.refresh(this._config); this.rageclicks = new rageclick_1.default(this._config.rageclick); this._elementSelectors = null; } Autocapture.prototype.setup = function (client) { this._compileUrlPatterns(); this._client = client; var subscription = client.onRemoteConfig(this.onRemoteConfig.bind(this)); if (this._disposed) { subscription.dispose(); return; } this._remoteConfigSubscription = subscription; this.startIfEnabled(); }; Autocapture.prototype.dispose = function () { var _a; if (this._disposed) { return; } this._disposed = true; this._client = undefined; (_a = this._remoteConfigSubscription) === null || _a === void 0 ? void 0 : _a.dispose(); this._remoteConfigSubscription = undefined; this._removeDomEventHandlers(); }; Autocapture.prototype._refreshConfig = function () { this._configSource.refresh(this._config); return this._config; }; Autocapture.prototype._compileUrlPatterns = function () { var _a, _b; this._refreshConfig(); this._config.url_allowlist = (_a = this._config.url_allowlist) === null || _a === void 0 ? void 0 : _a.map(function (url) { return new RegExp(url); }); this._config.url_ignorelist = (_b = this._config.url_ignorelist) === null || _b === void 0 ? void 0 : _b.map(function (url) { return new RegExp(url); }); return this._config; }; Autocapture.prototype._addDomEventHandlers = function () { var _this = this; if (!this.isBrowserSupported()) { logger.info('Disabling Automatic Event Collection because this browser is not supported'); return; } if (!globals_1.window || !globals_1.document) { return; } var handler = (this._domEventHandler = function (e) { e = e || (globals_1.window === null || globals_1.window === void 0 ? void 0 : globals_1.window.event); try { _this._captureEvent(e); } catch (error) { logger.error('Failed to capture event', error); } }); (0, general_utils_1.addEventListener)(globals_1.document, 'submit', handler, { capture: true }); (0, general_utils_1.addEventListener)(globals_1.document, 'change', handler, { capture: true }); (0, general_utils_1.addEventListener)(globals_1.document, 'click', handler, { capture: true }); if (this._refreshConfig().capture_copied_text) { var copiedTextHandler = (this._copiedTextHandler = function (e) { e = e || (globals_1.window === null || globals_1.window === void 0 ? void 0 : globals_1.window.event); try { _this._captureEvent(e, COPY_AUTOCAPTURE_EVENT); } catch (error) { logger.error('Failed to capture clipboard event', error); } }); (0, general_utils_1.addEventListener)(globals_1.document, 'copy', copiedTextHandler, { capture: true }); (0, general_utils_1.addEventListener)(globals_1.document, 'cut', copiedTextHandler, { capture: true }); (0, general_utils_1.addEventListener)(globals_1.document, 'paste', copiedTextHandler, { capture: true }); } }; Autocapture.prototype._removeDomEventHandlers = function () { if (this._domEventHandler) { globals_1.document === null || globals_1.document === void 0 ? void 0 : globals_1.document.removeEventListener('submit', this._domEventHandler, true); globals_1.document === null || globals_1.document === void 0 ? void 0 : globals_1.document.removeEventListener('change', this._domEventHandler, true); globals_1.document === null || globals_1.document === void 0 ? void 0 : globals_1.document.removeEventListener('click', this._domEventHandler, true); this._domEventHandler = undefined; } if (this._copiedTextHandler) { globals_1.document === null || globals_1.document === void 0 ? void 0 : globals_1.document.removeEventListener('copy', this._copiedTextHandler, true); globals_1.document === null || globals_1.document === void 0 ? void 0 : globals_1.document.removeEventListener('cut', this._copiedTextHandler, true); globals_1.document === null || globals_1.document === void 0 ? void 0 : globals_1.document.removeEventListener('paste', this._copiedTextHandler, true); this._copiedTextHandler = undefined; } this._initialized = false; }; Autocapture.prototype.startIfEnabled = function () { if (!this._disposed && this._client && this.isEnabled && !this._initialized) { this._addDomEventHandlers(); this._initialized = true; } }; Autocapture.prototype.onRemoteConfig = function (result) { var _a; if (this._disposed) { return; } this._hasReceivedConfigResponse = true; if (!result.ok) { // Failed fetch = opt-out unknown: keep the last known persisted server // value instead of defaulting to enabled, so a network error cannot turn // autocapture on for an opted-out project. No persistence write. this.startIfEnabled(); return; } var response = result.config; if (response.elementsChainAsString) { this._elementsChainAsString = response.elementsChainAsString; } // A missing autocapture_opt_out carries no opt-out information: // keep the last known server value, as with a failed fetch. var optOut = response['autocapture_opt_out']; if ((0, core_1.isBoolean)(optOut)) { (_a = this._client) === null || _a === void 0 ? void 0 : _a.kv.set(constants_1.AUTOCAPTURE_DISABLED_SERVER_SIDE, optOut); // store this in-memory in case persistence is disabled this._isDisabledServerSide = optOut; } this.startIfEnabled(); }; Autocapture.prototype.setElementSelectors = function (selectors) { this._elementSelectors = selectors; }; Autocapture.prototype.getElementSelectors = function (element) { var _a; var elementSelectors = []; (_a = this._elementSelectors) === null || _a === void 0 ? void 0 : _a.forEach(function (selector) { var matchedElements = globals_1.document === null || globals_1.document === void 0 ? void 0 : globals_1.document.querySelectorAll(selector); matchedElements === null || matchedElements === void 0 ? void 0 : matchedElements.forEach(function (matchedElement) { if (element === matchedElement) { elementSelectors.push(selector); } }); }); return elementSelectors; }; Object.defineProperty(Autocapture.prototype, "isEnabled", { get: function () { var _a, _b; if (this._disposed) { return false; } var persistedServerDisabled = (_a = this._client) === null || _a === void 0 ? void 0 : _a.kv.get(constants_1.AUTOCAPTURE_DISABLED_SERVER_SIDE); var memoryDisabled = this._isDisabledServerSide; // The /flags-disabled bypass only applies while no config outcome has arrived; // once a response (or failure) has been seen, an unknown opt-out stays off. var config = this._refreshConfig(); var clientConfigOnly = config.remoteRequestsDisabled && !this._hasReceivedConfigResponse; if ((0, core_1.isNull)(memoryDisabled) && !(0, core_1.isBoolean)(persistedServerDisabled) && !clientConfigOnly) { // We only enable if we know that the server has not disabled it return false; } var disabledServer = (_b = this._isDisabledServerSide) !== null && _b !== void 0 ? _b : !!persistedServerDisabled; var disabledClient = !config.enabled; return !disabledClient && !disabledServer; }, enumerable: false, configurable: true }); Autocapture.prototype._captureEvent = function (e, eventName) { var _a, _b, _c, _d; if (eventName === void 0) { eventName = '$autocapture'; } if (!this.isEnabled) { return; } /*** Don't mess with this code without running IE8 tests on it ***/ var target = (0, autocapture_utils_1.getEventTarget)(e); if ((0, element_utils_1.isTextNode)(target)) { // defeat Safari bug (see: http://www.quirksmode.org/js/events_properties.html) target = (target.parentNode || null); } var config = this._compileUrlPatterns(); if (eventName === '$autocapture' && e.type === 'click' && e instanceof MouseEvent) { if (!!config.rageclick && ((_a = this.rageclicks) === null || _a === void 0 ? void 0 : _a.isRageClick(e.clientX, e.clientY, e.timeStamp || new Date().getTime()))) { if ((0, autocapture_utils_1.shouldCaptureRageclick)(target, config.rageclick)) { this._captureEvent(e, '$rageclick'); } } } var isClipboardAutocapture = eventName === COPY_AUTOCAPTURE_EVENT; var eventConfig = isClipboardAutocapture ? __assign(__assign({}, config), { dom_event_allowlist: undefined }) : config; if (target && (0, autocapture_utils_1.shouldCaptureDomEvent)(target, e, eventConfig, // mostly this method cares about the target element, but for clipboard events, // we want some of the work this check does without insisting on the target element's type isClipboardAutocapture, // we also don't want to restrict clipboard checks to clicks, // so we pass that knowledge in here, rather than add the logic inside the check isClipboardAutocapture ? ['copy', 'cut', 'paste'] : undefined, { config: { get_current_url: config.getCurrentUrl } })) { var _e = autocapturePropertiesForElement(target, { e: e, maskAllElementAttributes: config.maskAllElementAttributes, maskAllText: config.maskAllText, elementAttributeIgnoreList: config.element_attribute_ignorelist, elementsChainAsString: this._elementsChainAsString, disableCaptureUrlHashes: config.disableCaptureUrlHashes, }), props = _e.props, explicitNoCapture = _e.explicitNoCapture; if (explicitNoCapture) { return false; } var elementSelectors = this.getElementSelectors(target); if (elementSelectors && elementSelectors.length > 0) { props['$element_selectors'] = elementSelectors; } if (eventName === COPY_AUTOCAPTURE_EVENT) { var clipType = e.type || 'clipboard'; // Don't add the contents for paste events, as usually the page emits an input change with the new value if (clipType !== 'paste') { var selectedText = (_b = globals_1.window === null || globals_1.window === void 0 ? void 0 : globals_1.window.getSelection()) === null || _b === void 0 ? void 0 : _b.toString(); var selectedContent = (0, autocapture_utils_1.makeSafeText)(selectedText); if (!selectedContent) { return false; } props['$selected_content'] = selectedContent; props['$clipboard_text_length'] = (_c = selectedText === null || selectedText === void 0 ? void 0 : selectedText.length) !== null && _c !== void 0 ? _c : 0; } props['$copy_type'] = clipType; } void ((_d = this._client) === null || _d === void 0 ? void 0 : _d.capture(eventName, props).catch(function (error) { return logger.error('Failed to capture event', error); })); return true; } }; Autocapture.prototype.isBrowserSupported = function () { return (0, core_1.isFunction)(globals_1.document === null || globals_1.document === void 0 ? void 0 : globals_1.document.querySelectorAll); }; return Autocapture; }()); exports.Autocapture = Autocapture; //# sourceMappingURL=autocapture.js.map