build/UserALEWebExtension/content.js (801 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. */ /* eslint-disable */ // these are default values, which can be overridden by the user on the options page var userAleHost = 'http://localhost:8000'; var userAleScript = 'userale-2.3.0.min.js'; var toolUser = 'nobody'; var toolName = 'test_app'; var toolVersion = '2.3.0'; /* 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. */ var prefix = 'USERALE_'; var CONFIG_CHANGE = prefix + 'CONFIG_CHANGE'; var ADD_LOG = prefix + 'ADD_LOG'; var version = "2.3.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. */ var sessionId = null; /** * Extracts the initial configuration settings from the * currently executing script tag. * @return {Object} The extracted configuration object */ function getInitialSettings() { var settings = {}; if (sessionId === null) { sessionId = getSessionId('userAleSessionId', 'session_' + String(Date.now())); } var script = document.currentScript || function () { var scripts = document.getElementsByTagName('script'); return scripts[scripts.length - 1]; }(); var get = script ? script.getAttribute.bind(script) : function () { return null; }; settings.autostart = get('data-autostart') === 'false' ? false : true; settings.url = get('data-url') || 'http://localhost:8000'; settings.transmitInterval = +get('data-interval') || 5000; settings.logCountThreshold = +get('data-threshold') || 5; settings.userId = get('data-user') || null; settings.version = get('data-version') || null; settings.logDetails = get('data-log-details') === 'true' ? true : false; settings.resolution = +get('data-resolution') || 500; settings.toolName = get('data-tool') || null; settings.userFromParams = get('data-user-from-params') || null; settings.time = timeStampScale(document.createEvent('CustomEvent')); settings.sessionID = get('data-session') || sessionId; settings.authHeader = get('data-auth') || null; settings.custIndex = get('data-index') || 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 {Object} e An event containing a timeStamp property. * @return {timeStampScale~tsScaler} The timestamp normalizing function. */ function timeStampScale(e) { var tsScaler; if (e.timeStamp && e.timeStamp > 0) { var 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 tsScaler() { return e.timeStamp / 1000; }; } else if (delta > e.timeStamp) { var navStart = performance.timing.navigationStart; tsScaler = function tsScaler(ts) { return ts + navStart; }; } else { tsScaler = function tsScaler(ts) { return ts; }; } } else { tsScaler = function tsScaler() { return Date.now(); }; } return tsScaler; } /* * 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. */ /** * Shallow merges the first argument with the second. * Retrieves/updates the userid if userFromParams is provided. * @param {Object} config Current configuration object to be merged into. * @param {Object} newConfig Configuration object to merge into the current config. */ function configure(config, newConfig) { var configAutostart = config['autostart']; var newConfigAutostart = newConfig['autostart']; Object.keys(newConfig).forEach(function (option) { if (option === 'userFromParams') { var userId = getUserIdFromParams(newConfig[option]); if (userId) { config.userId = userId; } } config[option] = newConfig[option]; }); if (configAutostart === false || newConfigAutostart === false) { config['autostart'] = false; } } /** * 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. */ function getUserIdFromParams(param) { var userField = param; var regex = new RegExp('[?&]' + userField + '(=([^&#]*)|&|#|$)'); var results = window.location.href.match(regex); if (results && results[2]) { return decodeURIComponent(results[2].replace(/\+/g, ' ')); } else { return 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 (!!userAgent) { return parseUserAgent(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. */ var browser$1 = detect(); var logs$1; var config$1; // Interval Logging Globals var intervalID; var intervalType; var intervalPath; var intervalTimer; var intervalCounter; var intervalLog; var cbHandlers = {}; /** * Adds named callbacks to be executed when logging. * @param {Object } newCallbacks An object containing named callback functions. */ function addCallbacks() { for (var _len = arguments.length, newCallbacks = new Array(_len), _key = 0; _key < _len; _key++) { newCallbacks[_key] = arguments[_key]; } newCallbacks.forEach(function (source) { var descriptors = Object.keys(source).reduce(function (descriptors, key) { descriptors[key] = Object.getOwnPropertyDescriptor(source, key); return descriptors; }, {}); Object.getOwnPropertySymbols(source).forEach(function (sym) { var descriptor = Object.getOwnPropertyDescriptor(source, sym); if (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} 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 {Object} 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; } var details = null; if (detailFcn) { details = detailFcn(e); } var timeFields = extractTimeFields(e.timeStamp && e.timeStamp > 0 ? config$1.time(e.timeStamp) : Date.now()); var log = { 'target': getSelector(e.target), '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': getSreenRes(), 'type': e.type, 'logType': 'raw', 'userAction': true, 'details': details, 'userId': config$1.userId, 'toolVersion': config$1.version, 'toolName': config$1.toolName, 'useraleVersion': config$1.useraleVersion, 'sessionID': config$1.sessionID }; for (var _i = 0, _Object$values = Object.values(cbHandlers); _i < _Object$values.length; _i++) { var func = _Object$values[_i]; 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 {Object} customLog The behavior to be logged. * @param {Function} 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; } var details = null; if (detailFcn) { details = detailFcn(); } var metaData = { 'pageUrl': window.location.href, 'pageTitle': document.title, 'pageReferrer': document.referrer, 'browser': detectBrowser(), 'clientTime': Date.now(), 'scrnRes': getSreenRes(), 'logType': 'custom', 'userAction': userAction, 'details': details, 'userId': config$1.userId, 'toolVersion': config$1.version, 'toolName': config$1.toolName, 'useraleVersion': config$1.useraleVersion, 'sessionID': config$1.sessionID }; var log = Object.assign(metaData, customLog); for (var _i2 = 0, _Object$values2 = Object.values(cbHandlers); _i2 < _Object$values2.length; _i2++) { var func = _Object$values2[_i2]; 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) { var target = getSelector(e.target); var path = buildPath(e); var type = e.type; var 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) { // 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.version, 'toolName': config$1.toolName, 'useraleVersion': config$1.useraleVersion, 'sessionID': config$1.sessionID }; for (var _i3 = 0, _Object$values3 = Object.values(cbHandlers); _i3 < _Object$values3.length; _i3++) { var func = _Object$values3[_i3]; if (typeof func === 'function') { intervalLog = func(intervalLog, null); if (!intervalLog) { return false; } } } 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 + 1; } return true; } /** * Extracts coordinate information from the event * depending on a few browser quirks. * @param {Object} 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.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 getSreenRes() { return { 'width': window.innerWidth, 'height': window.innerHeight }; } /** * Builds a string CSS selector from the provided element * @param {HTMLElement} 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.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 && ele.document && ele.location && ele.alert && ele.setInterval) { return "Window"; } else { return "Unknown"; } } /** * Builds an array of elements from the provided event target, to the root element. * @param {Object} 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) { if (e instanceof window.Event) { var path = e.composedPath(); return selectorizePath(path); } } /** * Builds a CSS selector path from the provided list of elements. * @param {HTMLElement[]} path Array of HTMLElements from which the path should be built. * @return {string[]} Array of string CSS selectors. */ function selectorizePath(path) { var i = 0; var pathEle; var pathSelectors = []; while (pathEle = path[i]) { pathSelectors.push(getSelector(pathEle)); ++i; } return pathSelectors; } function detectBrowser() { return { 'browser': browser$1 ? browser$1.name : '', 'version': browser$1 ? browser$1.version : '' }; } /* * 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 events; var bufferBools; var bufferedEvents; //@todo: Investigate drag events and their behavior var intervalEvents = ['click', 'focus', 'blur', 'input', 'change', 'mouseover', 'submit']; var refreshEvents; var windowEvents = ['load', 'blur', 'focus']; /** * Maps an event to an object containing useful information. * @param {Object} e Event to extract data from */ function extractMouseEvent(e) { return { 'clicks': e.detail, 'ctrl': e.ctrlKey, 'alt': e.altKey, 'shift': e.shiftKey, 'meta': e.metaKey // 'text' : e.target.innerHTML }; } /** * Defines the way information is extracted from various events. * Also defines which events we will listen to. * @param {Object} 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': extractMouseEvent, 'dblclick': extractMouseEvent, 'mousedown': extractMouseEvent, 'mouseup': extractMouseEvent, 'focus': null, 'blur': null, 'input': config.logDetails ? function (e) { return { 'value': e.target.value }; } : null, 'change': config.logDetails ? function (e) { return { 'value': e.target.value }; } : null, 'dragstart': null, 'dragend': null, 'drag': null, 'drop': null, 'keydown': config.logDetails ? function (e) { return { 'key': e.keyCode, 'ctrl': e.ctrlKey, 'alt': e.altKey, 'shift': e.shiftKey, 'meta': e.metaKey }; } : null, 'mouseover': null }; bufferBools = {}; bufferedEvents = { 'wheel': function wheel(e) { return { 'x': e.deltaX, 'y': e.deltaY, 'z': e.deltaZ }; }, 'scroll': function scroll() { return { 'x': window.scrollX, 'y': window.scrollY }; }, 'resize': function resize() { return { 'width': window.outerWidth, 'height': window.outerHeight }; } }; refreshEvents = { 'submit': null }; } /** * Hooks the event handlers for each event type of interest. * @param {Object} config Configuration object to use. * @return {boolean} Whether the operation succeeded */ function attachHandlers(config) { 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; } /* * 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 sendIntervalId = null; /** * Initializes the log queue processors. * @param {Array} logs Array of logs to append to. * @param {Object} config Configuration object to use when logging. */ function initSender(logs, config) { if (sendIntervalId !== null) { 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} logs Array of logs to read from. * @param {Object} config Configuration object 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, 0); // 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} logs Array of logs to be flushed. * @param {Object} config Configuration object to be read from. */ function sendOnClose(logs, config) { window.addEventListener('pagehide', function () { if (config.on && logs.length > 0) { navigator.sendBeacon(config.url, JSON.stringify(logs)); 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} logs Array of logs to send. * @param {string} config configuration parameters (e.g., to extract URL from & send the POST request to). * @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) { var req = new XMLHttpRequest(); // @todo setRequestHeader for Auth var data = JSON.stringify(logs); req.open('POST', config.url); if (config.authHeader) { req.setRequestHeader('Authorization', config.authHeader); } req.setRequestHeader('Content-type', 'application/json;charset=UTF-8'); req.onreadystatechange = function () { if (req.readyState === 4 && req.status !== 200) { if (retries > 0) { sendLogs(logs, config, retries--); } } }; req.send(data); } var config = {}; var logs = []; var startLoadTimestamp = Date.now(); var endLoadTimestamp; window.onload = function () { endLoadTimestamp = Date.now(); }; var started = false; // Start up Userale config.on = false; config.useraleVersion = version; configure(config, getInitialSettings()); initPackager(logs, config); if (config.autostart) { setup(config); } /** * Hooks the global event listener, and starts up the * logging interval. * @param {Object} config Configuration settings for the logger */ function setup(config) { if (!started) { setTimeout(function () { var state = document.readyState; if (config.autostart && (state === 'interactive' || state === 'complete')) { attachHandlers(config); initSender(logs, config); started = config.on = true; packageCustomLog({ type: 'load', logType: 'raw', details: { pageLoadTime: endLoadTimestamp - startLoadTimestamp } }, function () {}, false); } else { setup(config); } }, 100); } } /** * Updates the current configuration * object with the provided values. * @param {Object} newConfig The configuration options to use. * @return {Object} Returns the updated configuration. */ function options(newConfig) { if (newConfig !== undefined) { configure(config, 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. */ // browser is defined in firefox, but not in chrome. In chrome, they use // the 'chrome' global instead. Let's map it to browser so we don't have // to have if-conditions all over the place. var browser = browser || chrome; // creates a Future for retrieval of the named keys // the value specified is the default value if one doesn't exist in the storage browser.storage.local.get({ sessionId: null, userAleHost: userAleHost, userAleScript: userAleScript, toolUser: toolUser, toolName: toolName, toolVersion: toolVersion }, storeCallback); function storeCallback(item) { injectScript({ url: item.userAleHost, userId: item.toolUser, sessionID: item.sessionId, toolName: item.toolName, toolVersion: item.toolVersion }); } function queueLog(log) { browser.runtime.sendMessage({ type: ADD_LOG, payload: log }); } function injectScript(config) { options(config); // start(); not necessary given that autostart in place, and option is masked from WebExt users addCallbacks({ "function": function _function(log) { queueLog(Object.assign({}, log, { pageUrl: document.location.href })); console.log(log); return false; } }); } browser.runtime.onMessage.addListener(function (message) { if (message.type === CONFIG_CHANGE) { options({ url: message.payload.userAleHost, userId: message.payload.toolUser, toolName: message.payload.toolName, toolVersion: message.payload.toolVersion }); } }); /* eslint-enable */