// Copyright 2012 Georg-August-Universität Göttingen, Germany // // Licensed 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. /** * 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 autoquestDestination; /** * an ID that is more or less unique for the client */ var autoquestClientId; /** * the maximum number of recorded events to be put into one package sent to the server */ var autoquestPackageSize = 10; /** * this variable defines the tags for which event handling shall be added, as well as the * event handling action to be monitored */ var autoquestActionConfig = [ { "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 autoquestDoLog = 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 autoquestRecordedEvents = []; /** * 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"); determineDestination(); 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 determines the URL of this script. Based on * this URL, it calculates the destination to which the traced interactions must be sent */ function determineDestination() { var scriptElements = document.getElementsByTagName("script"); var i; var index; for (i = 0; i < scriptElements.length; i++) { if ((scriptElements[i].type === "text/javascript") && (scriptElements[i].src)) { index = scriptElements[i].src.lastIndexOf("script/autoquest-htmlmonitor.js"); if (index > -1) { autoquestDestination = scriptElements[i].src.substring(0, index - 1); log("using destination " + autoquestDestination); } } } } /** * 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 < autoquestActionConfig.length; i++) { if (node.tagName.toLowerCase() === autoquestActionConfig[i].tag.toLowerCase()) { for (k = 0; k < autoquestActionConfig[i].actions.length; k++) { value = "handleEvent('" + autoquestActionConfig[i].actions[k] + "', '" + nodePath + "', event);"; if (!node.getAttribute(autoquestActionConfig[i].actions[k])) { node.setAttribute(autoquestActionConfig[i].actions[k], value); } else { var oldValue = node.getAttribute(autoquestActionConfig[i].actions[k]); if (oldValue.indexOf(value) < 0) { node.setAttribute(autoquestActionConfig[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(); if ("input" === node.tagName.toLowerCase()) { if (node.type && (node.type !== "")) { nodePath += "_" + node.type; } else { nodePath += "_text"; } } } 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 autoquestRecordedEvents. If this list achieves the maximum * autoquestPackageSize 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) { log("handling event " + eventName); if (!autoquestDestination) { // do nothing if we have no destination to send data to return; } var eventType = eventName.toLowerCase(); var eventObj = autoquestRecordedEvents.pop(); // reuse previous on scroll events to prevent too many events if ((eventName !== "onscroll") || (!eventObj) || (eventObj.type !== "onscroll")) { if (eventObj) { autoquestRecordedEvents.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); } } log("storing event " + eventName); autoquestRecordedEvents.push(eventObj); if (autoquestRecordedEvents.length >= autoquestPackageSize) { log("initiating sending events"); setTimeout(sendRequest, 100); } else if (eventType === "onunload") { log("initiating sending events"); sendRequest(); } } /** * 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 autoquestDestination to send it to * the server */ function sendRequest() { var eventList; var message; var clientId; var i = 0; var request; if (autoquestRecordedEvents.length > 0) { log("creating message"); eventList = autoquestRecordedEvents; autoquestRecordedEvents = []; message = "{\"message\":{\"clientInfos\":{"; log("reading client id"); clientId = getClientId(); 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", autoquestDestination, false); request.setRequestHeader("Content-Type", "application/json"); 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]; } /** * generates a client id based on several information retrieved from the environment. The client * id is not always unique * * @returns the client id */ function getClientId() { var clientIdStr; var clientId; var i = 0; if (!autoquestClientId) { // create something like a more or less unique checksum. clientIdStr = navigator.appCodeName + navigator.appName + navigator.appVersion + navigator.cookieEnabled + navigator.language + navigator.platform + navigator.userAgent + navigator.javaEnabled() + window.location.protocol + window.location.host + new Date().getTimezoneOffset(); clientId = clientIdStr.length; for (i = 0; i < clientIdStr.length; i++) { clientId += clientIdStr.charCodeAt(i); } autoquestClientId = clientId; } return autoquestClientId; } /** * performs a simple logging by adding a specific div to the HTML * * @param text the text to be logged */ function log(text) { if (autoquestDoLog) { 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; }; }