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

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