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

Last change on this file since 858 was 858, checked in by pharms, 12 years ago
  • initial version of the HTML monitor
File size: 13.1 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
61var recordedEvents = [];
62
63/**
64 * is automatically executed to initialize the event handling
65 */
66(function() {
67    initEventHandling();
68}());
69
70
71/**
72 * initializes the event handling after the document is loaded completely
73 */
74function initEventHandling() {
75    if (document.body) {
76        log("adding event handling attributes");
77        addEventHandlingAttributes(document.documentElement, "");
78       
79        if (document.readyState !== "complete") {
80            // if the document is not loaded yet, try to add further event handling later
81            setTimeout(initEventHandling, 200);
82        }
83    }
84    else {
85        setTimeout(initEventHandling, 200);
86    }         
87}
88
89/**
90 * traverses the DOM-structure of the HTML-side, and adds event handling attributes to each
91 * relevant node
92 *
93 * @param node the node of the DOM structure that shall be adapted and whose children shall be
94 *             traversed
95 */
96function addEventHandlingAttributes(node, parentPath) {
97    var nodePath = getNodePath(node, parentPath);
98    var i;
99    var k;
100    var value;
101   
102    if (node.nodeType === Node.ELEMENT_NODE) {
103        for (i = 0; i < actionConfig.length; i++) {
104            if (node.tagName.toLowerCase() === actionConfig[i].tag.toLowerCase()) {
105                for (k = 0; k < actionConfig[i].actions.length; k++) {
106                    value = "handleEvent('" + actionConfig[i].actions[k] + "', '" + nodePath +
107                        "', event);";
108
109                    if (!node.getAttribute(actionConfig[i].actions[k])) {
110                        node.setAttribute(actionConfig[i].actions[k], value);
111                    }
112                    else {
113                        var oldValue = node.getAttribute(actionConfig[i].actions[k]);
114                        if (oldValue.indexOf(value) < 0) {
115                            node.setAttribute(actionConfig[i].actions[k], value + ' ' + oldValue);
116                        }
117                    }
118                }
119            }
120        }
121    }
122   
123    for (i = 0; i < node.childNodes.length; i++) {
124        addEventHandlingAttributes(node.childNodes[i], nodePath);
125    }
126}
127
128/**
129 *
130 */
131function getNodePath(node, parentPath) {
132    var nodePath = parentPath + "/";
133    var index = -1;
134    var i = 0;
135   
136    if (node.nodeType === Node.ELEMENT_NODE) {
137        nodePath += node.tagName.toLowerCase();
138    }
139    else {
140        nodePath += "undefined";
141    }
142   
143    if ((node.id) && (node.id !== "")) {
144        nodePath += "(id=" + node.id + ")";
145    }
146    else {
147       
148        if (node.parentNode) {
149            for (i = 0; i < node.parentNode.childNodes.length; i++) {
150                if (node.parentNode.childNodes[i].tagName === node.tagName) {
151                    index++;
152                    // if === also returns true if the nodes are not identical but only equal,
153                    // this may fail.
154                    if (node.parentNode.childNodes[i] === node) {
155                        break;
156                    }
157                }
158            }
159           
160        }
161        else {
162            index = 0;
163        }
164       
165        nodePath += "[" + index + "]";
166    }
167   
168    return nodePath;
169}
170
171/**
172 * handles an event that happened on a node
173 *
174 * @param node
175 * @returns {Boolean}
176 */
177function handleEvent(eventName, nodePath, event) {
178    var eventType = eventName.toLowerCase();
179   
180    var eventObj = recordedEvents.pop();
181   
182    // reuse previous on scroll events to prevent too many events
183    if ((eventName !== "onscroll") || (!eventObj) || (eventObj.type !== "onscroll")) {
184        if (eventObj) {
185            recordedEvents.push(eventObj);
186        }
187        eventObj = new Event(eventType, nodePath);
188    }
189   
190    if ((eventType === "onclick") || (eventType === "ondblclick")) {
191        eventObj.setClickCoordinates(getEventCoordinates(event));
192    }
193
194    if ((eventType === "onkeypress") || (eventType === "onkeydown") || (eventType === "onkeyup")) {
195        eventObj.setKey(event.keyCode);
196    }
197   
198    if (eventType === "onscroll") {
199        if (window.pageYOffset) {
200            eventObj.setScrollPosition(window.pageYOffset);
201        }
202    }
203
204    recordedEvents.push(eventObj);
205
206    if ((recordedEvents.length >= packageSize) || (eventType === "onunload")) {
207        setTimeout(sendRequest(), 100);
208    }
209
210}
211
212/**
213 * sends the collected data to the server, named in the destination-variable
214 */
215function sendRequest() {
216    var eventList;
217    var message;
218    var clientId;
219    var i = 0;
220    var request;
221   
222    if (recordedEvents.length > 0) {
223        eventList = recordedEvents;
224        recordedEvents = [];
225       
226        message = "{\"message\":{\"clientInfos\":{";
227       
228        log("reading client id");
229        clientId = readClientId();
230        if ((clientId) && (clientId !== "")) {
231            message += "\"clientId\":\"" + clientId + "\",";
232        }
233       
234        log("adding other infos");
235        message += "\"userAgent\":\"" + navigator.userAgent + "\",";
236        message += "\"title\":\"" + document.title + "\",";
237        message += "\"url\":\"" + document.URL + "\"},";
238       
239       
240        message += "\"events\":[";
241       
242        for (i = 0; i < eventList.length; i++) {
243            if (i > 0) {
244                message += ",";
245            }
246            message += eventList[i].toJSON();
247        }
248       
249        message += "]}}";
250       
251        request = null;
252       
253        // Mozilla
254        if (window.XMLHttpRequest) {
255            request = new XMLHttpRequest();
256        }
257        // IE
258        else if (window.ActiveXObject) {
259            request = new ActiveXObject("Microsoft.XMLHTTP");
260        }
261       
262        request.open("POST", destination, true);
263
264        log("sending " + message);
265        request.send(message);
266    }
267}
268
269/**
270 * determines the coordinates of an onclick or ondblclick event
271 */
272function getEventCoordinates(event) {
273    if (event.layerX) {
274        return [event.layerX, event.layerY];
275    }
276    else if (event.offsetX) {
277        return [event.offsetX, event.offsetY];
278    }
279
280    var obj = event.target || event.srcElement;
281    var objOffset = getPageOffset(obj);
282
283    return [(event.clientX - objOffset[0]), (event.clientY - objOffset[1])];
284}
285
286/**
287 * determines the page offset of an object using the parent objects offset
288 */
289function getPageOffset(object) {
290    var top = 0;
291    var left = 0;
292    var obj = object;
293
294    while (obj.offsetParent) {
295        left += obj.offsetLeft;
296        top += obj.offsetTop;
297        obj = obj.offsetParent;
298    }
299
300    return [left, top];
301}
302
303/**
304 *
305 */
306function readClientId() {
307    var cookie = document.cookie;
308   
309    var expectedCookieName = getClientIdCookieName();
310   
311    var cookiename = null;
312    var startIndex = 0;
313   
314    var clientId = null;
315
316    do {
317        cookie = cookie.substring(startIndex, cookie.length);
318        cookiename = cookie.substr(0, cookie.search('='));
319        startIndex = cookie.search(';') + 1;
320       
321        while (cookie.charAt(startIndex) === ' ') {
322            startIndex++;
323        }
324    }
325    while ((startIndex > 0) && (cookiename !== expectedCookieName));
326   
327    if (cookiename === expectedCookieName) {
328        clientId = cookie.substr(cookie.search('=') + 1, cookie.search(';'));
329        if (clientId === "") {
330            clientId = cookie.substr(cookie.search('=') + 1, cookie.length);
331        }
332    }
333   
334    if ((!clientId) || (clientId === "") || (clientId.search(getClientIdPrefix()) !== 0)) {
335        clientId = generateClientId();
336        storeClientId(clientId);
337    }
338   
339    return clientId;
340}
341
342
343/**
344 *
345 */
346function storeClientId(clientId) {
347    if ((clientId) && (clientId !== "")) {
348        var expiry = new Date();
349        // 10 years should be sufficient :-)
350        expiry = new Date(expiry.getTime() + 1000*60*60*24*365*10);
351        document.cookie = getClientIdCookieName() + '=' + clientId +
352            '; expires=' + expiry.toGMTString()+';';
353    }
354}
355
356/**
357 *
358 */
359function getClientIdCookieName() {
360    return document.URL + "/quest-htmlmonitor/quest-client-id";
361}
362
363/**
364 *
365 */
366function generateClientId() {
367    return getClientIdPrefix() + new Date().getTime();
368}
369
370/**
371 *
372 */
373function getClientIdPrefix() {
374    // create something like a more or less unique checksum. It is sufficient, if it differs
375    // only often, but not always, because it is concatenated with a time stamp, which differs
376    // much more often.
377    var prefixStr = navigator.userAgent + "_" + navigator.platform;
378    var prefixId = prefixStr.length;
379    var i = 0;
380   
381    for (i = 0; i < prefixStr.length; i++) {
382        prefixId += prefixStr.charCodeAt(i);
383    }
384   
385    // ensure, that a string is created and not a long. Otherwise, it can not be checked, if an
386    // existing client id starts with the client id prefix and can therefore be reused.
387    return prefixId.toString();
388}
389
390/**
391 * performs a simple logging
392 */
393function log(text) {
394    if (doLog) {
395        var loggingInfo = document.getElementById("loggingInfoParagraph");
396       
397        if (!loggingInfo) {
398            var div = document.createElement("div");
399            div.setAttribute("style", "z-index:1000000; background-color:#afa; " +
400                             "border:1px black solid; position:absolute; " +
401                             "top:10px; right:10px; width:350px; height:auto");
402           
403            loggingInfo = document.createElement("div");
404            loggingInfo.id = "loggingInfoParagraph";
405            div.appendChild(loggingInfo);
406           
407            var body = document.getElementsByTagName("body")[0];
408            if (!body) {
409                alert("could not enable logging");
410            }
411            else {
412                body.appendChild(div);
413            }
414        }
415       
416        loggingInfo.appendChild(document.createTextNode(text));
417        loggingInfo.appendChild(document.createElement("br"));
418    }
419}
420
421function Event(type, nodePath) {
422    this.type = type;
423    this.nodePath = nodePath;
424    this.time = new Date().getTime();
425   
426    this.setClickCoordinates = function(coordinates) {
427          this.clickCoordinates = coordinates;
428    };
429   
430    this.setKey = function(key) {
431          this.key = key;
432    };
433   
434    this.setScrollPosition = function(scrollPosition) {
435          this.scrollPosition = scrollPosition;
436    };
437   
438    this.toJSON = function() {
439        var eventInJSON =
440            "{\"path\":\"" + this.nodePath + "\",\"time\":" + this.time + ",\"eventType\":\"" +
441            this.type + "\"";
442
443        if ((this.clickCoordinates) && (!isNaN(this.clickCoordinates[0]))) {
444            eventInJSON += ",\"coordinates\":[" + this.clickCoordinates + "]";
445        }
446
447        if (this.key) {
448            eventInJSON += ",\"key\":" + this.key;
449        }
450       
451        if (this.scrollPosition) {
452            eventInJSON += ",\"scrollPosition\":" + this.scrollPosition;
453        }
454
455        eventInJSON += "}";
456       
457        return eventInJSON;
458    };
459}
460
Note: See TracBrowser for help on using the repository browser.