products/userale/build/UserALEWebExtension/options.js (984 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.
*/
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 = "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,
});
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);
};
}
/**
* 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;
}
/*
* 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";
function rerouteLog(log) {
browser.runtime.sendMessage({ type: messageTypes.ADD_LOG, payload: log });
return false;
}
/* 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.
*/
addCallbacks({ rerouteLog });
// TODO: Warn users when setting credentials with unsecured connection.
// const mitmWarning =
// "Setting credentials with http will expose you to a MITM attack. Are you sure you want to continue?";
function setConfig() {
const config = Configuration.getInstance();
config.update({
url: document.getElementById("url").value,
userId: document.getElementById("user").value,
toolName: document.getElementById("tool").value,
toolVersion: document.getElementById("toolVersion")
.value,
});
// Set a basic auth header if given credentials.
const password = document.getElementById("password")
.value;
if (config.userId && password) {
config.update({
authHeader: "Basic " + btoa(`${config.userId}:${password}`),
});
}
const payload = {
useraleConfig: config,
pluginConfig: {
urlWhitelist: document.getElementById("filter")
.value,
},
};
options(config);
browser.runtime.sendMessage({ type: messageTypes.CONFIG_CHANGE, payload });
}
function getConfig() {
// @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) => {
const payload = res[configKey];
const config = payload.useraleConfig;
options(config);
document.getElementById("url").value = config.url;
document.getElementById("user").value =
config.userId;
document.getElementById("tool").value =
config.toolName;
document.getElementById("toolVersion").value =
config.toolVersion;
document.getElementById("filter").value =
payload.pluginConfig.urlWhitelist;
});
document.getElementById("optionsForm").addEventListener("submit", setConfig);
const issueForm = document.getElementById("issueForm");
if (issueForm instanceof HTMLElement) {
issueForm.addEventListener("submit", reportIssue);
}
}
function reportIssue() {
browser.runtime.sendMessage({
type: messageTypes.ISSUE_REPORT,
payload: {
details: {
issueType: document.querySelector('input[name="issueType"]:checked').value,
issueDescription: document.getElementById("issueDescription").value,
},
type: "issue",
},
});
}
document.addEventListener("DOMContentLoaded", getConfig);
browser.runtime.onMessage.addListener(function (message, sender) {
if (message.type === messageTypes.ISSUE_REPORT) {
if (window.top === window) {
packageCustomLog(message.payload, () => {
return {};
}, true);
}
}
});