source: trunk/autoquest-htmlmonitor/src/main/js/autoquest-htmlmonitor.js @ 881

Last change on this file since 881 was 881, checked in by pharms, 12 years ago
  • changed client ID generation to not use cookies as those may be deleted by the monitored web application. Furthermore, cookies are relative to a concrete page on a side. This requires the usage of session ids which are handshaked with the server. The implementation of this behaviour would be rather complex.
File size: 15.5 KB
Line 
1/**
2 * AutoQUEST - HTML Monitor
3 *
4 * Description: This script records the interactions done by an user on an
5 * HTML-Website and sends them to a server. It does not register actions on
6 * Flash, Java, or other special inputs. This script is tested on Firefox
7 * 15.0.1.
8 *
9 * To insert it on your HTML-side, you need to write <script
10 * language="JavaScript" type="text/javascript" src="autoquest-htmlmonitor.js"></script> in the
11 * head and change the src-attribute to the location, you have chosen for this
12 * script.
13 *
14 * To change the recorded events, edit the action config array. If you want to change
15 * the server where the data is send to, rewrite the destination variable. The
16 * records are send to the server, JSON-formatted, if there are 10 inputs or the
17 * user changes/closes the site.
18 *
19 * Authors: Simon Leidenbach, Simon Reuss, Patrick Harms
20 *
21 * Version: 0.1
22 */
23
24/**
25 * the server to send the recorded data to
26 */
27var autoquestDestination;
28
29/**
30 * an ID that is more or less unique for the client
31 */
32var autoquestClientId;
33
34/**
35 * the maximum number of recorded events to be put into one package sent to the server
36 */
37var autoquestPackageSize = 10;
38
39/**
40 * this variable defines the tags for which event handling shall be added, as well as the
41 * event handling action to be monitored
42 */
43var autoquestActionConfig = [
44    { "tag": "body", "actions": [ "onunload", "onscroll" ] },
45    { "tag": "a", "actions": [ "onclick", "ondblclick", "onfocus" ] },
46    { "tag": "input", "actions": [ "onclick", "ondblclick", "onfocus" ] }
47];
48
49/**
50 * a possibility to trace, what is going on
51 */
52var autoquestDoLog = false;
53
54/*var matchedTags = ["A", "ABBR", "ACRONYM", "ADDRESS", "AREA", "B", "BIG", "BLOCKQUOTE", "BODY",
55                   "BUTTON", "CAPTION", "CENTER", "CITE", "CODE", "COL", "COLGROUP", "DD", "DEL",
56                   "DFN", "DIR", "DIV", "DL", "DT", "EM", "FIELDSET", "FORM", "H1", "H2", "H3",
57                   "H4", "H5", "H6", "HR", "I", "IMG", "INPUT", "INS", "KBD", "LABEL", "LEGEND",
58                   "LI", "LINK", "MAP", "MENU", "NOFRAMES", "NOSCRIPT", "OBJECT", "OL",
59                   "OPTGROUP", "OPTION", "P", "PRE", "Q", "S", "SAMP", "SELECT", "SMALL", "SPAN",
60                   "STRIKE", "STRONG", "SUB", "SUP", "TABLE", "TBODY", "TD", "TEXTAREA", "TFOOT",
61                   "TH", "THEAD", "TR", "TT", "U", "UL", "VAR"];*/
62/*var actions = ['onclick', 'ondblclick', 'onkeypress', 'onkeydown', 'onkeyup',
63'onmouseout' , 'onmousemove' ,'onfocus','onscroll'];  // edit*/
64
65/**
66 * stores events, which were recorded but not sent to the server yet
67 */
68var autoquestRecordedEvents = [];
69
70/**
71 * automatically executed to initialize the event handling
72 */
73(function() {
74    initEventHandling();
75}());
76
77
78/**
79 * initializes the event handling after the document is loaded completely
80 */
81function initEventHandling() {
82    if (document.body) {
83        log("adding event handling attributes");
84        determineDestination();
85        addEventHandlingAttributes(document.documentElement, "");
86       
87        if (document.readyState !== "complete") {
88            // if the document is not loaded yet, try to add further event handling later
89            setTimeout(initEventHandling, 200);
90        }
91    }
92    else {
93        setTimeout(initEventHandling, 200);
94    }         
95}
96
97/**
98 * traverses the DOM-structure of the HTML-site and determines the URL of this script. Based on
99 * this URL, it calculates the destination to which the traced interactions must be sent
100 */
101function determineDestination() {
102    var scriptElements = document.getElementsByTagName("script");
103    var i;
104    var index;
105   
106    for (i = 0; i < scriptElements.length; i++) {
107        if ((scriptElements[i].type === "text/javascript") && (scriptElements[i].src)) {
108            index = scriptElements[i].src.lastIndexOf("script/autoquest-htmlmonitor.js");
109            if (index > -1) {
110                autoquestDestination = scriptElements[i].src.substring(0, index - 1);
111                log("using destination " + autoquestDestination);
112            }
113        }
114    }
115}
116
117/**
118 * traverses the DOM-structure of the HTML-site and adds event handling attributes to each
119 * relevant node
120 *
121 * @param node       the node of the DOM structure that shall be adapted and whose children shall
122 *                   be traversed
123 * @param parentPath the path to the parent node of the provided node within the DOM-structure of
124 *                   the HTML-site
125 */
126function addEventHandlingAttributes(node, parentPath) {
127    var nodePath = getNodePath(node, parentPath);
128    var i;
129    var k;
130    var value;
131   
132    if (node.nodeType === Node.ELEMENT_NODE) {
133        for (i = 0; i < autoquestActionConfig.length; i++) {
134            if (node.tagName.toLowerCase() === autoquestActionConfig[i].tag.toLowerCase()) {
135                for (k = 0; k < autoquestActionConfig[i].actions.length; k++) {
136                    value = "handleEvent('" + autoquestActionConfig[i].actions[k] + "', '" +
137                        nodePath + "', event);";
138
139                    if (!node.getAttribute(autoquestActionConfig[i].actions[k])) {
140                        node.setAttribute(autoquestActionConfig[i].actions[k], value);
141                    }
142                    else {
143                        var oldValue = node.getAttribute(autoquestActionConfig[i].actions[k]);
144                        if (oldValue.indexOf(value) < 0) {
145                            node.setAttribute(autoquestActionConfig[i].actions[k],
146                                              value + ' ' + oldValue);
147                        }
148                    }
149                }
150            }
151        }
152    }
153   
154    for (i = 0; i < node.childNodes.length; i++) {
155        addEventHandlingAttributes(node.childNodes[i], nodePath);
156    }
157}
158
159/**
160 * generates a path through the DOM-structure of the HTML-site depending on a node and the path
161 * to its parent node. The result is the parent path plus a path element for the provided node.
162 * If the node has a tag name, this is the first part of the path element generated for the node.
163 * If the node has an id, it becomes the second part of the path element. If the node does not have
164 * an id, the method calculates the index of the node within all children of the same type within
165 * the parent node. This index becomes then the second part of the path element generated for the
166 * node.
167 *
168 * @param node       the node of the DOM structure for which the path shall be created
169 * @param parentPath the path to the parent node of the provided node
170 *
171 * @returns a path in the DOM-structure of the HTML-site including the parent path an a path
172 *          element for the provided node
173 */
174function getNodePath(node, parentPath) {
175    var nodePath = parentPath + "/";
176    var index = -1;
177    var i = 0;
178   
179    if (node.nodeType === Node.ELEMENT_NODE) {
180        nodePath += node.tagName.toLowerCase();
181    }
182    else {
183        nodePath += "undefined";
184    }
185   
186    if ((node.id) && (node.id !== "")) {
187        nodePath += "(id=" + node.id + ")";
188    }
189    else {
190       
191        if (node.parentNode) {
192            for (i = 0; i < node.parentNode.childNodes.length; i++) {
193                if (node.parentNode.childNodes[i].tagName === node.tagName) {
194                    index++;
195                    // if === also returns true if the nodes are not identical but only equal,
196                    // this may fail.
197                    if (node.parentNode.childNodes[i] === node) {
198                        break;
199                    }
200                }
201            }
202           
203        }
204        else {
205            index = 0;
206        }
207       
208        nodePath += "[" + index + "]";
209    }
210   
211    return nodePath;
212}
213
214/**
215 * handles an event that happened on a node. This method is called by the event handling attributes
216 * of the nodes. These attributes are generated by the
217 * {@link #addEventHandlingAttributes(node,parentPath)} function. It creates a new Event object and
218 * add it to the list of <code>autoquestRecordedEvents</code>. If this list achieves the maximum
219 * <code>autoquestPackageSize</code> the events in the list are sent to the server asynchronously
220 * through calling {@link #sendRequest()}.
221 *
222 * @param eventName the name of the event, e.g. onscroll
223 * @param nodePath  the path to the node in the HTML DOM on which the event occurred
224 * @param event     the HTML event that occured
225 */
226function handleEvent(eventName, nodePath, event) {
227    log("handling event " + eventName);
228   
229    if (!autoquestDestination) {
230        // do nothing if we have no destination to send data to
231        return;
232    }
233   
234    var eventType = eventName.toLowerCase();
235   
236    var eventObj = autoquestRecordedEvents.pop();
237   
238    // reuse previous on scroll events to prevent too many events
239    if ((eventName !== "onscroll") || (!eventObj) || (eventObj.type !== "onscroll")) {
240        if (eventObj) {
241            autoquestRecordedEvents.push(eventObj);
242        }
243        eventObj = new Event(eventType, nodePath);
244    }
245   
246    if ((eventType === "onclick") || (eventType === "ondblclick")) {
247        eventObj.setClickCoordinates(getEventCoordinates(event));
248    }
249
250    if ((eventType === "onkeypress") || (eventType === "onkeydown") || (eventType === "onkeyup")) {
251        eventObj.setKey(event.keyCode);
252    }
253   
254    if (eventType === "onscroll") {
255        if (window.pageYOffset) {
256            eventObj.setScrollPosition(window.pageYOffset);
257        }
258    }
259
260    log("storing event " + eventName);
261    autoquestRecordedEvents.push(eventObj);
262
263    if (autoquestRecordedEvents.length >= autoquestPackageSize) {
264        log("initiating sending events");
265        setTimeout(sendRequest, 100);
266    }
267    else if (eventType === "onunload") {
268        log("initiating sending events");
269        sendRequest();
270    }
271
272}
273
274/**
275 * sends the collected data to the server, named in the destination-variable. For this it generates
276 * a JSON formatted message and uses Ajax and the <code>autoquestDestination</code> to send it to
277 * the server
278 */
279function sendRequest() {
280    var eventList;
281    var message;
282    var clientId;
283    var i = 0;
284    var request;
285   
286    if (autoquestRecordedEvents.length > 0) {
287        log("creating message");
288        eventList = autoquestRecordedEvents;
289        autoquestRecordedEvents = [];
290       
291        message = "{\"message\":{\"clientInfos\":{";
292       
293        log("reading client id");
294        clientId = getClientId();
295        if ((clientId) && (clientId !== "")) {
296            message += "\"clientId\":\"" + clientId + "\",";
297        }
298       
299        log("adding other infos");
300        message += "\"userAgent\":\"" + navigator.userAgent + "\",";
301        message += "\"title\":\"" + document.title + "\",";
302        message += "\"url\":\"" + document.URL + "\"},";
303       
304       
305        message += "\"events\":[";
306       
307        for (i = 0; i < eventList.length; i++) {
308            if (i > 0) {
309                message += ",";
310            }
311            message += eventList[i].toJSON();
312        }
313       
314        message += "]}}";
315       
316        request = null;
317       
318        // Mozilla
319        if (window.XMLHttpRequest) {
320            request = new XMLHttpRequest();
321        }
322        // IE
323        else if (window.ActiveXObject) {
324            request = new ActiveXObject("Microsoft.XMLHTTP");
325        }
326       
327        request.open("POST", autoquestDestination, false);
328        request.setRequestHeader("Content-Type", "application/json");
329
330        log("sending " + message);
331        request.send(message);
332    }
333}
334
335/**
336 * determines the coordinates of an onclick or ondblclick event. If the coordinates to not come
337 * with the provided event, they are determined based on the surrounding object
338 *
339 * @param event the event to extract the coordinates of
340 *
341 * @returns the coordinates of the event as an array with x and y coordinate
342 */
343function getEventCoordinates(event) {
344    if (event.layerX) {
345        return [event.layerX, event.layerY];
346    }
347    else if (event.offsetX) {
348        return [event.offsetX, event.offsetY];
349    }
350
351    var obj = event.target || event.srcElement;
352    var objOffset = getPageOffset(obj);
353
354    return [(event.clientX - objOffset[0]), (event.clientY - objOffset[1])];
355}
356
357/**
358 * determines the page offset of an object using the parent objects offset
359 */
360function getPageOffset(object) {
361    var top = 0;
362    var left = 0;
363    var obj = object;
364
365    while (obj.offsetParent) {
366        left += obj.offsetLeft;
367        top += obj.offsetTop;
368        obj = obj.offsetParent;
369    }
370
371    return [left, top];
372}
373
374/**
375 * generates a client id based on several information retrieved from the environment. The client
376 * id is not always unique
377 *
378 * @returns the client id
379 */
380function getClientId() {
381    var clientIdStr;
382    var clientId;
383    var i = 0;
384   
385    if (!autoquestClientId) {
386        // create something like a more or less unique checksum.
387        clientIdStr =
388            navigator.appCodeName + navigator.appName + navigator.appVersion +
389            navigator.cookieEnabled + navigator.language + navigator.platform +
390            navigator.userAgent + navigator.javaEnabled() + window.location.protocol +
391            window.location.host + new Date().getTimezoneOffset();
392
393        clientId = clientIdStr.length;
394
395        for (i = 0; i < clientIdStr.length; i++) {
396            clientId += clientIdStr.charCodeAt(i);
397        }
398       
399        autoquestClientId = clientId;
400    }
401
402    return autoquestClientId;
403}
404
405/**
406 * performs a simple logging by adding a specific div to the HTML
407 *
408 * @param text the text to be logged
409 */
410function log(text) {
411    if (autoquestDoLog) {
412        var loggingInfo = document.getElementById("loggingInfoParagraph");
413       
414        if (!loggingInfo) {
415            var div = document.createElement("div");
416            div.setAttribute("style", "z-index:1000000; background-color:#afa; " +
417                             "border:1px black solid; position:absolute; " +
418                             "top:10px; right:10px; width:350px; height:auto");
419           
420            loggingInfo = document.createElement("div");
421            loggingInfo.id = "loggingInfoParagraph";
422            div.appendChild(loggingInfo);
423           
424            var body = document.getElementsByTagName("body")[0];
425            if (!body) {
426                alert("could not enable logging");
427            }
428            else {
429                body.appendChild(div);
430            }
431        }
432       
433        loggingInfo.appendChild(document.createTextNode(text));
434        loggingInfo.appendChild(document.createElement("br"));
435    }
436}
437
438/**
439 * this class represents a single event
440 *
441 * @param type     the type of the event
442 * @param nodePath the path through the HTML DOM to the event target
443 */
444function Event(type, nodePath) {
445    this.type = type;
446    this.nodePath = nodePath;
447    this.time = new Date().getTime();
448   
449    this.setClickCoordinates = function(coordinates) {
450          this.clickCoordinates = coordinates;
451    };
452   
453    this.setKey = function(key) {
454          this.key = key;
455    };
456   
457    this.setScrollPosition = function(scrollPosition) {
458          this.scrollPosition = scrollPosition;
459    };
460   
461    this.toJSON = function() {
462        var eventInJSON =
463            "{\"path\":\"" + this.nodePath + "\",\"time\":" + this.time + ",\"eventType\":\"" +
464            this.type + "\"";
465
466        if ((this.clickCoordinates) && (!isNaN(this.clickCoordinates[0]))) {
467            eventInJSON += ",\"coordinates\":[" + this.clickCoordinates + "]";
468        }
469
470        if (this.key) {
471            eventInJSON += ",\"key\":" + this.key;
472        }
473       
474        if (this.scrollPosition) {
475            eventInJSON += ",\"scrollPosition\":" + this.scrollPosition;
476        }
477
478        eventInJSON += "}";
479       
480        return eventInJSON;
481    };
482}
483
Note: See TracBrowser for help on using the repository browser.