Skip to main content
Glama
autocapture.js15.8 kB
import { addEventListener, each, extend } from './utils'; import { autocaptureCompatibleElements, getClassNames, getDirectAndNestedSpanText, getElementsChainString, getEventTarget, getSafeText, isAngularStyleAttr, isSensitiveElement, makeSafeText, shouldCaptureDomEvent, shouldCaptureElement, shouldCaptureValue, splitClassString, } from './autocapture-utils'; import RageClick from './extensions/rageclick'; import { COPY_AUTOCAPTURE_EVENT } from './types'; import { AUTOCAPTURE_DISABLED_SERVER_SIDE } from './constants'; import { isBoolean, isFunction, isNull, isObject } from './utils/type-utils'; import { createLogger } from './utils/logger'; import { document, window } from './utils/globals'; import { convertToURL } from './utils/request-utils'; import { isDocumentFragment, isElementNode, isTag, isTextNode } from './utils/element-utils'; import { includes } from './utils/string-utils'; var logger = createLogger('[AutoCapture]'); function limitText(length, text) { if (text.length > length) { return text.slice(0, length) + '...'; } return text; } export function getAugmentPropertiesFromElement(elem) { var shouldCaptureEl = shouldCaptureElement(elem); if (!shouldCaptureEl) { return {}; } var props = {}; 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 && shouldCaptureValue(propertyValue)) { props[propertyKey] = propertyValue; } } }); return props; } export 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 && !isElementNode(_el)); return _el; } export function getDefaultProperties(eventType) { return { $event_type: eventType, $ce_version: 1, }; } export function getPropertiesFromElement(elem, maskAllAttributes, maskText, elementAttributeIgnorelist) { var tag_name = elem.tagName.toLowerCase(); var props = { tag_name: tag_name, }; if (autocaptureCompatibleElements.indexOf(tag_name) > -1 && !maskText) { if (tag_name.toLowerCase() === 'a' || tag_name.toLowerCase() === 'button') { props['$el_text'] = limitText(1024, getDirectAndNestedSpanText(elem)); } else { props['$el_text'] = limitText(1024, getSafeText(elem)); } } var classes = 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 each(elem.attributes, function (attr) { // Only capture attributes we know are safe if (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 && shouldCaptureValue(attr.value) && !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 = splitClassString(value).join(' '); } props['attr__' + attr.name] = limitText(1024, value); } }); var nthChild = 1; var nthOfType = 1; var currentElem = elem; while ((currentElem = previousElementSibling(currentElem))) { // eslint-disable-line no-cond-assign nthChild++; if (currentElem.tagName === elem.tagName) { nthOfType++; } } props['nth_child'] = nthChild; props['nth_of_type'] = nthOfType; return props; } export 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; var targetElementList = [target]; var curEl = target; while (curEl.parentNode && !isTag(curEl, 'body')) { if (isDocumentFragment(curEl.parentNode)) { targetElementList.push(curEl.parentNode.host); curEl = curEl.parentNode.host; continue; } targetElementList.push(curEl.parentNode); curEl = curEl.parentNode; } var elementsJson = []; var autocaptureAugmentProperties = {}; var href = false; var explicitNoCapture = false; each(targetElementList, function (el) { var shouldCaptureEl = shouldCaptureElement(el); // if the element or a parent element is an anchor tag // include the href as a property if (el.tagName.toLowerCase() === 'a') { href = el.getAttribute('href'); href = shouldCaptureEl && href && shouldCaptureValue(href) && href; } // allow users to programmatically prevent capturing of elements by adding class 'ph-no-capture' var classes = getClassNames(el); if (includes(classes, 'ph-no-capture')) { explicitNoCapture = true; } elementsJson.push(getPropertiesFromElement(el, maskAllElementAttributes, maskAllText, elementAttributeIgnoreList)); var augmentProperties = getAugmentPropertiesFromElement(el); extend(autocaptureAugmentProperties, augmentProperties); }); 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 (target.tagName.toLowerCase() === 'a' || target.tagName.toLowerCase() === 'button') { elementsJson[0]['$el_text'] = getDirectAndNestedSpanText(target); } else { elementsJson[0]['$el_text'] = getSafeText(target); } } var externalHref; if (href) { elementsJson[0]['attr__href'] = href; var hrefHost = (_b = convertToURL(href)) === null || _b === void 0 ? void 0 : _b.host; var locationHost = (_c = window === null || window === void 0 ? void 0 : window.location) === null || _c === void 0 ? void 0 : _c.host; if (hrefHost && locationHost && hrefHost !== locationHost) { externalHref = href; } } var props = 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: 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(instance) { this._initialized = false; this._isDisabledServerSide = null; this.rageclicks = new RageClick(); this._elementsChainAsString = false; this.instance = instance; this._elementSelectors = null; } Object.defineProperty(Autocapture.prototype, "config", { get: function () { var _a, _b; var config = isObject(this.instance.config.autocapture) ? this.instance.config.autocapture : {}; // precompile the regex config.url_allowlist = (_a = config.url_allowlist) === null || _a === void 0 ? void 0 : _a.map(function (url) { return new RegExp(url); }); config.url_ignorelist = (_b = config.url_ignorelist) === null || _b === void 0 ? void 0 : _b.map(function (url) { return new RegExp(url); }); return config; }, enumerable: false, configurable: true }); Autocapture.prototype._addDomEventHandlers = function () { var _this = this; if (!this.isBrowserSupported()) { logger.info('Disabling Automatic Event Collection because this browser is not supported'); return; } if (!window || !document) { return; } var handler = function (e) { e = e || (window === null || window === void 0 ? void 0 : window.event); try { _this._captureEvent(e); } catch (error) { logger.error('Failed to capture event', error); } }; addEventListener(document, 'submit', handler, { capture: true }); addEventListener(document, 'change', handler, { capture: true }); addEventListener(document, 'click', handler, { capture: true }); if (this.config.capture_copied_text) { var copiedTextHandler = function (e) { e = e || (window === null || window === void 0 ? void 0 : window.event); _this._captureEvent(e, COPY_AUTOCAPTURE_EVENT); }; addEventListener(document, 'copy', copiedTextHandler, { capture: true }); addEventListener(document, 'cut', copiedTextHandler, { capture: true }); } }; Autocapture.prototype.startIfEnabled = function () { if (this.isEnabled && !this._initialized) { this._addDomEventHandlers(); this._initialized = true; } }; Autocapture.prototype.onRemoteConfig = function (response) { var _a; if (response.elementsChainAsString) { this._elementsChainAsString = response.elementsChainAsString; } if (this.instance.persistence) { this.instance.persistence.register((_a = {}, _a[AUTOCAPTURE_DISABLED_SERVER_SIDE] = !!response['autocapture_opt_out'], _a)); } // store this in-memory in case persistence is disabled this._isDisabledServerSide = !!response['autocapture_opt_out']; 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 = document === null || document === void 0 ? void 0 : 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; var persistedServerDisabled = (_a = this.instance.persistence) === null || _a === void 0 ? void 0 : _a.props[AUTOCAPTURE_DISABLED_SERVER_SIDE]; var memoryDisabled = this._isDisabledServerSide; if (isNull(memoryDisabled) && !isBoolean(persistedServerDisabled) && !this.instance.config.advanced_disable_decide) { // We only enable if we know that the server has not disabled it (unless decide is disabled) return false; } var disabledServer = (_b = this._isDisabledServerSide) !== null && _b !== void 0 ? _b : !!persistedServerDisabled; var disabledClient = !this.instance.config.autocapture; return !disabledClient && !disabledServer; }, enumerable: false, configurable: true }); Autocapture.prototype._captureEvent = function (e, eventName) { var _a, _b; if (eventName === void 0) { eventName = '$autocapture'; } if (!this.isEnabled) { return; } /*** Don't mess with this code without running IE8 tests on it ***/ var target = getEventTarget(e); if (isTextNode(target)) { // defeat Safari bug (see: http://www.quirksmode.org/js/events_properties.html) target = (target.parentNode || null); } if (eventName === '$autocapture' && e.type === 'click' && e instanceof MouseEvent) { if (this.instance.config.rageclick && ((_a = this.rageclicks) === null || _a === void 0 ? void 0 : _a.isRageClick(e.clientX, e.clientY, new Date().getTime()))) { this._captureEvent(e, '$rageclick'); } } var isCopyAutocapture = eventName === COPY_AUTOCAPTURE_EVENT; if (target && shouldCaptureDomEvent(target, e, this.config, // mostly this method cares about the target element, but in the case of copy events, // we want some of the work this check does without insisting on the target element's type isCopyAutocapture, // we also don't want to restrict copy checks to clicks, // so we pass that knowledge in here, rather than add the logic inside the check isCopyAutocapture ? ['copy', 'cut'] : undefined)) { var _c = autocapturePropertiesForElement(target, { e: e, maskAllElementAttributes: this.instance.config.mask_all_element_attributes, maskAllText: this.instance.config.mask_all_text, elementAttributeIgnoreList: this.config.element_attribute_ignorelist, elementsChainAsString: this._elementsChainAsString, }), props = _c.props, explicitNoCapture = _c.explicitNoCapture; if (explicitNoCapture) { return false; } var elementSelectors = this.getElementSelectors(target); if (elementSelectors && elementSelectors.length > 0) { props['$element_selectors'] = elementSelectors; } if (eventName === COPY_AUTOCAPTURE_EVENT) { // you can't read the data from the clipboard event, // but you can guess that you can read it from the window's current selection var selectedContent = makeSafeText((_b = window === null || window === void 0 ? void 0 : window.getSelection()) === null || _b === void 0 ? void 0 : _b.toString()); var clipType = e.type || 'clipboard'; if (!selectedContent) { return false; } props['$selected_content'] = selectedContent; props['$copy_type'] = clipType; } this.instance.capture(eventName, props); return true; } }; Autocapture.prototype.isBrowserSupported = function () { return isFunction(document === null || document === void 0 ? void 0 : document.querySelectorAll); }; return Autocapture; }()); export { Autocapture }; //# sourceMappingURL=autocapture.js.map

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sadiuysal/mem0-mcp-server-ts'

If you have feedback or need assistance with the MCP directory API, please join our Discord server