/**
* AutoQUEST - HTML Monitor
*
* Description: This script records the interactions done by an user on an
* HTML-Website and sends them to a server. It does not register actions on
* Flash, Java, or other special inputs. This script is tested on Firefox
* 15.0.1.
*
* To insert it on your HTML-side, you need to write in the
* head and change the src-attribute to the location, you have chosen for this
* script.
*
* To change the recorded events, edit the action config array. If you want to change
* the server where the data is send to, rewrite the destination variable. The
* records are send to the server, JSON-formatted, if there are 10 inputs or the
* user changes/closes the site.
*
* Authors: Simon Leidenbach, Simon Reuss, Patrick Harms
*
* Version: 0.1
*/
/**
* the server to send the recorded data to
*/
var destination = "http://localhost:8090"; // change to the location of your server
/**
* the maximum number of recorded events to be put into one package sent to the server
*/
var packageSize = 10;
/**
* this variable defines the tags for which event handling shall be added, as well as the
* event handling action to be monitored
*/
var actionConfig = [
{ "tag": "body", "actions": [ "onunload", "onscroll" ] },
{ "tag": "a", "actions": [ "onclick", "ondblclick", "onfocus" ] },
{ "tag": "input", "actions": [ "onclick", "ondblclick", "onfocus" ] }
];
/**
* a possibility to trace, what is going on
*/
var doLog = false;
/*var matchedTags = ["A", "ABBR", "ACRONYM", "ADDRESS", "AREA", "B", "BIG", "BLOCKQUOTE", "BODY",
"BUTTON", "CAPTION", "CENTER", "CITE", "CODE", "COL", "COLGROUP", "DD", "DEL",
"DFN", "DIR", "DIV", "DL", "DT", "EM", "FIELDSET", "FORM", "H1", "H2", "H3",
"H4", "H5", "H6", "HR", "I", "IMG", "INPUT", "INS", "KBD", "LABEL", "LEGEND",
"LI", "LINK", "MAP", "MENU", "NOFRAMES", "NOSCRIPT", "OBJECT", "OL",
"OPTGROUP", "OPTION", "P", "PRE", "Q", "S", "SAMP", "SELECT", "SMALL", "SPAN",
"STRIKE", "STRONG", "SUB", "SUP", "TABLE", "TBODY", "TD", "TEXTAREA", "TFOOT",
"TH", "THEAD", "TR", "TT", "U", "UL", "VAR"];*/
/*var actions = ['onclick', 'ondblclick', 'onkeypress', 'onkeydown', 'onkeyup',
'onmouseout' , 'onmousemove' ,'onfocus','onscroll']; // edit*/
/**
* stores events, which were recorded but not sent to the server yet
*/
var recordedEvents = [];
/**
* automatically executed to initialize the event handling
*/
(function() {
initEventHandling();
}());
/**
* initializes the event handling after the document is loaded completely
*/
function initEventHandling() {
if (document.body) {
log("adding event handling attributes");
addEventHandlingAttributes(document.documentElement, "");
if (document.readyState !== "complete") {
// if the document is not loaded yet, try to add further event handling later
setTimeout(initEventHandling, 200);
}
}
else {
setTimeout(initEventHandling, 200);
}
}
/**
* traverses the DOM-structure of the HTML-site, and adds event handling attributes to each
* relevant node
*
* @param node the node of the DOM structure that shall be adapted and whose children shall
* be traversed
* @param parentPath the path to the parent node of the provided node within the DOM-structure of
* the HTML-site
*/
function addEventHandlingAttributes(node, parentPath) {
var nodePath = getNodePath(node, parentPath);
var i;
var k;
var value;
if (node.nodeType === Node.ELEMENT_NODE) {
for (i = 0; i < actionConfig.length; i++) {
if (node.tagName.toLowerCase() === actionConfig[i].tag.toLowerCase()) {
for (k = 0; k < actionConfig[i].actions.length; k++) {
value = "handleEvent('" + actionConfig[i].actions[k] + "', '" + nodePath +
"', event);";
if (!node.getAttribute(actionConfig[i].actions[k])) {
node.setAttribute(actionConfig[i].actions[k], value);
}
else {
var oldValue = node.getAttribute(actionConfig[i].actions[k]);
if (oldValue.indexOf(value) < 0) {
node.setAttribute(actionConfig[i].actions[k], value + ' ' + oldValue);
}
}
}
}
}
}
for (i = 0; i < node.childNodes.length; i++) {
addEventHandlingAttributes(node.childNodes[i], nodePath);
}
}
/**
* generates a path through the DOM-structure of the HTML-site depending on a node and the path
* to its parent node. The result is the parent path plus a path element for the provided node.
* If the node has a tag name, this is the first part of the path element generated for the node.
* If the node has an id, it becomes the second part of the path element. If the node does not have
* an id, the method calculates the index of the node within all children of the same type within
* the parent node. This index becomes then the second part of the path element generated for the
* node.
*
* @param node the node of the DOM structure for which the path shall be created
* @param parentPath the path to the parent node of the provided node
*
* @returns a path in the DOM-structure of the HTML-site including the parent path an a path
* element for the provided node
*/
function getNodePath(node, parentPath) {
var nodePath = parentPath + "/";
var index = -1;
var i = 0;
if (node.nodeType === Node.ELEMENT_NODE) {
nodePath += node.tagName.toLowerCase();
}
else {
nodePath += "undefined";
}
if ((node.id) && (node.id !== "")) {
nodePath += "(id=" + node.id + ")";
}
else {
if (node.parentNode) {
for (i = 0; i < node.parentNode.childNodes.length; i++) {
if (node.parentNode.childNodes[i].tagName === node.tagName) {
index++;
// if === also returns true if the nodes are not identical but only equal,
// this may fail.
if (node.parentNode.childNodes[i] === node) {
break;
}
}
}
}
else {
index = 0;
}
nodePath += "[" + index + "]";
}
return nodePath;
}
/**
* handles an event that happened on a node. This method is called by the event handling attributes
* of the nodes. These attributes are generated by the
* {@link #addEventHandlingAttributes(node,parentPath)} function. It creates a new Event object and
* add it to the list of recordedEvents
. If this list achieves the maximum
* packageSize
the events in the list are sent to the server asynchronously through
* calling {@link #sendRequest()}.
*
* @param eventName the name of the event, e.g. onscroll
* @param nodePath the path to the node in the HTML DOM on which the event occurred
* @param event the HTML event that occured
*/
function handleEvent(eventName, nodePath, event) {
var eventType = eventName.toLowerCase();
var eventObj = recordedEvents.pop();
// reuse previous on scroll events to prevent too many events
if ((eventName !== "onscroll") || (!eventObj) || (eventObj.type !== "onscroll")) {
if (eventObj) {
recordedEvents.push(eventObj);
}
eventObj = new Event(eventType, nodePath);
}
if ((eventType === "onclick") || (eventType === "ondblclick")) {
eventObj.setClickCoordinates(getEventCoordinates(event));
}
if ((eventType === "onkeypress") || (eventType === "onkeydown") || (eventType === "onkeyup")) {
eventObj.setKey(event.keyCode);
}
if (eventType === "onscroll") {
if (window.pageYOffset) {
eventObj.setScrollPosition(window.pageYOffset);
}
}
recordedEvents.push(eventObj);
if ((recordedEvents.length >= packageSize) || (eventType === "onunload")) {
setTimeout(sendRequest, 100);
}
}
/**
* sends the collected data to the server, named in the destination-variable. For this it generates
* a JSON formatted message and uses Ajax and the destination
to send it to the server
*/
function sendRequest() {
var eventList;
var message;
var clientId;
var i = 0;
var request;
if (recordedEvents.length > 0) {
eventList = recordedEvents;
recordedEvents = [];
message = "{\"message\":{\"clientInfos\":{";
log("reading client id");
clientId = readClientId();
if ((clientId) && (clientId !== "")) {
message += "\"clientId\":\"" + clientId + "\",";
}
log("adding other infos");
message += "\"userAgent\":\"" + navigator.userAgent + "\",";
message += "\"title\":\"" + document.title + "\",";
message += "\"url\":\"" + document.URL + "\"},";
message += "\"events\":[";
for (i = 0; i < eventList.length; i++) {
if (i > 0) {
message += ",";
}
message += eventList[i].toJSON();
}
message += "]}}";
request = null;
// Mozilla
if (window.XMLHttpRequest) {
request = new XMLHttpRequest();
}
// IE
else if (window.ActiveXObject) {
request = new ActiveXObject("Microsoft.XMLHTTP");
}
request.open("POST", destination, true);
log("sending " + message);
request.send(message);
}
}
/**
* determines the coordinates of an onclick or ondblclick event. If the coordinates to not come
* with the provided event, they are determined based on the surrounding object
*
* @param event the event to extract the coordinates of
*
* @returns the coordinates of the event as an array with x and y coordinate
*/
function getEventCoordinates(event) {
if (event.layerX) {
return [event.layerX, event.layerY];
}
else if (event.offsetX) {
return [event.offsetX, event.offsetY];
}
var obj = event.target || event.srcElement;
var objOffset = getPageOffset(obj);
return [(event.clientX - objOffset[0]), (event.clientY - objOffset[1])];
}
/**
* determines the page offset of an object using the parent objects offset
*/
function getPageOffset(object) {
var top = 0;
var left = 0;
var obj = object;
while (obj.offsetParent) {
left += obj.offsetLeft;
top += obj.offsetTop;
obj = obj.offsetParent;
}
return [left, top];
}
/**
* reads the id of the client from the cookies.
*
* @returns the client id or null, if none is found in the cookies
*/
function readClientId() {
var cookie = document.cookie;
var expectedCookieName = getClientIdCookieName();
var cookiename = null;
var startIndex = 0;
var clientId = null;
do {
cookie = cookie.substring(startIndex, cookie.length);
cookiename = cookie.substr(0, cookie.search('='));
startIndex = cookie.search(';') + 1;
while (cookie.charAt(startIndex) === ' ') {
startIndex++;
}
}
while ((startIndex > 0) && (cookiename !== expectedCookieName));
if (cookiename === expectedCookieName) {
clientId = cookie.substr(cookie.search('=') + 1, cookie.search(';'));
if (clientId === "") {
clientId = cookie.substr(cookie.search('=') + 1, cookie.length);
}
}
if ((!clientId) || (clientId === "") || (clientId.search(getClientIdPrefix()) !== 0)) {
clientId = generateClientId();
storeClientId(clientId);
}
return clientId;
}
/**
* stores the provided client id in the cookies
*
* @param clientId the client id to be stored
*/
function storeClientId(clientId) {
if ((clientId) && (clientId !== "")) {
var expiry = new Date();
// 10 years should be sufficient :-)
expiry = new Date(expiry.getTime() + 1000*60*60*24*365*10);
document.cookie = getClientIdCookieName() + '=' + clientId +
'; expires=' + expiry.toGMTString()+';';
}
}
/**
* returns the name of the cookie used to store the client id
*
* @returns as described
*/
function getClientIdCookieName() {
return document.URL + "/quest-htmlmonitor/quest-client-id";
}
/**
* generates a client id based on the result of {@link #getClientIdPrefix()} and the current time
* stamp
*
* @returns the client id
*/
function generateClientId() {
return getClientIdPrefix() + new Date().getTime();
}
/**
* generates a client id prefix based on the user agent and the navigators platform. The prefix
* is a simple checksum of the concatenation of both strings
*
* @returns the client id prefix
*/
function getClientIdPrefix() {
// create something like a more or less unique checksum. It is sufficient, if it differs
// only often, but not always, because it is concatenated with a time stamp, which differs
// much more often.
var prefixStr = navigator.userAgent + "_" + navigator.platform;
var prefixId = prefixStr.length;
var i = 0;
for (i = 0; i < prefixStr.length; i++) {
prefixId += prefixStr.charCodeAt(i);
}
// ensure, that a string is created and not a long. Otherwise, it can not be checked, if an
// existing client id starts with the client id prefix and can therefore be reused.
return prefixId.toString();
}
/**
* performs a simple logging by adding a specific div to the HTML
*
* @param text the text to be logged
*/
function log(text) {
if (doLog) {
var loggingInfo = document.getElementById("loggingInfoParagraph");
if (!loggingInfo) {
var div = document.createElement("div");
div.setAttribute("style", "z-index:1000000; background-color:#afa; " +
"border:1px black solid; position:absolute; " +
"top:10px; right:10px; width:350px; height:auto");
loggingInfo = document.createElement("div");
loggingInfo.id = "loggingInfoParagraph";
div.appendChild(loggingInfo);
var body = document.getElementsByTagName("body")[0];
if (!body) {
alert("could not enable logging");
}
else {
body.appendChild(div);
}
}
loggingInfo.appendChild(document.createTextNode(text));
loggingInfo.appendChild(document.createElement("br"));
}
}
/**
* this class represents a single event
*
* @param type the type of the event
* @param nodePath the path through the HTML DOM to the event target
*/
function Event(type, nodePath) {
this.type = type;
this.nodePath = nodePath;
this.time = new Date().getTime();
this.setClickCoordinates = function(coordinates) {
this.clickCoordinates = coordinates;
};
this.setKey = function(key) {
this.key = key;
};
this.setScrollPosition = function(scrollPosition) {
this.scrollPosition = scrollPosition;
};
this.toJSON = function() {
var eventInJSON =
"{\"path\":\"" + this.nodePath + "\",\"time\":" + this.time + ",\"eventType\":\"" +
this.type + "\"";
if ((this.clickCoordinates) && (!isNaN(this.clickCoordinates[0]))) {
eventInJSON += ",\"coordinates\":[" + this.clickCoordinates + "]";
}
if (this.key) {
eventInJSON += ",\"key\":" + this.key;
}
if (this.scrollPosition) {
eventInJSON += ",\"scrollPosition\":" + this.scrollPosition;
}
eventInJSON += "}";
return eventInJSON;
};
}