Repository: incubator-senssoft-useralejs
Updated Branches:
  refs/heads/SENSSOFT-192 6c64cfa17 -> 1349cadda


Keep build directory as our distribution


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/1349cadd
Tree: 
http://git-wip-us.apache.org/repos/asf/incubator-senssoft-useralejs/tree/1349cadd
Diff: 
http://git-wip-us.apache.org/repos/asf/incubator-senssoft-useralejs/diff/1349cadd

Branch: refs/heads/SENSSOFT-192
Commit: 1349cadda2f6532077138d3a0a937e35424c7a47
Parents: 6c64cfa
Author: msbeard <msbe...@apache.org>
Authored: Mon Feb 12 10:35:28 2018 -0500
Committer: msbeard <msbe...@apache.org>
Committed: Mon Feb 12 10:35:28 2018 -0500

----------------------------------------------------------------------
 .gitignore                                    |   1 -
 build/UserAleWebExtension/background.js       | 388 +++++++++++
 build/UserAleWebExtension/content.js          | 729 +++++++++++++++++++++
 build/UserAleWebExtension/icons/border-48.png | Bin 0 -> 225 bytes
 build/UserAleWebExtension/manifest.json       |  29 +
 build/UserAleWebExtension/options.js          |  56 ++
 build/UserAleWebExtension/optionsPage.html    |  37 ++
 build/userale-1.0.0.js                        | 702 ++++++++++++++++++++
 build/userale-1.0.0.min.js                    |   1 +
 9 files changed, 1942 insertions(+), 1 deletion(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-senssoft-useralejs/blob/1349cadd/.gitignore
----------------------------------------------------------------------
diff --git a/.gitignore b/.gitignore
index 8ed5199..4c0ed70 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,5 @@
 .idea
 /node_modules/*
-/build/*
 /logs/*
 /.git/*
 npm-debug.log

http://git-wip-us.apache.org/repos/asf/incubator-senssoft-useralejs/blob/1349cadd/build/UserAleWebExtension/background.js
----------------------------------------------------------------------
diff --git a/build/UserAleWebExtension/background.js 
b/build/UserAleWebExtension/background.js
new file mode 100644
index 0000000..633dc71
--- /dev/null
+++ b/build/UserAleWebExtension/background.js
@@ -0,0 +1,388 @@
+/* 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-0.2.1.min.js';
+var toolUser = 'nobody';
+var toolName = 'test_app';
+var toolVersion = '0.1.0';
+
+/* eslint-enable */
+
+var prefix = 'USERALE_';
+
+var CONFIG_CHANGE = prefix + 'CONFIG_CHANGE';
+var ADD_LOG = prefix + 'ADD_LOG';
+
+/**
+ * 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.
+ */
+
+var logs$1;
+var config$1;
+
+// Interval Logging Globals
+var intervalID;
+var intervalType;
+var intervalPath;
+var intervalTimer;
+var intervalCounter;
+var intervalLog;
+
+var filterHandler = null;
+var mapHandler = null;
+
+/**
+ * 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;
+  filterHandler = null;
+  mapHandler = null;
+  intervalID = null;
+  intervalType = null;
+  intervalPath = null;
+  intervalTimer = null;
+  intervalCounter = 0;
+  intervalLog = null;
+}
+
+/**
+ * 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)),
+  };
+}
+
+/*
+ * 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.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 (!config.on) {
+    return;
+  }
+
+  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);
+}
+
+// inherent dependency on globals.js, loaded by the webext
+
+// 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;
+var logs = [];
+var config = {
+  autostart: true,
+  url: 'http://localhost:8000',
+  transmitInterval: 5000,
+  logCountThreshold: 5,
+  userId: null,
+  version: null,
+  resolution: 500,
+  time: timeStampScale({}),
+  on: true,
+};
+var sessionId = 'session_' + Date.now();
+
+var getTimestamp = ((typeof performance !== 'undefined') && (typeof 
performance.now !== 'undefined'))
+  ? function () { return performance.now() + 
performance.timing.navigationStart; }
+  : Date.now;
+
+browser.storage.local.set({ sessionId: sessionId });
+
+var store = browser.storage.local.get({
+  userAleHost: userAleHost,
+  userAleScript: userAleScript,
+  toolUser: toolUser,
+  toolName: toolName,
+  toolVersion: toolVersion,
+}, storeCallback);
+        
+function storeCallback(item) {
+  config = Object.assign({}, config, {
+    url: item.userAleHost,
+    userId: item.toolUser,
+    sessionID: sessionId,
+    toolName: item.toolName,
+    toolVersion: item.toolVersion
+  });
+
+  initPackager(logs, config);
+  initSender(logs, config);
+}
+
+function dispatchTabMessage(message) {
+  browser.tabs.query({}, function (tabs) {
+    tabs.forEach(function (tab) {
+      browser.tabs.sendMessage(tab.id, message);
+    });
+  });
+}
+
+function packageBrowserLog(type, logDetail) {
+  var timeFields = extractTimeFields(getTimestamp());
+
+  logs.push({
+    'target' : null,
+    'path' : null,
+    'clientTime' : timeFields.milli,
+    'microTime' : timeFields.micro,
+    'location' : null,
+    'type' : 'browser.' + type,
+    'logType': 'raw',
+    'userAction' : true,
+    'details' : logDetail,
+    'userId' : toolUser,
+    'toolVersion': null,
+    'toolName': null,
+    'useraleVersion': null,
+    'sessionID': sessionId,
+  });
+}
+
+browser.runtime.onMessage.addListener(function (message) {
+  switch (message.type) {
+    case CONFIG_CHANGE:
+      (function () {
+        var updatedConfig = Object.assign({}, config, {
+          url: message.payload.userAleHost,
+          userId: message.payload.toolUser,
+          toolName: message.payload.toolName,
+          toolVersion: message.payload.toolVersion
+        });
+        initPackager(logs, updatedConfig);
+        initSender(logs, updatedConfig);
+        dispatchTabMessage(message);
+      })();
+      break;
+
+    case ADD_LOG:
+      (function () {
+        logs.push(message.payload);
+      })();
+      break;
+
+    default:
+      console.log('got unknown message type ', message);
+  }
+});
+
+function getTabDetailById(tabId, onReady) {
+  browser.tabs.get(tabId, function (tab) {
+    onReady({
+      active: tab.active,
+      audible: tab.audible,
+      incognito: tab.incognito,
+      index: tab.index,
+      muted: tab.mutedInfo ? tab.mutedInfo.muted : null,
+      pinned: tab.pinned,
+      selected: tab.selected,
+      tabId: tab.id,
+      title: tab.title,
+      url: tab.url,
+      windowId: tab.windowId,
+    });
+  });
+}
+
+browser.tabs.onActivated.addListener(function (e) {
+  getTabDetailById(e.tabId, function (detail) {
+    packageBrowserLog('tabs.onActivated', detail);
+  });
+});
+
+browser.tabs.onCreated.addListener(function (tab, e) {
+  packageBrowserLog('tabs.onCreated', {
+    active: tab.active,
+    audible: tab.audible,
+    incognito: tab.incognito,
+    index: tab.index,
+    muted: tab.mutedInfo ? tab.mutedInfo.muted : null,
+    pinned: tab.pinned,
+    selected: tab.selected,
+    tabId: tab.id,
+    title: tab.title,
+    url: tab.url,
+    windowId: tab.windowId,
+  });
+});
+
+browser.tabs.onDetached.addListener(function (tabId) {
+  getTabDetailById(tabId, function (detail) {
+    packageBrowserLog('tabs.onDetached', detail);
+  });
+});
+
+browser.tabs.onMoved.addListener(function (tabId) {
+  getTabDetailById(tabId, function (detail) {
+    packageBrowserLog('tabs.onMoved', detail);
+  });
+});
+
+browser.tabs.onRemoved.addListener(function (tabId) {
+  packageBrowserLog('tabs.onRemoved', { tabId: tabId });
+});
+
+browser.tabs.onZoomChange.addListener(function (e) {
+  getTabDetailById(e.tabId, function (detail) {
+    packageBrowserLog('tabs.onZoomChange', Object.assign({}, {
+      oldZoomFactor: e.oldZoomFactor,
+      newZoomFactor: e.newZoomFactor,
+    }, detail));
+  });
+});
+
+/*
+ eslint-enable
+ */
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-senssoft-useralejs/blob/1349cadd/build/UserAleWebExtension/content.js
----------------------------------------------------------------------
diff --git a/build/UserAleWebExtension/content.js 
b/build/UserAleWebExtension/content.js
new file mode 100644
index 0000000..51b5f10
--- /dev/null
+++ b/build/UserAleWebExtension/content.js
@@ -0,0 +1,729 @@
+/* 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-0.2.1.min.js';
+var toolUser = 'nobody';
+var toolName = 'test_app';
+var toolVersion = '0.1.0';
+
+/* eslint-enable */
+
+var prefix = 'USERALE_';
+
+var CONFIG_CHANGE = prefix + 'CONFIG_CHANGE';
+var ADD_LOG = prefix + 'ADD_LOG';
+
+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'));
+  settings.sessionID = get('data-session') || 'session_' + String(Date.now());
+
+  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;
+
+// Interval Logging Globals
+var intervalID;
+var intervalType;
+var intervalPath;
+var intervalTimer;
+var intervalCounter;
+var intervalLog;
+
+var filterHandler = null;
+var mapHandler = null;
+
+/**
+ * Assigns a handler to filter logs out of the queue.
+ * @param  {Function} callback The handler to invoke when logging.
+ */
+function setLogFilter(callback) {
+  filterHandler = callback;
+}
+
+/**
+ * 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;
+  filterHandler = null;
+  mapHandler = null;
+  intervalID = null;
+  intervalType = null;
+  intervalPath = null;
+  intervalTimer = null;
+  intervalCounter = 0;
+  intervalLog = null;
+}
+
+/**
+ * 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 timeFields = extractTimeFields(
+    (e.timeStamp && e.timeStamp > 0) ? config$1.time(e.timeStamp) : Date.now()
+  );
+
+  var log = {
+    'target' : getSelector(e.target),
+    'path' : buildPath(e),
+    'clientTime' : timeFields.milli,
+    'microTime' : timeFields.micro,
+    'location' : getLocation(e),
+    '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
+  };
+
+  if ((typeof filterHandler === 'function') && !filterHandler(log)) {
+    return false;
+  }
+
+  if (typeof mapHandler === 'function') {
+    log = mapHandler(log);
+  }
+
+  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,
+            '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
+        };
+
+        if (typeof filterHandler === 'function' && 
!filterHandler(intervalLog)) {
+          return false;
+        }
+
+        if (typeof mapHandler === 'function') {
+          intervalLog = mapHandler(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 + 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 };
+  }
+}
+
+/**
+ * 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;
+//@todo: Investigate drag events and their behavior
+var intervalEvents = ['click', 'focus', 'blur', 'input', 'change', 
'mouseover', 'submit'];
+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
+  };
+}
+
+/**
+ * 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,
+    '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 }; }
+  };
+}
+
+/**
+ * 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);
+  });
+
+  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.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 (!config.on) {
+    return;
+  }
+
+  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 = [];
+var 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 (!started) {
+    setTimeout(function() {
+      var state = document.readyState;
+
+      if (state === 'interactive' || state === 'complete') {
+        attachHandlers(config);
+        initSender(logs, config);
+        started = config.on = true;
+      } else {
+        setup(config);
+      }
+    }, 100);
+  }
+}
+
+
+/**
+ * Used to start the logging process if the
+ * autostart configuration option is set to false.
+ */
+function start() {
+  if (!started) {
+    setup(config);
+  }
+
+  config.on = true;
+}
+
+/**
+ * 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;
+}
+
+// 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
+let store = 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();
+  setLogFilter(function (log) {
+    queueLog(Object.assign({}, log, {
+      pageUrl: document.location.href,
+    }));
+    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
+ */
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-senssoft-useralejs/blob/1349cadd/build/UserAleWebExtension/icons/border-48.png
----------------------------------------------------------------------
diff --git a/build/UserAleWebExtension/icons/border-48.png 
b/build/UserAleWebExtension/icons/border-48.png
new file mode 100644
index 0000000..90687de
Binary files /dev/null and b/build/UserAleWebExtension/icons/border-48.png 
differ

http://git-wip-us.apache.org/repos/asf/incubator-senssoft-useralejs/blob/1349cadd/build/UserAleWebExtension/manifest.json
----------------------------------------------------------------------
diff --git a/build/UserAleWebExtension/manifest.json 
b/build/UserAleWebExtension/manifest.json
new file mode 100644
index 0000000..32a47e7
--- /dev/null
+++ b/build/UserAleWebExtension/manifest.json
@@ -0,0 +1,29 @@
+{
+  "manifest_version": 2,
+  "name": "User ALE Extension",
+  "version": "1.0.1",
+  "description": "Injects User ALE into every page for testing purposes",
+  "icons": {
+    "48": "icons/border-48.png"
+  },
+  "permissions": [
+    "activeTab",
+    "storage",
+    "tabs",
+    "<all_urls>"
+  ],
+  "background": {
+    "scripts": ["background.js"]
+  },
+  "content_scripts": [
+    {
+      "matches": [
+        "<all_urls>"
+      ],
+      "js": ["content.js"]
+    }
+  ],
+  "options_ui": {
+    "page": "optionsPage.html"
+  }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-senssoft-useralejs/blob/1349cadd/build/UserAleWebExtension/options.js
----------------------------------------------------------------------
diff --git a/build/UserAleWebExtension/options.js 
b/build/UserAleWebExtension/options.js
new file mode 100644
index 0000000..0c8bbd6
--- /dev/null
+++ b/build/UserAleWebExtension/options.js
@@ -0,0 +1,56 @@
+/* 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-0.2.1.min.js';
+var toolUser = 'nobody';
+var toolName = 'test_app';
+var toolVersion = '0.1.0';
+
+/* eslint-enable */
+
+var prefix = 'USERALE_';
+
+var CONFIG_CHANGE = prefix + 'CONFIG_CHANGE';
+
+if (chrome) {
+  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
+let store = browser.storage.local.get({
+  userAleHost: userAleHost,
+  userAleScript: userAleScript,
+  toolUser: toolUser,
+  toolName: toolName,
+  toolVersion: toolVersion,
+}, storeCallback);
+
+function storeCallback(item) {
+  document.getElementById("host").value = item.userAleHost;
+  document.getElementById("clientScript").value = item.userAleScript;
+  document.getElementById("toolUser").value = item.toolUser;
+  document.getElementById("toolName").value = item.toolName;
+  document.getElementById("toolVersion").value = item.toolVersion;
+}
+
+function saveOptions(e) {
+  const updatedConfig = {
+    userAleHost: document.getElementById("host").value,
+    userAleScript: document.getElementById("clientScript").value,
+    toolUser: document.getElementById("toolUser").value,
+    toolName: document.getElementById("toolName").value,
+    toolVersion: document.getElementById("toolVersion").value,
+  };
+
+  browser.storage.local.set(updatedConfig);
+
+  browser.runtime.sendMessage({ type: CONFIG_CHANGE, payload: updatedConfig });
+}
+
+document.addEventListener("submit", function() {
+  saveOptions();
+});
+
+/* eslint-enable */
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-senssoft-useralejs/blob/1349cadd/build/UserAleWebExtension/optionsPage.html
----------------------------------------------------------------------
diff --git a/build/UserAleWebExtension/optionsPage.html 
b/build/UserAleWebExtension/optionsPage.html
new file mode 100644
index 0000000..d2c4b59
--- /dev/null
+++ b/build/UserAleWebExtension/optionsPage.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <title>User ALE Web Extension - Options</title>
+    <script src="globals.js"></script>
+    <script src="options.js"></script>
+    <meta charset="utf-8">
+</head>
+<body>
+<h1>Options</h1>
+<form>
+    <label>User ALE Server Host:</label>
+    <input id="host"/>
+    <br/>
+    
+    <label>User ALE Client Script:</label>
+    <input id="clientScript"/>
+    <br/>
+
+    <label>User:</label>
+    <input id="toolUser"/>
+    <br/>
+
+    <label>Tool Name:</label>
+    <input id="toolName"/>
+    <br/>
+
+    <label>Tool Version:</label>
+    <input id="toolVersion"/>
+    <br/>
+
+    <div align="right">
+    <button type="submit">Save</button>
+    </div>
+</form>
+</body>
+</html>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-senssoft-useralejs/blob/1349cadd/build/userale-1.0.0.js
----------------------------------------------------------------------
diff --git a/build/userale-1.0.0.js b/build/userale-1.0.0.js
new file mode 100644
index 0000000..38b526a
--- /dev/null
+++ b/build/userale-1.0.0.js
@@ -0,0 +1,702 @@
+(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'));
+         settings.sessionID = get('data-session') || 'session_' + 
String(Date.now());
+
+         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;
+
+       // Interval Logging Globals
+       var intervalID;
+       var intervalType;
+       var intervalPath;
+       var intervalTimer;
+       var intervalCounter;
+       var intervalLog;
+
+       var filterHandler = null;
+       var mapHandler = null;
+
+       /**
+        * Assigns a handler to filter logs out of the queue.
+        * @param  {Function} callback The handler to invoke when logging.
+        */
+       function setLogFilter(callback) {
+         filterHandler = callback;
+       }
+
+       /**
+        * Assigns a handler to transform logs from their default structure.
+        * @param  {Function} callback The handler to invoke when logging.
+        */
+       function setLogMapper(callback) {
+         mapHandler = callback;
+       }
+
+
+       /**
+        * 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;
+         filterHandler = null;
+         mapHandler = null;
+         intervalID = null;
+         intervalType = null;
+         intervalPath = null;
+         intervalTimer = null;
+         intervalCounter = 0;
+         intervalLog = null;
+       }
+
+       /**
+        * 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 timeFields = extractTimeFields(
+           (e.timeStamp && e.timeStamp > 0) ? config$1.time(e.timeStamp) : 
Date.now()
+         );
+
+         var log = {
+           'target' : getSelector(e.target),
+           'path' : buildPath(e),
+           'clientTime' : timeFields.milli,
+           'microTime' : timeFields.micro,
+           'location' : getLocation(e),
+           '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
+         };
+
+         if ((typeof filterHandler === 'function') && !filterHandler(log)) {
+           return false;
+         }
+
+         if (typeof mapHandler === 'function') {
+           log = mapHandler(log);
+         }
+
+         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,
+                   '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
+               };
+
+               if (typeof filterHandler === 'function' && 
!filterHandler(intervalLog)) {
+                 return false;
+               }
+
+               if (typeof mapHandler === 'function') {
+                 intervalLog = mapHandler(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 + 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 };
+         }
+       }
+
+       /**
+        * 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;
+       //@todo: Investigate drag events and their behavior
+       var intervalEvents = ['click', 'focus', 'blur', 'input', 'change', 
'mouseover', 'submit'];
+       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
+         };
+       }
+
+       /**
+        * 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,
+           '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 }; }
+         };
+       }
+
+       /**
+        * 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);
+         });
+
+         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.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 (!config.on) {
+           return;
+         }
+
+         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;
+       exports.map = setLogMapper;
+       exports.filter = setLogFilter;
+
+}((this.userale = this.userale || {})));
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-senssoft-useralejs/blob/1349cadd/build/userale-1.0.0.min.js
----------------------------------------------------------------------
diff --git a/build/userale-1.0.0.min.js b/build/userale-1.0.0.min.js
new file mode 100644
index 0000000..6adafeb
--- /dev/null
+++ b/build/userale-1.0.0.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 o=performance.timing.navigationStart;e=function(t){return 
t+o}}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 
r=o(e[n]);r&&(t.userId=r)}t[n]=e[n]})}function o(t){var e=t,n=new 
RegExp("[?&]"+e+"(=([^&#]*)|&|#|$)"),o=window.location.href.match(n);return 
o&&o[2]?decodeURIComponent(o[2].replace(/\+/g," ")):null}function 
r(t){F=t}function u(t){R=t}function a(t,e){if(!T.on)return!1;var 
n=null;e&&(n=e(t));var 
o=i(t.timeStamp&&t.timeStamp>0?T.time(t.timeStamp):Date.now()),r={target:c(t.target),path:d(t),clientTime:o.milli,microTime:o.micro,location:s(t),type:t.type,logType:"raw",userAction:!0,details:n,userId:T.userId,toolVersion:T.version,toolName:T.toolName,useraleVersion:T.useraleVersion,sessionID:T.sessionID};r
 eturn!("function"==typeof F&&!F(r))&&("function"==typeof 
R&&(r=R(r)),D.push(r),!0)}function 
i(t){return{milli:Math.floor(t),micro:Number((t%1).toFixed(3))}}function 
l(t){var 
e=c(t.target),n=d(t),o=t.type,r=Math.floor(t.timeStamp&&t.timeStamp>0?T.time(t.timeStamp):Date.now());if(null==k&&(k=e,C=o,K=n,L=r,x=0),k!==e||C!==o){if(V={target:k,path:K,count:x,duration:r-L,startTime:L,endTime:r,type:C,logType:"interval",targetChange:k!==e,typeChange:C!==o,userAction:!1,userId:T.userId,toolVersion:T.version,toolName:T.toolName,useraleVersion:T.useraleVersion,sessionID:T.sessionID},"function"==typeof
 F&&!F(V))return!1;"function"==typeof 
R&&(V=R(V)),D.push(V),k=e,C=o,K=n,L=r,x=0}return k==e&&C==o&&(x+=1),!0}function 
s(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
 c(t){return 
t.localName?t.localName+(t.id?"#"+t.id:"")+(t.className?"."+t.className:""):t.nodeNam
 
e?t.nodeName+(t.id?"#"+t.id:"")+(t.className?"."+t.className:""):t&&t.document&&t.location&&t.alert&&t.setInterval?"Window":"Unknown"}function
 d(t){var e=[];if(t.path)e=t.path;else for(var 
n=t.target;n;)e.push(n),n=n.parentElement;return f(e)}function f(t){for(var 
e,n=0,o=[];e=t[n];)o.push(c(e)),++n;return o}function 
m(t){return{clicks:t.detail,ctrl:t.ctrlKey,alt:t.altKey,shift:t.shiftKey,meta:t.metaKey}}function
 
p(t){X={click:m,dblclick:m,mousedown:m,mouseup:m,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},O={},j={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,he
 ight:window.outerHeight}}}}function h(t){return 
p(t),Object.keys(X).forEach(function(t){document.addEventListener(t,function(e){a(e,X[t])},!0)}),Y.forEach(function(t){document.addEventListener(t,function(t){l(t)},!0)}),Object.keys(j).forEach(function(e){O[e]=!0,window.addEventListener(e,function(n){O[e]&&(O[e]=!1,a(n,j[e]),setTimeout(function(){O[e]=!0},t.resolution))},!0)}),A.forEach(function(t){window.addEventListener(t,function(t){a(t,function(){return{window:!0}})},!0)}),!0}function
 g(t,e){null!==B&&clearInterval(B),B=v(t,e),y(t,e)}function v(t,e){return 
setInterval(function(){e.on&&t.length>=e.logCountThreshold&&(w(t.slice(0),e.url,0),t.splice(0))},e.transmitInterval)}function
 
y(t,e){e.on&&(navigator.sendBeacon?window.addEventListener("unload",function(){navigator.sendBeacon(e.url,JSON.stringify(t))}):window.addEventListener("beforeunload",function(){t.length>0&&w(t,e.url,1)}))}function
 w(t,e,n){var o=new 
XMLHttpRequest,r=JSON.stringify(t);o.open("POST",e),o.setRequestHeader("C
 
ontent-type","application/json;charset=UTF-8"),o.onreadystatechange=function(){4===o.readyState&&200!==o.status&&n>0&&w(t,e,n--)},o.send(r)}function
 S(e){t.started||setTimeout(function(){var 
n=document.readyState;"interactive"===n||"complete"===n?(h(e),g(M,e),t.started=e.on=!0):S(e)},100)}function
 E(){t.started||S(H),H.on=!0}function I(){H.on=!1}function N(t){return void 
0!==t&&n(H,t),H}function b(t){return null!==t&&"object"==typeof 
t&&(M.push(t),!0)}var 
D,T,k,C,K,L,x,V,X,O,j,F=null,R=null,Y=["click","focus","blur","input","change","mouseover","submit"],A=["load","blur","focus"],B=null,H={},M=[];t.started=!1,H.on=!1,H.useraleVersion="1.0.0",n(H,function(){var
 t={},n=document.currentScript||function(){var 
t=document.getElementsByTagName("script");return 
t[t.length-1]}(),o=n?n.getAttribute.bind(n):function(){return null};return 
t.autostart="false"!==o("data-autostart"),t.url=o("data-url")||"http://localhost:8000",t.transmitInterval=+o("data-interval")||5e3,t.logCountThreshold=+o("dat
 
a-threshold")||5,t.userId=o("data-user")||null,t.version=o("data-version")||null,t.logDetails="true"===o("data-log-details"),t.resolution=+o("data-resolution")||500,t.toolName=o("data-tool")||null,t.userFromParams=o("data-user-from-params")||null,t.time=e(document.createEvent("CustomEvent")),t.sessionID=o("data-session")||"session_"+String(Date.now()),t}()),function(t,e){D=t,T=e,F=null,R=null,k=null,C=null,K=null,L=null,x=0,V=null}(M,H),H.autostart&&S(H);t.version="1.0.0",t.start=E,t.stop=I,t.options=N,t.log=b,t.map=u,t.filter=r}(this.userale=this.userale||{});
\ No newline at end of file

Reply via email to