blob: db287d17e0da28feeec1a4e103cccf8380f21f06 [file] [log] [blame]
/*
* 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 : ''
};
}