/** * 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://someserver: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; }; }