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

Last change on this file since 879 was 879, checked in by pharms, 12 years ago
  • changed implementation so that java script is served by server itself and that it determines the servers location through its own URL
File size: 17.4 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 * the maximum number of recorded events to be put into one package sent to the server
31 */
32var autoquestPackageSize = 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 autoquestActionConfig = [
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 autoquestDoLog = 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 autoquestRecordedEvents = [];
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        determineDestination();
80        addEventHandlingAttributes(document.documentElement, "");
81       
82        if (document.readyState !== "complete") {
83            // if the document is not loaded yet, try to add further event handling later
84            setTimeout(initEventHandling, 200);
85        }
86    }
87    else {
88        setTimeout(initEventHandling, 200);
89    }         
90}
91
92/**
93 * traverses the DOM-structure of the HTML-site and determines the URL of this script. Based on
94 * this URL, it calculates the destination to which the traced interactions must be sent
95 */
96function determineDestination() {
97    var scriptElements = document.getElementsByTagName("script");
98    var i;
99    var index;
100   
101    for (i = 0; i < scriptElements.length; i++) {
102        if ((scriptElements[i].type === "text/javascript") && (scriptElements[i].src)) {
103            index = scriptElements[i].src.lastIndexOf("script/autoquest-htmlmonitor.js");
104            if (index > -1) {
105                autoquestDestination = scriptElements[i].src.substring(0, index - 1);
106                log("using destination " + autoquestDestination);
107            }
108        }
109    }
110}
111
112/**
113 * traverses the DOM-structure of the HTML-site and adds event handling attributes to each
114 * relevant node
115 *
116 * @param node       the node of the DOM structure that shall be adapted and whose children shall
117 *                   be traversed
118 * @param parentPath the path to the parent node of the provided node within the DOM-structure of
119 *                   the HTML-site
120 */
121function addEventHandlingAttributes(node, parentPath) {
122    var nodePath = getNodePath(node, parentPath);
123    var i;
124    var k;
125    var value;
126   
127    if (node.nodeType === Node.ELEMENT_NODE) {
128        for (i = 0; i < autoquestActionConfig.length; i++) {
129            if (node.tagName.toLowerCase() === autoquestActionConfig[i].tag.toLowerCase()) {
130                for (k = 0; k < autoquestActionConfig[i].actions.length; k++) {
131                    value = "handleEvent('" + autoquestActionConfig[i].actions[k] + "', '" +
132                        nodePath + "', event);";
133
134                    if (!node.getAttribute(autoquestActionConfig[i].actions[k])) {
135                        node.setAttribute(autoquestActionConfig[i].actions[k], value);
136                    }
137                    else {
138                        var oldValue = node.getAttribute(autoquestActionConfig[i].actions[k]);
139                        if (oldValue.indexOf(value) < 0) {
140                            node.setAttribute(autoquestActionConfig[i].actions[k],
141                                              value + ' ' + oldValue);
142                        }
143                    }
144                }
145            }
146        }
147    }
148   
149    for (i = 0; i < node.childNodes.length; i++) {
150        addEventHandlingAttributes(node.childNodes[i], nodePath);
151    }
152}
153
154/**
155 * generates a path through the DOM-structure of the HTML-site depending on a node and the path
156 * to its parent node. The result is the parent path plus a path element for the provided node.
157 * If the node has a tag name, this is the first part of the path element generated for the node.
158 * If the node has an id, it becomes the second part of the path element. If the node does not have
159 * an id, the method calculates the index of the node within all children of the same type within
160 * the parent node. This index becomes then the second part of the path element generated for the
161 * node.
162 *
163 * @param node       the node of the DOM structure for which the path shall be created
164 * @param parentPath the path to the parent node of the provided node
165 *
166 * @returns a path in the DOM-structure of the HTML-site including the parent path an a path
167 *          element for the provided node
168 */
169function getNodePath(node, parentPath) {
170    var nodePath = parentPath + "/";
171    var index = -1;
172    var i = 0;
173   
174    if (node.nodeType === Node.ELEMENT_NODE) {
175        nodePath += node.tagName.toLowerCase();
176    }
177    else {
178        nodePath += "undefined";
179    }
180   
181    if ((node.id) && (node.id !== "")) {
182        nodePath += "(id=" + node.id + ")";
183    }
184    else {
185       
186        if (node.parentNode) {
187            for (i = 0; i < node.parentNode.childNodes.length; i++) {
188                if (node.parentNode.childNodes[i].tagName === node.tagName) {
189                    index++;
190                    // if === also returns true if the nodes are not identical but only equal,
191                    // this may fail.
192                    if (node.parentNode.childNodes[i] === node) {
193                        break;
194                    }
195                }
196            }
197           
198        }
199        else {
200            index = 0;
201        }
202       
203        nodePath += "[" + index + "]";
204    }
205   
206    return nodePath;
207}
208
209/**
210 * handles an event that happened on a node. This method is called by the event handling attributes
211 * of the nodes. These attributes are generated by the
212 * {@link #addEventHandlingAttributes(node,parentPath)} function. It creates a new Event object and
213 * add it to the list of <code>autoquestRecordedEvents</code>. If this list achieves the maximum
214 * <code>autoquestPackageSize</code> the events in the list are sent to the server asynchronously
215 * through calling {@link #sendRequest()}.
216 *
217 * @param eventName the name of the event, e.g. onscroll
218 * @param nodePath  the path to the node in the HTML DOM on which the event occurred
219 * @param event     the HTML event that occured
220 */
221function handleEvent(eventName, nodePath, event) {
222    log("handling event " + eventName);
223   
224    if (!autoquestDestination) {
225        // do nothing if we have no destination to send data to
226        return;
227    }
228   
229    var eventType = eventName.toLowerCase();
230   
231    var eventObj = autoquestRecordedEvents.pop();
232   
233    // reuse previous on scroll events to prevent too many events
234    if ((eventName !== "onscroll") || (!eventObj) || (eventObj.type !== "onscroll")) {
235        if (eventObj) {
236            autoquestRecordedEvents.push(eventObj);
237        }
238        eventObj = new Event(eventType, nodePath);
239    }
240   
241    if ((eventType === "onclick") || (eventType === "ondblclick")) {
242        eventObj.setClickCoordinates(getEventCoordinates(event));
243    }
244
245    if ((eventType === "onkeypress") || (eventType === "onkeydown") || (eventType === "onkeyup")) {
246        eventObj.setKey(event.keyCode);
247    }
248   
249    if (eventType === "onscroll") {
250        if (window.pageYOffset) {
251            eventObj.setScrollPosition(window.pageYOffset);
252        }
253    }
254
255    log("storing event " + eventName);
256    autoquestRecordedEvents.push(eventObj);
257
258    if (autoquestRecordedEvents.length >= autoquestPackageSize) {
259        log("initiating sending events");
260        setTimeout(sendRequest, 100);
261    }
262    else if (eventType === "onunload") {
263        log("initiating sending events");
264        sendRequest();
265    }
266
267}
268
269/**
270 * sends the collected data to the server, named in the destination-variable. For this it generates
271 * a JSON formatted message and uses Ajax and the <code>autoquestDestination</code> to send it to
272 * the server
273 */
274function sendRequest() {
275    var eventList;
276    var message;
277    var clientId;
278    var i = 0;
279    var request;
280   
281    if (autoquestRecordedEvents.length > 0) {
282        log("creating message");
283        eventList = autoquestRecordedEvents;
284        autoquestRecordedEvents = [];
285       
286        message = "{\"message\":{\"clientInfos\":{";
287       
288        log("reading client id");
289        clientId = readClientId();
290        if ((clientId) && (clientId !== "")) {
291            message += "\"clientId\":\"" + clientId + "\",";
292        }
293       
294        log("adding other infos");
295        message += "\"userAgent\":\"" + navigator.userAgent + "\",";
296        message += "\"title\":\"" + document.title + "\",";
297        message += "\"url\":\"" + document.URL + "\"},";
298       
299       
300        message += "\"events\":[";
301       
302        for (i = 0; i < eventList.length; i++) {
303            if (i > 0) {
304                message += ",";
305            }
306            message += eventList[i].toJSON();
307        }
308       
309        message += "]}}";
310       
311        request = null;
312       
313        // Mozilla
314        if (window.XMLHttpRequest) {
315            request = new XMLHttpRequest();
316        }
317        // IE
318        else if (window.ActiveXObject) {
319            request = new ActiveXObject("Microsoft.XMLHTTP");
320        }
321       
322        log("sending message");
323        request.open("POST", autoquestDestination, true);
324
325        log("sending " + message);
326        request.send(message);
327    }
328}
329
330/**
331 * determines the coordinates of an onclick or ondblclick event. If the coordinates to not come
332 * with the provided event, they are determined based on the surrounding object
333 *
334 * @param event the event to extract the coordinates of
335 *
336 * @returns the coordinates of the event as an array with x and y coordinate
337 */
338function getEventCoordinates(event) {
339    if (event.layerX) {
340        return [event.layerX, event.layerY];
341    }
342    else if (event.offsetX) {
343        return [event.offsetX, event.offsetY];
344    }
345
346    var obj = event.target || event.srcElement;
347    var objOffset = getPageOffset(obj);
348
349    return [(event.clientX - objOffset[0]), (event.clientY - objOffset[1])];
350}
351
352/**
353 * determines the page offset of an object using the parent objects offset
354 */
355function getPageOffset(object) {
356    var top = 0;
357    var left = 0;
358    var obj = object;
359
360    while (obj.offsetParent) {
361        left += obj.offsetLeft;
362        top += obj.offsetTop;
363        obj = obj.offsetParent;
364    }
365
366    return [left, top];
367}
368
369/**
370 * reads the id of the client from the cookies.
371 *
372 * @returns the client id or null, if none is found in the cookies
373 */
374function readClientId() {
375    var cookie = document.cookie;
376   
377    var expectedCookieName = getClientIdCookieName();
378   
379    var cookiename = null;
380    var startIndex = 0;
381   
382    var clientId = null;
383
384    do {
385        cookie = cookie.substring(startIndex, cookie.length);
386        cookiename = cookie.substr(0, cookie.search('='));
387        startIndex = cookie.search(';') + 1;
388       
389        while (cookie.charAt(startIndex) === ' ') {
390            startIndex++;
391        }
392    }
393    while ((startIndex > 0) && (cookiename !== expectedCookieName));
394   
395    if (cookiename === expectedCookieName) {
396        clientId = cookie.substr(cookie.search('=') + 1, cookie.search(';'));
397        if (clientId === "") {
398            clientId = cookie.substr(cookie.search('=') + 1, cookie.length);
399        }
400    }
401   
402    if ((!clientId) || (clientId === "") || (clientId.search(getClientIdPrefix()) !== 0)) {
403        clientId = generateClientId();
404        storeClientId(clientId);
405    }
406   
407    return clientId;
408}
409
410
411/**
412 * stores the provided client id in the cookies
413 *
414 * @param clientId the client id to be stored
415 */
416function storeClientId(clientId) {
417    if ((clientId) && (clientId !== "")) {
418        var expiry = new Date();
419        // 10 years should be sufficient :-)
420        expiry = new Date(expiry.getTime() + 1000*60*60*24*365*10);
421        document.cookie = getClientIdCookieName() + '=' + clientId +
422            '; expires=' + expiry.toGMTString()+';';
423    }
424}
425
426/**
427 * returns the name of the cookie used to store the client id
428 *
429 * @returns as described
430 */
431function getClientIdCookieName() {
432    return document.URL + "/quest-htmlmonitor/quest-client-id";
433}
434
435/**
436 * generates a client id based on the result of {@link #getClientIdPrefix()} and the current time
437 * stamp
438 *
439 * @returns the client id
440 */
441function generateClientId() {
442    return getClientIdPrefix() + new Date().getTime();
443}
444
445/**
446 * generates a client id prefix based on the user agent and the navigators platform. The prefix
447 * is a simple checksum of the concatenation of both strings
448 *
449 * @returns the client id prefix
450 */
451function getClientIdPrefix() {
452    // create something like a more or less unique checksum. It is sufficient, if it differs
453    // only often, but not always, because it is concatenated with a time stamp, which differs
454    // much more often.
455    var prefixStr = navigator.userAgent + "_" + navigator.platform;
456    var prefixId = prefixStr.length;
457    var i = 0;
458   
459    for (i = 0; i < prefixStr.length; i++) {
460        prefixId += prefixStr.charCodeAt(i);
461    }
462   
463    // ensure, that a string is created and not a long. Otherwise, it can not be checked, if an
464    // existing client id starts with the client id prefix and can therefore be reused.
465    return prefixId.toString();
466}
467
468/**
469 * performs a simple logging by adding a specific div to the HTML
470 *
471 * @param text the text to be logged
472 */
473function log(text) {
474    if (autoquestDoLog) {
475        var loggingInfo = document.getElementById("loggingInfoParagraph");
476       
477        if (!loggingInfo) {
478            var div = document.createElement("div");
479            div.setAttribute("style", "z-index:1000000; background-color:#afa; " +
480                             "border:1px black solid; position:absolute; " +
481                             "top:10px; right:10px; width:350px; height:auto");
482           
483            loggingInfo = document.createElement("div");
484            loggingInfo.id = "loggingInfoParagraph";
485            div.appendChild(loggingInfo);
486           
487            var body = document.getElementsByTagName("body")[0];
488            if (!body) {
489                alert("could not enable logging");
490            }
491            else {
492                body.appendChild(div);
493            }
494        }
495       
496        loggingInfo.appendChild(document.createTextNode(text));
497        loggingInfo.appendChild(document.createElement("br"));
498    }
499}
500
501/**
502 * this class represents a single event
503 *
504 * @param type     the type of the event
505 * @param nodePath the path through the HTML DOM to the event target
506 */
507function Event(type, nodePath) {
508    this.type = type;
509    this.nodePath = nodePath;
510    this.time = new Date().getTime();
511   
512    this.setClickCoordinates = function(coordinates) {
513          this.clickCoordinates = coordinates;
514    };
515   
516    this.setKey = function(key) {
517          this.key = key;
518    };
519   
520    this.setScrollPosition = function(scrollPosition) {
521          this.scrollPosition = scrollPosition;
522    };
523   
524    this.toJSON = function() {
525        var eventInJSON =
526            "{\"path\":\"" + this.nodePath + "\",\"time\":" + this.time + ",\"eventType\":\"" +
527            this.type + "\"";
528
529        if ((this.clickCoordinates) && (!isNaN(this.clickCoordinates[0]))) {
530            eventInJSON += ",\"coordinates\":[" + this.clickCoordinates + "]";
531        }
532
533        if (this.key) {
534            eventInJSON += ",\"key\":" + this.key;
535        }
536       
537        if (this.scrollPosition) {
538            eventInJSON += ",\"scrollPosition\":" + this.scrollPosition;
539        }
540
541        eventInJSON += "}";
542       
543        return eventInJSON;
544    };
545}
546
Note: See TracBrowser for help on using the repository browser.