posthog-js
Version:
Posthog-js allows you to automatically capture usage and send events to PostHog.
466 lines • 23.7 kB
JavaScript
"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