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

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