| /* |
| * 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. |
| */ |
| |
| import { detect } from 'detect-browser'; |
| const browser = detect(); |
| |
| export let logs; |
| let config; |
| |
| // Interval Logging Globals |
| let intervalID; |
| let intervalType; |
| let intervalPath; |
| let intervalTimer; |
| let intervalCounter; |
| let intervalLog; |
| |
| export let filterHandler = null; |
| export let mapHandler = null; |
| export let cbHandlers = {}; |
| |
| /** |
| * Assigns a handler to filter logs out of the queue. |
| * @deprecated Use addCallbacks and removeCallbacks instead |
| * @param {Function} callback The handler to invoke when logging. |
| */ |
| export function setLogFilter(callback) { |
| console.warn("setLogFilter() is deprecated and will be removed in a futre release"); |
| filterHandler = callback; |
| } |
| |
| /** |
| * Assigns a handler to transform logs from their default structure. |
| * @deprecated Use addCallbacks and removeCallbacks instead |
| * @param {Function} callback The handler to invoke when logging. |
| */ |
| export function setLogMapper(callback) { |
| console.warn("setLogMapper() is deprecated and will be removed in a futre release"); |
| mapHandler = callback; |
| } |
| |
| /** |
| * Adds named callbacks to be executed when logging. |
| * @param {Object } newCallbacks An object containing named callback functions. |
| */ |
| export function addCallbacks(...newCallbacks) { |
| newCallbacks.forEach((source) => { |
| const descriptors = Object.keys(source).reduce((descriptors, key) => { |
| descriptors[key] = Object.getOwnPropertyDescriptor(source, key); |
| return descriptors; |
| }, {}); |
| |
| Object.getOwnPropertySymbols(source).forEach((sym) => { |
| const descriptor = Object.getOwnPropertyDescriptor(source, sym); |
| if (descriptor.enumerable) { |
| descriptors[sym] = descriptor; |
| } |
| }); |
| Object.defineProperties(cbHandlers, descriptors); |
| }); |
| return cbHandlers; |
| } |
| |
| /** |
| * Removes callbacks by name. |
| * @param {String[]} targetKeys A list of names of functions to remove. |
| */ |
| export function removeCallbacks(targetKeys) { |
| targetKeys.forEach(key => { |
| if(Object.hasOwn(cbHandlers, key)) { |
| delete cbHandlers[key]; |
| } |
| }); |
| } |
| |
| /** |
| * 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. |
| */ |
| export function initPackager(newLogs, newConfig) { |
| logs = newLogs; |
| config = 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. |
| */ |
| export function packageLog(e, detailFcn) { |
| if (!config.on) { |
| return false; |
| } |
| |
| let details = null; |
| if (detailFcn) { |
| details = detailFcn(e); |
| } |
| |
| const timeFields = extractTimeFields( |
| (e.timeStamp && e.timeStamp > 0) ? config.time(e.timeStamp) : Date.now() |
| ); |
| |
| let 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.userId, |
| 'toolVersion' : config.version, |
| 'toolName' : config.toolName, |
| 'useraleVersion': config.useraleVersion, |
| 'sessionID': config.sessionID, |
| }; |
| |
| if ((typeof filterHandler === 'function') && !filterHandler(log)) { |
| return false; |
| } |
| |
| if (typeof mapHandler === 'function') { |
| log = mapHandler(log, e); |
| } |
| |
| for (const func of Object.values(cbHandlers)) { |
| if (typeof func === 'function') { |
| log = func(log, e); |
| if(!log) { |
| return false; |
| } |
| } |
| } |
| |
| logs.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. |
| */ |
| export function packageCustomLog(customLog, detailFcn, userAction) { |
| if (!config.on) { |
| return false; |
| } |
| |
| let details = null; |
| if (detailFcn) { |
| details = detailFcn(); |
| } |
| |
| const 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.userId, |
| 'toolVersion' : config.version, |
| 'toolName' : config.toolName, |
| 'useraleVersion': config.useraleVersion, |
| 'sessionID': config.sessionID |
| }; |
| |
| let log = Object.assign(metaData, customLog); |
| |
| if ((typeof filterHandler === 'function') && !filterHandler(log)) { |
| return false; |
| } |
| |
| if (typeof mapHandler === 'function') { |
| log = mapHandler(log); |
| } |
| |
| for (const func of Object.values(cbHandlers)) { |
| if (typeof func === 'function') { |
| log = func(log, null); |
| if(!log) { |
| return false; |
| } |
| } |
| } |
| |
| logs.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. |
| */ |
| export 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 |
| */ |
| export function packageIntervalLog(e) { |
| const target = getSelector(e.target); |
| const path = buildPath(e); |
| const type = e.type; |
| const timestamp = Math.floor((e.timeStamp && e.timeStamp > 0) ? config.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.userId, |
| 'toolVersion': config.version, |
| 'toolName': config.toolName, |
| 'useraleVersion': config.useraleVersion, |
| 'sessionID': config.sessionID |
| }; |
| |
| if (typeof filterHandler === 'function' && !filterHandler(intervalLog)) { |
| return false; |
| } |
| |
| if (typeof mapHandler === 'function') { |
| intervalLog = mapHandler(intervalLog, e); |
| } |
| |
| for (const func of Object.values(cbHandlers)) { |
| if (typeof func === 'function') { |
| intervalLog = func(intervalLog, null); |
| if(!intervalLog) { |
| return false; |
| } |
| } |
| } |
| |
| logs.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. |
| */ |
| export 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 |
| */ |
| export 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. |
| */ |
| export 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. |
| */ |
| export function buildPath(e) { |
| if (e instanceof window.Event) { |
| const 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. |
| */ |
| export function selectorizePath(path) { |
| let i = 0; |
| let pathEle; |
| const pathSelectors = []; |
| while (pathEle = path[i]) { |
| pathSelectors.push(getSelector(pathEle)); |
| ++i; |
| } |
| return pathSelectors; |
| } |
| |
| export function detectBrowser() { |
| return { |
| 'browser': browser ? browser.name : '', |
| 'version': browser ? browser.version : '' |
| }; |
| } |