Added lib directory for compiled userale and minified version.
Project: http://git-wip-us.apache.org/repos/asf/incubator-senssoft-useralejs/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-senssoft-useralejs/commit/e2ab5f7a Tree: http://git-wip-us.apache.org/repos/asf/incubator-senssoft-useralejs/tree/e2ab5f7a Diff: http://git-wip-us.apache.org/repos/asf/incubator-senssoft-useralejs/diff/e2ab5f7a Branch: refs/heads/SENSSOFT-192 Commit: e2ab5f7aa68e8b8f92f8e8b61bdd35acdc7e6f92 Parents: f2c4219 Author: msbeard <msbe...@apache.org> Authored: Tue Jan 30 11:05:45 2018 -0500 Committer: msbeard <msbe...@apache.org> Committed: Tue Jan 30 11:05:45 2018 -0500 ---------------------------------------------------------------------- lib/userale.js | 533 ++++++++++++++++++++++++++++++++++++++++++++++++ lib/userale.min.js | 1 + 2 files changed, 534 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-senssoft-useralejs/blob/e2ab5f7a/lib/userale.js ---------------------------------------------------------------------- diff --git a/lib/userale.js b/lib/userale.js new file mode 100644 index 0000000..f3d8d67 --- /dev/null +++ b/lib/userale.js @@ -0,0 +1,533 @@ +(function (exports) { + 'use strict'; + + var version$1 = "1.0.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. + */ + + /** + * Extracts the initial configuration settings from the + * currently executing script tag. + * @return {Object} The extracted configuration object + */ + function getInitialSettings() { + var settings = {}; + + 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')); + + return settings; + } + + /** + * 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) { + 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. + */ + var tsScaler; + + if (delta < 0) { + tsScaler = function () { + return e.timeStamp / 1000; + }; + } else if (delta > e.timeStamp) { + var navStart = performance.timing.navigationStart; + tsScaler = function (ts) { + return ts + navStart; + } + } else { + tsScaler = function (ts) { + return ts; + } + } + } else { + tsScaler = function () { 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) { + Object.keys(newConfig).forEach(function(option) { + if (option === 'userFromParams') { + var userId = getUserIdFromParams(newConfig[option]); + if (userId) { + config.userId = userId; + } + } + config[option] = newConfig[option]; + }); + } + + /** + * 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; + } + } + + /* + * 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 logs$1; + var config$1; + + /** + * 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; + } + + /** + * Transforms the provided event into a log and appends it to the log container. + * @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 log = { + 'target' : getSelector(e.target), + 'path' : buildPath(e), + 'clientTime' : Math.floor((e.timeStamp && e.timeStamp > 0) ? config$1.time(e.timeStamp) : Date.now()), + 'location' : getLocation(e), + 'type' : e.type, + 'userAction' : true, + 'details' : details, + 'userId' : config$1.userId, + 'toolVersion' : config$1.version, + 'toolName' : config$1.toolName, + 'useraleVersion': config$1.useraleVersion + }; + + logs$1.push(log); + + 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 }; + } + } + + /** + * 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) { + var path = []; + if (e.path) { + path = e.path; + } else { + var ele = e.target + while(ele) { + path.push(ele); + ele = ele.parentElement; + } + } + + 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; + } + + var events; + var bufferBools; + var bufferedEvents; + var windowEvents; + + /** + * 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' : function(e) { return { 'clicks' : e.detail, 'ctrl' : e.ctrlKey, 'alt' : e.altKey, 'shift' : e.shiftKey, 'meta' : e.metaKey }; }, + 'dblclick' : function(e) { return { 'clicks' : e.detail, 'ctrl' : e.ctrlKey, 'alt' : e.altKey, 'shift' : e.shiftKey, 'meta' : e.metaKey }; }, + 'mousedown' : function(e) { return { 'clicks' : e.detail, 'ctrl' : e.ctrlKey, 'alt' : e.altKey, 'shift' : e.shiftKey, 'meta' : e.metaKey }; }, + 'mouseup' : function(e) { return { 'clicks' : e.detail, 'ctrl' : e.ctrlKey, 'alt' : e.altKey, 'shift' : e.shiftKey, 'meta' : e.metaKey }; }, + '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, + 'submit' : null + }; + + bufferBools = {}; + bufferedEvents = { + 'wheel' : function(e) { return { 'x' : e.deltaX, 'y' : e.deltaY, 'z' : e.deltaZ }; }, + 'scroll' : function() { return { 'x' : window.scrollX, 'y' : window.scrollY }; }, + 'resize' : function() { return { 'width' : window.outerWidth, 'height' : window.outerHeight }; } + }; + + windowEvents = ['load', 'blur', 'focus']; + } + + /** + * 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); + }); + + 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); + }); + + 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. + */ + + /** + * 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) { + 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. + */ + function sendOnInterval(logs, config) { + setInterval(function() { + if (logs.length >= config.logCountThreshold) { + sendLogs(logs.slice(0), config.url, 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) { + if (navigator.sendBeacon) { + window.addEventListener('unload', function() { + navigator.sendBeacon(config.url, JSON.stringify(logs)); + }); + } else { + window.addEventListener('beforeunload', function() { + if (logs.length > 0) { + sendLogs(logs, config.url, 1); + } + }) + } + } + + /** + * 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} url URL to send the POST request to. + * @param {Number} retries Maximum number of attempts to send the logs. + */ + function sendLogs(logs, url, retries) { + var req = new XMLHttpRequest(); + + var data = JSON.stringify(logs); + + req.open('POST', url); + req.setRequestHeader('Content-type', 'application/json;charset=UTF-8'); + + req.onreadystatechange = function() { + if (req.readyState === 4 && req.status !== 200) { + if (retries > 0) { + sendLogs(logs, url, retries--); + } + } + }; + + req.send(data); + } + + var config = {}; + var logs = []; + exports.started = false; + + + // Start up Userale + config.on = false; + config.useraleVersion = version$1; + + 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 (!exports.started) { + setTimeout(function() { + var state = document.readyState; + + if (state === 'interactive' || state === 'complete') { + attachHandlers(config); + initSender(logs, config); + exports.started = config.on = true; + } else { + setup(config); + } + }, 100); + } + } + + + // Export the Userale API + var version = version$1; + + /** + * Used to start the logging process if the + * autostart configuration option is set to false. + */ + function start() { + if (!exports.started) { + setup(config); + } + + config.on = true; + } + + /** + * Halts the logging process. Logs will no longer be sent. + */ + function stop() { + config.on = false; + } + + /** + * 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; + } + + /** + * Appends a log to the log queue. + * @param {Object} customLog The log to append. + * @return {boolean} Whether the operation succeeded. + */ + function log(customLog) { + if (customLog !== null && typeof customLog === 'object') { + logs.push(customLog); + return true; + } else { + return false; + } + } + + exports.version = version; + exports.start = start; + exports.stop = stop; + exports.options = options; + exports.log = log; + +}((this.userale = this.userale || {}))); \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-senssoft-useralejs/blob/e2ab5f7a/lib/userale.min.js ---------------------------------------------------------------------- diff --git a/lib/userale.min.js b/lib/userale.min.js new file mode 100644 index 0000000..5253a54 --- /dev/null +++ b/lib/userale.min.js @@ -0,0 +1 @@ +!function(t){"use strict";function e(t){if(t.timeStamp&&t.timeStamp>0){var e,n=Date.now()-t.timeStamp;if(n<0)e=function(){return t.timeStamp/1e3};else if(n>t.timeStamp){var r=performance.timing.navigationStart;e=function(t){return t+r}}else e=function(t){return t}}else e=function(){return Date.now()};return e}function n(t,e){Object.keys(e).forEach(function(n){if("userFromParams"===n){var a=r(e[n]);a&&(t.userId=a)}t[n]=e[n]})}function r(t){var e=t,n=new RegExp("[?&]"+e+"(=([^&#]*)|&|#|$)"),r=window.location.href.match(n);return r&&r[2]?decodeURIComponent(r[2].replace(/\+/g," ")):null}function a(t,e){if(!E.on)return!1;var n=null;e&&(n=e(t));var r={target:l(t.target),path:u(t),clientTime:Math.floor(t.timeStamp&&t.timeStamp>0?E.time(t.timeStamp):Date.now()),location:o(t),type:t.type,userAction:!0,details:n,userId:E.userId,toolVersion:E.version,toolName:E.toolName,useraleVersion:E.useraleVersion};return K.push(r),!0}function o(t){return null!=t.pageX?{x:t.pageX,y:t.pageY}:null!=t.clientX ?{x:document.documentElement.scrollLeft+t.clientX,y:document.documentElement.scrollTop+t.clientY}:{x:null,y:null}}function l(t){return t.localName?t.localName+(t.id?"#"+t.id:"")+(t.className?"."+t.className:""):t.nodeName?t.nodeName+(t.id?"#"+t.id:"")+(t.className?"."+t.className:""):t&&t.document&&t.location&&t.alert&&t.setInterval?"Window":"Unknown"}function u(t){var e=[];if(t.path)e=t.path;else for(var n=t.target;n;)e.push(n),n=n.parentElement;return i(e)}function i(t){for(var e,n=0,r=[];e=t[n];)r.push(l(e)),++n;return r}function c(t){S={click:function(t){return{clicks:t.detail,ctrl:t.ctrlKey,alt:t.altKey,shift:t.shiftKey,meta:t.metaKey}},dblclick:function(t){return{clicks:t.detail,ctrl:t.ctrlKey,alt:t.altKey,shift:t.shiftKey,meta:t.metaKey}},mousedown:function(t){return{clicks:t.detail,ctrl:t.ctrlKey,alt:t.altKey,shift:t.shiftKey,meta:t.metaKey}},mouseup:function(t){return{clicks:t.detail,ctrl:t.ctrlKey,alt:t.altKey,shift:t.shiftKey,meta:t.metaKey}},focus:null,blur:null,input:t. logDetails?function(t){return{value:t.target.value}}:null,change:t.logDetails?function(t){return{value:t.target.value}}:null,dragstart:null,dragend:null,drag:null,drop:null,keydown:t.logDetails?function(t){return{key:t.keyCode,ctrl:t.ctrlKey,alt:t.altKey,shift:t.shiftKey,meta:t.metaKey}}:null,mouseover:null,submit:null},N={},k={wheel:function(t){return{x:t.deltaX,y:t.deltaY,z:t.deltaZ}},scroll:function(){return{x:window.scrollX,y:window.scrollY}},resize:function(){return{width:window.outerWidth,height:window.outerHeight}}},b=["load","blur","focus"]}function s(t){return c(t),Object.keys(S).forEach(function(t){document.addEventListener(t,function(e){a(e,S[t])},!0)}),Object.keys(k).forEach(function(e){N[e]=!0,window.addEventListener(e,function(n){N[e]&&(N[e]=!1,a(n,k[e]),setTimeout(function(){N[e]=!0},t.resolution))},!0)}),b.forEach(function(t){window.addEventListener(t,function(t){a(t,function(){return{window:!0}})},!0)}),!0}function d(t,e){f(t,e),m(t,e)}function f(t,e){setInterval(fu nction(){t.length>=e.logCountThreshold&&(h(t.slice(0),e.url,0),t.splice(0))},e.transmitInterval)}function m(t,e){navigator.sendBeacon?window.addEventListener("unload",function(){navigator.sendBeacon(e.url,JSON.stringify(t))}):window.addEventListener("beforeunload",function(){t.length>0&&h(t,e.url,1)})}function h(t,e,n){var r=new XMLHttpRequest,a=JSON.stringify(t);r.open("POST",e),r.setRequestHeader("Content-type","application/json;charset=UTF-8"),r.onreadystatechange=function(){4===r.readyState&&200!==r.status&&n>0&&h(t,e,n--)},r.send(a)}function p(e){t.started||setTimeout(function(){var n=document.readyState;"interactive"===n||"complete"===n?(s(e),d(T,e),t.started=e.on=!0):p(e)},100)}function y(){t.started||p(I),I.on=!0}function v(){I.on=!1}function g(t){return void 0!==t&&n(I,t),I}function w(t){return null!==t&&"object"==typeof t&&(T.push(t),!0)}var K,E,S,N,k,b,I={},T=[];t.started=!1,I.on=!1,I.useraleVersion="1.0.0",n(I,function(){var t={},n=document.currentScript||function(){var t=document.getElementsByTagName("script");return t[t.length-1]}(),r=n?n.getAttribute.bind(n):function(){return null};return t.autostart="false"!==r("data-autostart"),t.url=r("data-url")||"http://localhost:8000",t.transmitInterval=+r("data-interval")||5e3,t.logCountThreshold=+r("data-threshold")||5,t.userId=r("data-user")||null,t.version=r("data-version")||null,t.logDetails="true"===r("data-log-details"),t.resolution=+r("data-resolution")||500,t.toolName=r("data-tool")||null,t.userFromParams=r("data-user-from-params")||null,t.time=e(document.createEvent("CustomEvent")),t}()),function(t,e){K=t,E=e}(T,I),I.autostart&&p(I);t.version="1.0.0",t.start=y,t.stop=v,t.options=g,t.log=w}(this.userale=this.userale||{}); \ No newline at end of file