products/userale/build/UserALEWebExtension/background.js (1,088 lines of code) (raw):

/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol */ function __rest(s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var messageTypes; (function (messageTypes) { messageTypes["CONFIG_CHANGE"] = "USERALE_CONFIG_CHANGE"; messageTypes["ADD_LOG"] = "USERALE_ADD_LOG"; messageTypes["HTTP_SESSION"] = "USERALE_HTTP_SESSION"; messageTypes["ISSUE_REPORT"] = "USERALE_ISSUE_REPORT"; })(messageTypes || (messageTypes = {})); var version$1 = "2.4.0"; /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the 'License'); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an 'AS IS' BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ let sessionId = null; let httpSessionId = null; /** * Extracts the initial configuration settings from the * currently executing script tag. * @return {Object} The extracted configuration object */ function getInitialSettings() { if (sessionId === null) { sessionId = getsessionId("userAlesessionId", "session_" + String(Date.now())); } if (httpSessionId === null) { httpSessionId = getsessionId("userAleHttpSessionId", generatehttpSessionId()); } const script = document.currentScript || (function () { const scripts = document.getElementsByTagName("script"); return scripts[scripts.length - 1]; })(); const get = script ? script.getAttribute.bind(script) : function () { return null; }; const headers = get("data-headers"); const settings = { authHeader: get("data-auth") || null, autostart: get("data-autostart") === "false" ? false : true, browserSessionId: null, custIndex: get("data-index") || null, headers: headers ? JSON.parse(headers) : null, httpSessionId: httpSessionId, logCountThreshold: +(get("data-threshold") || 5), logDetails: get("data-log-details") === "true" ? true : false, resolution: +(get("data-resolution") || 500), sessionId: get("data-session") || sessionId, time: timeStampScale(document.createEvent("CustomEvent")), toolName: get("data-tool") || null, toolVersion: get("data-version") || null, transmitInterval: +(get("data-interval") || 5000), url: get("data-url") || "http://localhost:8000", useraleVersion: get("data-userale-version") || null, userFromParams: get("data-user-from-params") || null, userId: get("data-user") || null, }; return settings; } /** * defines sessionId, stores it in sessionStorage, checks to see if there is a sessionId in * storage when script is started. This prevents events like 'submit', which refresh page data * from refreshing the current user session * */ function getsessionId(sessionKey, value) { if (window.sessionStorage.getItem(sessionKey) === null) { window.sessionStorage.setItem(sessionKey, JSON.stringify(value)); return value; } return JSON.parse(window.sessionStorage.getItem(sessionKey) || ""); } /** * Creates a function to normalize the timestamp of the provided event. * @param {Event} e An event containing a timeStamp property. * @return {typeof timeStampScale~tsScaler} The timestamp normalizing function. */ function timeStampScale(e) { let tsScaler; if (e.timeStamp && e.timeStamp > 0) { const delta = Date.now() - e.timeStamp; /** * Returns a timestamp depending on various browser quirks. * @param {?Number} ts A timestamp to use for normalization. * @return {Number} A normalized timestamp. */ if (delta < 0) { tsScaler = function () { return e.timeStamp / 1000; }; } else if (delta > e.timeStamp) { const navStart = performance.timeOrigin; tsScaler = function (ts) { return ts + navStart; }; } else { tsScaler = function (ts) { return ts; }; } } else { tsScaler = function () { return Date.now(); }; } return tsScaler; } /** * Creates a cryptographiclly random string to represent this http session. * @return {String} A random 32 digit hex string */ function generatehttpSessionId() { // 32 digit hex -> 128 bits of info -> 2^64 ~= 10^19 sessions needed for 50% chance of collison const len = 32; const arr = new Uint8Array(len / 2); window.crypto.getRandomValues(arr); return Array.from(arr, (dec) => { return dec.toString(16).padStart(2, "0"); }).join(""); } /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // Singleton Configuration class class Configuration { // Private constructor to prevent external instantiation constructor() { // Public properties corresponding to fields in the Config interface this.autostart = false; this.authHeader = null; this.browserSessionId = null; this.custIndex = null; this.headers = null; this.httpSessionId = null; this.logCountThreshold = 0; this.logDetails = false; this.on = false; this.resolution = 0; this.sessionId = null; this.time = () => Date.now(); this.toolName = null; this.toolVersion = null; this.transmitInterval = 0; this.url = ""; this.userFromParams = null; this.useraleVersion = null; this.userId = null; this.version = null; this.websocketsEnabled = false; // Call the initialization method only if it's the first time instantiating if (Configuration.instance === null) { this.initialize(); } } // Static method to get the singleton instance static getInstance() { if (Configuration.instance === null) { Configuration.instance = new Configuration(); } return Configuration.instance; } initialize() { const settings = getInitialSettings(); this.update(settings); } /** * Resets the configuration to its initial state. */ reset() { this.initialize(); } /** * Shallow merges a newConfig with the configuration class, updating it. * Retrieves/updates the userid if userFromParams is provided. * @param {Partial<Settings.Config>} newConfig Configuration object to merge into the current config. */ update(newConfig) { Object.keys(newConfig).forEach((option) => { if (option === "userFromParams") { const userParamString = newConfig[option]; const userId = userParamString ? Configuration.getUserIdFromParams(userParamString) : null; if (userId) { this["userId"] = userId; } } const hasNewUserFromParams = newConfig["userFromParams"]; const willNullifyUserId = option === "userId" && newConfig[option] === null; if (willNullifyUserId && hasNewUserFromParams) { return; } const newOption = newConfig[option]; if (newOption !== undefined) { this[option] = newOption; } }); } /** * Attempts to extract the userid from the query parameters of the URL. * @param {string} param The name of the query parameter containing the userid. * @return {string | null} The extracted/decoded userid, or null if none is found. */ static getUserIdFromParams(param) { const userField = param; const regex = new RegExp("[?&]" + userField + "(=([^&#]*)|&|#|$)"); const results = window.location.href.match(regex); if (results && results[2]) { return decodeURIComponent(results[2].replace(/\+/g, " ")); } return null; } } // Private static property to hold the singleton instance Configuration.instance = null; var __spreadArray = (undefined && undefined.__spreadArray) || function (to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); }; var BrowserInfo = /** @class */ (function () { function BrowserInfo(name, version, os) { this.name = name; this.version = version; this.os = os; this.type = 'browser'; } return BrowserInfo; }()); var NodeInfo = /** @class */ (function () { function NodeInfo(version) { this.version = version; this.type = 'node'; this.name = 'node'; this.os = process.platform; } return NodeInfo; }()); var SearchBotDeviceInfo = /** @class */ (function () { function SearchBotDeviceInfo(name, version, os, bot) { this.name = name; this.version = version; this.os = os; this.bot = bot; this.type = 'bot-device'; } return SearchBotDeviceInfo; }()); var BotInfo = /** @class */ (function () { function BotInfo() { this.type = 'bot'; this.bot = true; // NOTE: deprecated test name instead this.name = 'bot'; this.version = null; this.os = null; } return BotInfo; }()); var ReactNativeInfo = /** @class */ (function () { function ReactNativeInfo() { this.type = 'react-native'; this.name = 'react-native'; this.version = null; this.os = null; } return ReactNativeInfo; }()); // tslint:disable-next-line:max-line-length var SEARCHBOX_UA_REGEX = /alexa|bot|crawl(er|ing)|facebookexternalhit|feedburner|google web preview|nagios|postrank|pingdom|slurp|spider|yahoo!|yandex/; var SEARCHBOT_OS_REGEX = /(nuhk|curl|Googlebot|Yammybot|Openbot|Slurp|MSNBot|Ask\ Jeeves\/Teoma|ia_archiver)/; var REQUIRED_VERSION_PARTS = 3; var userAgentRules = [ ['aol', /AOLShield\/([0-9\._]+)/], ['edge', /Edge\/([0-9\._]+)/], ['edge-ios', /EdgiOS\/([0-9\._]+)/], ['yandexbrowser', /YaBrowser\/([0-9\._]+)/], ['kakaotalk', /KAKAOTALK\s([0-9\.]+)/], ['samsung', /SamsungBrowser\/([0-9\.]+)/], ['silk', /\bSilk\/([0-9._-]+)\b/], ['miui', /MiuiBrowser\/([0-9\.]+)$/], ['beaker', /BeakerBrowser\/([0-9\.]+)/], ['edge-chromium', /EdgA?\/([0-9\.]+)/], [ 'chromium-webview', /(?!Chrom.*OPR)wv\).*Chrom(?:e|ium)\/([0-9\.]+)(:?\s|$)/, ], ['chrome', /(?!Chrom.*OPR)Chrom(?:e|ium)\/([0-9\.]+)(:?\s|$)/], ['phantomjs', /PhantomJS\/([0-9\.]+)(:?\s|$)/], ['crios', /CriOS\/([0-9\.]+)(:?\s|$)/], ['firefox', /Firefox\/([0-9\.]+)(?:\s|$)/], ['fxios', /FxiOS\/([0-9\.]+)/], ['opera-mini', /Opera Mini.*Version\/([0-9\.]+)/], ['opera', /Opera\/([0-9\.]+)(?:\s|$)/], ['opera', /OPR\/([0-9\.]+)(:?\s|$)/], ['pie', /^Microsoft Pocket Internet Explorer\/(\d+\.\d+)$/], ['pie', /^Mozilla\/\d\.\d+\s\(compatible;\s(?:MSP?IE|MSInternet Explorer) (\d+\.\d+);.*Windows CE.*\)$/], ['netfront', /^Mozilla\/\d\.\d+.*NetFront\/(\d.\d)/], ['ie', /Trident\/7\.0.*rv\:([0-9\.]+).*\).*Gecko$/], ['ie', /MSIE\s([0-9\.]+);.*Trident\/[4-7].0/], ['ie', /MSIE\s(7\.0)/], ['bb10', /BB10;\sTouch.*Version\/([0-9\.]+)/], ['android', /Android\s([0-9\.]+)/], ['ios', /Version\/([0-9\._]+).*Mobile.*Safari.*/], ['safari', /Version\/([0-9\._]+).*Safari/], ['facebook', /FB[AS]V\/([0-9\.]+)/], ['instagram', /Instagram\s([0-9\.]+)/], ['ios-webview', /AppleWebKit\/([0-9\.]+).*Mobile/], ['ios-webview', /AppleWebKit\/([0-9\.]+).*Gecko\)$/], ['curl', /^curl\/([0-9\.]+)$/], ['searchbot', SEARCHBOX_UA_REGEX], ]; var operatingSystemRules = [ ['iOS', /iP(hone|od|ad)/], ['Android OS', /Android/], ['BlackBerry OS', /BlackBerry|BB10/], ['Windows Mobile', /IEMobile/], ['Amazon OS', /Kindle/], ['Windows 3.11', /Win16/], ['Windows 95', /(Windows 95)|(Win95)|(Windows_95)/], ['Windows 98', /(Windows 98)|(Win98)/], ['Windows 2000', /(Windows NT 5.0)|(Windows 2000)/], ['Windows XP', /(Windows NT 5.1)|(Windows XP)/], ['Windows Server 2003', /(Windows NT 5.2)/], ['Windows Vista', /(Windows NT 6.0)/], ['Windows 7', /(Windows NT 6.1)/], ['Windows 8', /(Windows NT 6.2)/], ['Windows 8.1', /(Windows NT 6.3)/], ['Windows 10', /(Windows NT 10.0)/], ['Windows ME', /Windows ME/], ['Windows CE', /Windows CE|WinCE|Microsoft Pocket Internet Explorer/], ['Open BSD', /OpenBSD/], ['Sun OS', /SunOS/], ['Chrome OS', /CrOS/], ['Linux', /(Linux)|(X11)/], ['Mac OS', /(Mac_PowerPC)|(Macintosh)/], ['QNX', /QNX/], ['BeOS', /BeOS/], ['OS/2', /OS\/2/], ]; function detect(userAgent) { if (typeof document === 'undefined' && typeof navigator !== 'undefined' && navigator.product === 'ReactNative') { return new ReactNativeInfo(); } if (typeof navigator !== 'undefined') { return parseUserAgent(navigator.userAgent); } return getNodeVersion(); } function matchUserAgent(ua) { // opted for using reduce here rather than Array#first with a regex.test call // this is primarily because using the reduce we only perform the regex // execution once rather than once for the test and for the exec again below // probably something that needs to be benchmarked though return (ua !== '' && userAgentRules.reduce(function (matched, _a) { var browser = _a[0], regex = _a[1]; if (matched) { return matched; } var uaMatch = regex.exec(ua); return !!uaMatch && [browser, uaMatch]; }, false)); } function parseUserAgent(ua) { var matchedRule = matchUserAgent(ua); if (!matchedRule) { return null; } var name = matchedRule[0], match = matchedRule[1]; if (name === 'searchbot') { return new BotInfo(); } // Do not use RegExp for split operation as some browser do not support it (See: http://blog.stevenlevithan.com/archives/cross-browser-split) var versionParts = match[1] && match[1].split('.').join('_').split('_').slice(0, 3); if (versionParts) { if (versionParts.length < REQUIRED_VERSION_PARTS) { versionParts = __spreadArray(__spreadArray([], versionParts, true), createVersionParts(REQUIRED_VERSION_PARTS - versionParts.length), true); } } else { versionParts = []; } var version = versionParts.join('.'); var os = detectOS(ua); var searchBotMatch = SEARCHBOT_OS_REGEX.exec(ua); if (searchBotMatch && searchBotMatch[1]) { return new SearchBotDeviceInfo(name, version, os, searchBotMatch[1]); } return new BrowserInfo(name, version, os); } function detectOS(ua) { for (var ii = 0, count = operatingSystemRules.length; ii < count; ii++) { var _a = operatingSystemRules[ii], os = _a[0], regex = _a[1]; var match = regex.exec(ua); if (match) { return os; } } return null; } function getNodeVersion() { var isNode = typeof process !== 'undefined' && process.version; return isNode ? new NodeInfo(process.version.slice(1)) : null; } function createVersionParts(count) { var output = []; for (var ii = 0; ii < count; ii++) { output.push('0'); } return output; } /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const browserInfo = detect(); let logs$1; let config$1; // Interval Logging Globals let intervalId; let intervalType; let intervalPath; let intervalTimer; let intervalCounter; let intervalLog; const filterHandler = null; const mapHandler = null; let cbHandlers = {}; /** * Adds named callbacks to be executed when logging. * @param {Object } newCallbacks An object containing named callback functions. */ function addCallbacks(...newCallbacks) { newCallbacks.forEach((source) => { let descriptors = {}; descriptors = Object.keys(source).reduce((descriptors, key) => { descriptors[key] = Object.getOwnPropertyDescriptor(source, key); return descriptors; }, descriptors); Object.getOwnPropertySymbols(source).forEach((sym) => { const descriptor = Object.getOwnPropertyDescriptor(source, sym); if (descriptor === null || descriptor === void 0 ? void 0 : descriptor.enumerable) { descriptors[sym] = descriptor; } }); Object.defineProperties(cbHandlers, descriptors); }); return cbHandlers; } /** * Assigns the config and log container to be used by the logging functions. * @param {Array<Logging.Log>} newLogs Log container. * @param {Object} newConfig Configuration to use while logging. */ function initPackager(newLogs, newConfig) { logs$1 = newLogs; config$1 = newConfig; cbHandlers = {}; intervalId = null; intervalType = null; intervalPath = null; intervalTimer = null; intervalCounter = 0; intervalLog = null; } /** * Transforms the provided HTML event into a log and appends it to the log queue. * @param {Event} e The event to be logged. * @param {Function} detailFcn The function to extract additional log parameters from the event. * @return {boolean} Whether the event was logged. */ function packageLog(e, detailFcn) { if (!config$1.on) { return false; } let details = null; if (detailFcn) { details = detailFcn(e); } const timeFields = extractTimeFields(e.timeStamp && e.timeStamp > 0 ? config$1.time(e.timeStamp) : Date.now()); let log = { target: e.target ? getSelector(e.target) : null, path: buildPath(e), pageUrl: window.location.href, pageTitle: document.title, pageReferrer: document.referrer, browser: detectBrowser(), clientTime: timeFields.milli, microTime: timeFields.micro, location: getLocation(e), scrnRes: getScreenRes(), type: e.type, logType: "raw", userAction: true, details: details, userId: config$1.userId, toolVersion: config$1.toolVersion, toolName: config$1.toolName, useraleVersion: config$1.useraleVersion, sessionId: config$1.sessionId, httpSessionId: config$1.httpSessionId, browserSessionId: config$1.browserSessionId, attributes: buildAttrs(e), style: buildCSS(e), }; for (const func of Object.values(cbHandlers)) { if (typeof func === "function") { log = func(log, e); if (!log) { return false; } } } logs$1.push(log); return true; } /** * Packages the provided customLog to include standard meta data and appends it to the log queue. * @param {Logging.CustomLog} customLog The behavior to be logged. * @param {Logging.DynamicDetailFunction} detailFcn The function to extract additional log parameters from the event. * @param {boolean} userAction Indicates user behavior (true) or system behavior (false) * @return {boolean} Whether the event was logged. */ function packageCustomLog(customLog, detailFcn, userAction) { if (!config$1.on) { return false; } let details = null; if (detailFcn.length === 0) { // In the case of a union, the type checker will default to the more stringent // type, i.e. the DetailFunction that expects an argument for safety purposes. // To avoid this, we must explicitly check the type by asserting it receives // no arguments (detailFcn.length === 0) and then cast it to the // StaticDetailFunction type. const staticDetailFcn = detailFcn; details = staticDetailFcn(); } const metaData = { pageUrl: window.location.href, pageTitle: document.title, pageReferrer: document.referrer, browser: detectBrowser(), clientTime: Date.now(), scrnRes: getScreenRes(), logType: "custom", userAction: userAction, details: details, userId: config$1.userId, toolVersion: config$1.toolVersion, toolName: config$1.toolName, useraleVersion: config$1.useraleVersion, sessionId: config$1.sessionId, httpSessionId: config$1.httpSessionId, browserSessionId: config$1.browserSessionId, }; let log = Object.assign(metaData, customLog); for (const func of Object.values(cbHandlers)) { if (typeof func === "function") { log = func(log, null); if (!log) { return false; } } } logs$1.push(log); return true; } /** * Extract the millisecond and microsecond portions of a timestamp. * @param {Number} timeStamp The timestamp to split into millisecond and microsecond fields. * @return {Object} An object containing the millisecond * and microsecond portions of the timestamp. */ function extractTimeFields(timeStamp) { return { milli: Math.floor(timeStamp), micro: Number((timeStamp % 1).toFixed(3)), }; } /** * Track intervals and gather details about it. * @param {Object} e * @return boolean */ function packageIntervalLog(e) { try { const target = e.target ? getSelector(e.target) : null; const path = buildPath(e); const type = e.type; const timestamp = Math.floor(e.timeStamp && e.timeStamp > 0 ? config$1.time(e.timeStamp) : Date.now()); // Init - this should only happen once on initialization if (intervalId == null) { intervalId = target; intervalType = type; intervalPath = path; intervalTimer = timestamp; intervalCounter = 0; } if ((intervalId !== target || intervalType !== type) && intervalTimer) { // When to create log? On transition end // @todo Possible for intervalLog to not be pushed in the event the interval never ends... intervalLog = { target: intervalId, path: intervalPath, pageUrl: window.location.href, pageTitle: document.title, pageReferrer: document.referrer, browser: detectBrowser(), count: intervalCounter, duration: timestamp - intervalTimer, // microseconds startTime: intervalTimer, endTime: timestamp, type: intervalType, logType: "interval", targetChange: intervalId !== target, typeChange: intervalType !== type, userAction: false, userId: config$1.userId, toolVersion: config$1.toolVersion, toolName: config$1.toolName, useraleVersion: config$1.useraleVersion, sessionId: config$1.sessionId, httpSessionId: config$1.httpSessionId, browserSessionId: config$1.browserSessionId, }; if (typeof filterHandler === "function" && !filterHandler(intervalLog)) ; if (typeof mapHandler === "function") ; for (const func of Object.values(cbHandlers)) { if (typeof func === "function") { intervalLog = func(intervalLog, null); if (!intervalLog) { return false; } } } if (intervalLog) logs$1.push(intervalLog); // Reset intervalId = target; intervalType = type; intervalPath = path; intervalTimer = timestamp; intervalCounter = 0; } // Interval is still occuring, just update counter if (intervalId == target && intervalType == type && intervalCounter) { intervalCounter = intervalCounter + 1; } return true; } catch (_a) { return false; } } /** * Extracts coordinate information from the event * depending on a few browser quirks. * @param {Event} e The event to extract coordinate information from. * @return {Object} An object containing nullable x and y coordinates for the event. */ function getLocation(e) { if (e instanceof MouseEvent) { if (e.pageX != null) { return { x: e.pageX, y: e.pageY }; } else if (e.clientX != null) { return { x: document.documentElement.scrollLeft + e.clientX, y: document.documentElement.scrollTop + e.clientY, }; } } else { return { x: null, y: null }; } } /** * Extracts innerWidth and innerHeight to provide estimates of screen resolution * @return {Object} An object containing the innerWidth and InnerHeight */ function getScreenRes() { return { width: window.innerWidth, height: window.innerHeight }; } /** * Builds a string CSS selector from the provided element * @param {EventTarget} ele The element from which the selector is built. * @return {string} The CSS selector for the element, or Unknown if it can't be determined. */ function getSelector(ele) { if (ele instanceof HTMLElement || ele instanceof Element) { if (ele.localName) { return (ele.localName + (ele.id ? "#" + ele.id : "") + (ele.className ? "." + ele.className : "")); } else if (ele.nodeName) { return (ele.nodeName + (ele.id ? "#" + ele.id : "") + (ele.className ? "." + ele.className : "")); } } else if (ele instanceof Document) { return "#document"; } else if (ele === globalThis) { return "Window"; } return "Unknown"; } /** * Builds an array of elements from the provided event target, to the root element. * @param {Event} e Event from which the path should be built. * @return {HTMLElement[]} Array of elements, starting at the event target, ending at the root element. */ function buildPath(e) { const path = e.composedPath(); return selectorizePath(path); } /** * Builds a CSS selector path from the provided list of elements. * @param {EventTarget[]} path Array of HTML Elements from which the path should be built. * @return {string[]} Array of string CSS selectors. */ function selectorizePath(path) { let i = 0; let pathEle; const pathSelectors = []; while ((pathEle = path[i])) { pathSelectors.push(getSelector(pathEle)); ++i; pathEle = path[i]; } return pathSelectors; } function detectBrowser() { return { browser: browserInfo ? browserInfo.name : "", version: browserInfo ? browserInfo.version : "", }; } /** * Builds an object containing attributes of an element. * Attempts to parse all attribute values as JSON text. * @param {Event} e Event from which the target element's attributes should be extracted. * @return {Record<string, any>} Object with element attributes as key-value pairs. */ function buildAttrs(e) { const attributes = {}; const attributeBlackList = ["style"]; if (e.target && e.target instanceof Element) { for (const attr of e.target.attributes) { if (attributeBlackList.includes(attr.name)) continue; let val = attr.value; try { val = JSON.parse(val); } catch (error) { // Ignore parsing errors, fallback to raw string value } attributes[attr.name] = val; } } return attributes; } /** * Builds an object containing all CSS properties of an element. * @param {Event} e Event from which the target element's properties should be extracted. * @return {Record<string, string>} Object with all CSS properties as key-value pairs. */ function buildCSS(e) { const properties = {}; if (e.target && e.target instanceof HTMLElement) { const styleObj = e.target.style; for (let i = 0; i < styleObj.length; i++) { const prop = styleObj[i]; properties[prop] = styleObj.getPropertyValue(prop); } } return properties; } /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ //@todo: Investigate drag events and their behavior let events; let bufferBools; let bufferedEvents; let refreshEvents; const intervalEvents = [ "click", "focus", "blur", "input", "change", "mouseover", "submit", ]; const windowEvents = ["load", "blur", "focus"]; /** * Maps a MouseEvent to an object containing useful information. * @param {MouseEvent} e Event to extract data from */ function extractMouseDetails(e) { return { clicks: e.detail, ctrl: e.ctrlKey, alt: e.altKey, shift: e.shiftKey, meta: e.metaKey, // 'text' : e.target.innerHTML }; } /** Maps a KeyboardEvent to an object containing useful infromation * @param {KeyboardEvent} e Event to extract data from */ function extractKeyboardDetails(e) { return { key: e.key, code: e.code, ctrl: e.ctrlKey, alt: e.altKey, shift: e.shiftKey, meta: e.metaKey, }; } /** * Maps a ChangeEvent to an object containing useful information. * @param {Events.ChangeEvent} e Event to extract data from */ function extractChangeDetails(e) { return { value: e.target.value, }; } /** * Maps a WheelEvent to an object containing useful information. * @param {WheelEvent} e Event to extract data from */ function extractWheelDetails(e) { return { x: e.deltaX, y: e.deltaY, z: e.deltaZ, }; } /** * Maps a ScrollEvent to an object containing useful information. */ function extractScrollDetails() { return { x: window.scrollX, y: window.scrollY, }; } /** * Maps a ResizeEvent to an object containing useful information. */ function extractResizeDetails() { return { width: window.outerWidth, height: window.outerHeight, }; } /** * Defines the way information is extracted from various events. * Also defines which events we will listen to. * @param {Settings.Config} config Configuration object to read from. */ function defineDetails(config) { // Events list // Keys are event types // Values are functions that return details object if applicable events = { click: extractMouseDetails, dblclick: extractMouseDetails, mousedown: extractMouseDetails, mouseup: extractMouseDetails, focus: null, blur: null, input: config.logDetails ? extractKeyboardDetails : null, change: config.logDetails ? extractChangeDetails : null, dragstart: null, dragend: null, drag: null, drop: null, keydown: config.logDetails ? extractKeyboardDetails : null, mouseover: null, }; bufferBools = {}; bufferedEvents = { wheel: extractWheelDetails, scroll: extractScrollDetails, resize: extractResizeDetails, }; refreshEvents = { submit: null, }; } /** * Hooks the event handlers for each event type of interest. * @param {Configuration} config Configuration singleton to use. * @return {boolean} Whether the operation succeeded */ function attachHandlers(config) { try { defineDetails(config); Object.keys(events).forEach(function (ev) { document.addEventListener(ev, function (e) { packageLog(e, events[ev]); }, true); }); intervalEvents.forEach(function (ev) { document.addEventListener(ev, function (e) { packageIntervalLog(e); }, true); }); Object.keys(bufferedEvents).forEach(function (ev) { bufferBools[ev] = true; window.addEventListener(ev, function (e) { if (bufferBools[ev]) { bufferBools[ev] = false; packageLog(e, bufferedEvents[ev]); setTimeout(function () { bufferBools[ev] = true; }, config.resolution); } }, true); }); Object.keys(refreshEvents).forEach(function (ev) { document.addEventListener(ev, function (e) { packageLog(e, events[ev]); }, true); }); windowEvents.forEach(function (ev) { window.addEventListener(ev, function (e) { packageLog(e, function () { return { window: true }; }); }, true); }); return true; } catch (_a) { return false; } } /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ let sendIntervalId; /** * Initializes the log queue processors. * @param {Array<Logging.Log>} logs Array of logs to append to. * @param {Configuration} config Configuration object to use when logging. */ function initSender(logs, config) { if (sendIntervalId) { clearInterval(sendIntervalId); } sendIntervalId = sendOnInterval(logs, config); sendOnClose(logs, config); } /** * Checks the provided log array on an interval, flushing the logs * if the queue has reached the threshold specified by the provided config. * @param {Array<Logging.Log>} logs Array of logs to read from. * @param {Configuration} config Configuration singleton to be read from. * @return {Number} The newly created interval id. */ function sendOnInterval(logs, config) { return setInterval(function () { if (!config.on) { return; } if (logs.length >= config.logCountThreshold) { sendLogs(logs.slice(0), config); // Send a copy logs.splice(0); // Clear array reference (no reassignment) } }, config.transmitInterval); } /** * Attempts to flush the remaining logs when the window is closed. * @param {Array<Logging.Log>} logs Array of logs to be flushed. * @param {Configuration} config Configuration singleton to be read from. */ function sendOnClose(logs, config) { window.addEventListener("pagehide", function () { if (!config.on) { return; } if (logs.length > 0) { if (config.websocketsEnabled) { const data = JSON.stringify(logs); wsock.send(data); } else { const headers = new Headers(); headers.set("Content-Type", "applicaiton/json;charset=UTF-8"); if (config.authHeader) { headers.set("Authorization", config.authHeader.toString()); } fetch(config.url, { keepalive: true, method: "POST", headers: headers, body: JSON.stringify(logs), }).catch((error) => { console.error(error); }); } logs.splice(0); // clear log queue } }); } /** * Sends the provided array of logs to the specified url, * retrying the request up to the specified number of retries. * @param {Array<Logging.Log>} logs Array of logs to send. * @param {Configuration} config configuration singleton. * @param {Number} retries Maximum number of attempts to send the logs. */ // @todo expose config object to sendLogs replate url with config.url function sendLogs(logs, config, retries) { const data = JSON.stringify(logs); if (config.websocketsEnabled) { wsock.send(data); } else { const req = new XMLHttpRequest(); req.open("POST", config.url); if (config.authHeader) { req.setRequestHeader("Authorization", typeof config.authHeader === "function" ? config.authHeader() : config.authHeader); } req.setRequestHeader("Content-type", "application/json;charset=UTF-8"); if (config.headers) { Object.entries(config.headers).forEach(([header, value]) => { req.setRequestHeader(header, value); }); } req.onreadystatechange = function () { if (req.readyState === 4 && req.status !== 200) ; }; req.send(data); } } /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const config = Configuration.getInstance(); const logs = []; const startLoadTimestamp = Date.now(); let endLoadTimestamp; window.onload = function () { endLoadTimestamp = Date.now(); }; let started = false; let wsock; config.update({ useraleVersion: version$1, }); initPackager(logs, config); getWebsocketsEnabled(config); if (config.autostart) { setup(config); } /** * Hooks the global event listener, and starts up the * logging interval. * @param {Configuration} config Configuration settings for the logger */ function setup(config) { if (!started) { setTimeout(function () { const state = document.readyState; if (config.autostart && (state === "interactive" || state === "complete")) { attachHandlers(config); initSender(logs, config); started = config.on = true; packageCustomLog({ type: "load", details: { pageLoadTime: endLoadTimestamp - startLoadTimestamp }, }, () => ({}), false); } else { setup(config); } }, 100); } } /** * Checks to see if the specified backend URL supporsts Websockets * and updates the config accordingly */ function getWebsocketsEnabled(config) { wsock = new WebSocket(config.url.replace("http://", "ws://")); wsock.onerror = () => { console.log("no websockets detected"); }; wsock.onopen = () => { console.log("connection established with websockets"); config.websocketsEnabled = true; }; wsock.onclose = () => { sendOnClose(logs, config); }; } // Export the Userale API const version = version$1; /** * Updates the current configuration * object with the provided values. * @param {Partial<Settings.Config>} newConfig The configuration options to use. * @return {Settings.Config} Returns the updated configuration. */ function options(newConfig) { if (newConfig) { config.update(newConfig); } return config; } /** * Appends a log to the log queue. * @param {Logging.CustomLog} customLog The log to append. * @return {boolean} Whether the operation succeeded. */ function log(customLog) { if (customLog) { logs.push(customLog); return true; } else { return false; } } /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* eslint-disable */ // browser is defined in firefox, but chrome uses the 'chrome' global. var browser = window.browser || chrome; const configKey = "useraleConfigPayload"; /* eslint-enable */ /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // Initalize userale plugin options const defaultConfig = { useraleConfig: { url: "http://localhost:8000", userId: "pluginUser", authHeader: null, toolName: "useralePlugin", toolVersion: version, }, pluginConfig: { // Default to a regex that will match no string urlWhitelist: "(?!x)x", }, }; let urlWhitelist; const tabToHttpSession = {}; let browserSessionId = null; /** * Add log to UserALE buffer for sending to backend * @param {any} message The message to send * @return {void} */ function addLog(message) { let log$1 = message.payload; log$1.browserSessionId = browserSessionId; // Apply url filter to logs generated outside the background page. log$1 = filterUrl(log$1); if (log$1) { log(log$1); } } /** * add tab id to http session id mapping * @param {any} message The message to send * @param {browser.runtime.MessageSender} sender The sender of the message * @return {void} */ function updateTabToHttpSessionMapping(message, sender) { var _a; if ((_a = sender.tab) === null || _a === void 0 ? void 0 : _a.id) { tabToHttpSession[sender.tab.id] = message.payload; } } /** * Apply the extension config to both the background and content instances of userale * @param {Extension.ConfigPayload} config The extension config to apply * @return {void} */ function updateConfig(payload) { urlWhitelist = new RegExp(payload.pluginConfig.urlWhitelist); options(payload.useraleConfig); browser.storage.local.set({ [configKey]: payload }); dispatchTabMessage({ type: messageTypes.CONFIG_CHANGE, payload: payload.useraleConfig, }); } /** * Send a message to all tabs * @param {any} message The message to send * @return {void} */ function dispatchTabMessage(message) { // @ts-expect-error Typescript is not aware that firefox's broswer is overloaded // to support chromium style MV2 callbacks browser.tabs.query({}, function (tabs) { tabs.forEach(function (tab) { if (!tab.id) return; browser.tabs.sendMessage(tab.id, message); }); }); } /** * Send a message to the current tab * @param {any} message The message to send * @return {void} */ function messageCurrentTab(message) { // @ts-expect-error Typescript is not aware that firefox's broswer is overloaded // to support chromium style MV2 callbacks browser.tabs.query({ active: true, currentWindow: true }, function (tabs) { if (tabs.length > 0) { const activeTab = tabs[0]; browser.tabs.sendMessage(activeTab.id, message); } else { console.error("No active tab found"); } }); } /** * Callback for filtering out logs with urls that do not match the regex defined in extension options. * @param {Logging.Log} log The candidate log * @return {Object} The transformed log */ function filterUrl(log) { if (urlWhitelist.test(log.pageUrl)) { return log; } return false; } /** * Callback for setting the session id's of tab logs to that of the target tab * @param {Logging.Log} log The candidate log * @return {Object} The transformed log */ function injectSessions(log) { var _a; const id = (_a = log.details) === null || _a === void 0 ? void 0 : _a.id; if (id && id in tabToHttpSession) { log.httpSessionId = tabToHttpSession[id]; } else { log.httpSessionId = null; } log.browserSessionId = browserSessionId; return log; } // @ts-expect-error Typescript is not aware that firefox's broswer is overloaded // to support chromium style MV2 callbacks browser.storage.local.get([configKey], (res) => { if (res[configKey]) { updateConfig(res[configKey]); } else { updateConfig(defaultConfig); } // Apply url filter to logs generated by the background page. addCallbacks({ filterUrl, injectSessions }); const userAleHttpSessionId = window.sessionStorage.getItem("userAleHttpSessionId"); browserSessionId = userAleHttpSessionId ? JSON.parse(userAleHttpSessionId) : null; }); browser.runtime.onMessage.addListener(function (message, sender) { switch (message.type) { case messageTypes.ADD_LOG: addLog(message); break; case messageTypes.HTTP_SESSION: updateTabToHttpSessionMapping(message, sender); break; case messageTypes.CONFIG_CHANGE: updateConfig(message.payload); break; case messageTypes.ISSUE_REPORT: messageCurrentTab(message); break; default: console.log("got unknown message type ", message); } }); /** * Extract tab details then log a tab event * @param {integer} tabId The id of the target tab * @param {Logging.CustomLog} data The data of the tab event * @param {String} type The type of tab event * @return {undefined} */ function packageTabLog(tabId, data, type) { // @ts-expect-error Typescript is not aware that firefox's broswer is overloaded // to support chromium style MV2 callbacks browser.tabs.get(tabId, (tab) => { packageDetailedTabLog(tab, data, type); }); } /** * Log a tab event with tab details * @param {browser.tabs.Tab} tab The target tab object * @param {Logging.CustomLog} data The data of the tab event * @param {String} type The type of tab event * @return {undefined} */ function packageDetailedTabLog(tab, data, type) { Object.assign(data, { type }); // Two fields in the Tab interface do not match our serializable JSONObject interface // remove them before create custom log const restOfTab = __rest(tab, ["mutedInfo", "sharingState"]); const payload = Object.assign({}, restOfTab); packageCustomLog(data, () => { return payload; }, true); } // Attach Handlers for tab events // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs browser.tabs.onActivated.addListener((activeInfo) => { // Adds needed string indexing signature to payload that to // make it conform to the JSONObject interface const payload = Object.assign({}, activeInfo); packageTabLog(activeInfo.tabId, payload, "tabs.onActivated"); }); browser.tabs.onAttached.addListener((tabId, attachInfo) => { const payload = Object.assign({}, attachInfo); packageTabLog(tabId, payload, "tabs.onAttached"); }); browser.tabs.onCreated.addListener((tab) => { packageDetailedTabLog(tab, {}, "tabs.onCreated"); }); browser.tabs.onDetached.addListener((tabId, detachInfo) => { const payload = Object.assign({}, detachInfo); packageTabLog(tabId, payload, "tabs.onDetached"); }); browser.tabs.onMoved.addListener((tabId, moveInfo) => { const payload = Object.assign({}, moveInfo); packageTabLog(tabId, payload, "tabs.onMoved"); }); browser.tabs.onRemoved.addListener((tabId, removeInfo) => { const payload = Object.assign({}, removeInfo); packageDetailedTabLog({ id: tabId }, payload, "tabs.onRemoved"); }); browser.tabs.onUpdated.addListener((_tabId, changeInfo, tab) => { const restOfTab = __rest(changeInfo, ["mutedInfo", "sharingState"]); const payload = Object.assign({}, restOfTab); packageDetailedTabLog(tab, payload, "tabs.onUpdated"); }); browser.tabs.onZoomChange.addListener((ZoomChangeInfo) => { const restOfTab = __rest(ZoomChangeInfo, ["zoomSettings"]); const payload = Object.assign({}, restOfTab); packageTabLog(ZoomChangeInfo.tabId, payload, "tabs.onZoomChange"); });