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

Last change on this file since 927 was 927, checked in by sherbold, 12 years ago
  • added copyright under the Apache License, Version 2.0
File size: 16.2 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    else {
197        nodePath += "undefined";
198    }
199   
200    if ((node.id) && (node.id !== "")) {
201        nodePath += "(id=" + node.id + ")";
202    }
203    else {
204       
205        if (node.parentNode) {
206            for (i = 0; i < node.parentNode.childNodes.length; i++) {
207                if (node.parentNode.childNodes[i].tagName === node.tagName) {
208                    index++;
209                    // if === also returns true if the nodes are not identical but only equal,
210                    // this may fail.
211                    if (node.parentNode.childNodes[i] === node) {
212                        break;
213                    }
214                }
215            }
216           
217        }
218        else {
219            index = 0;
220        }
221       
222        nodePath += "[" + index + "]";
223    }
224   
225    return nodePath;
226}
227
228/**
229 * handles an event that happened on a node. This method is called by the event handling attributes
230 * of the nodes. These attributes are generated by the
231 * {@link #addEventHandlingAttributes(node,parentPath)} function. It creates a new Event object and
232 * add it to the list of <code>autoquestRecordedEvents</code>. If this list achieves the maximum
233 * <code>autoquestPackageSize</code> the events in the list are sent to the server asynchronously
234 * through calling {@link #sendRequest()}.
235 *
236 * @param eventName the name of the event, e.g. onscroll
237 * @param nodePath  the path to the node in the HTML DOM on which the event occurred
238 * @param event     the HTML event that occured
239 */
240function handleEvent(eventName, nodePath, event) {
241    log("handling event " + eventName);
242   
243    if (!autoquestDestination) {
244        // do nothing if we have no destination to send data to
245        return;
246    }
247   
248    var eventType = eventName.toLowerCase();
249   
250    var eventObj = autoquestRecordedEvents.pop();
251   
252    // reuse previous on scroll events to prevent too many events
253    if ((eventName !== "onscroll") || (!eventObj) || (eventObj.type !== "onscroll")) {
254        if (eventObj) {
255            autoquestRecordedEvents.push(eventObj);
256        }
257        eventObj = new Event(eventType, nodePath);
258    }
259   
260    if ((eventType === "onclick") || (eventType === "ondblclick")) {
261        eventObj.setClickCoordinates(getEventCoordinates(event));
262    }
263
264    if ((eventType === "onkeypress") || (eventType === "onkeydown") || (eventType === "onkeyup")) {
265        eventObj.setKey(event.keyCode);
266    }
267   
268    if (eventType === "onscroll") {
269        if (window.pageYOffset) {
270            eventObj.setScrollPosition(window.pageYOffset);
271        }
272    }
273
274    log("storing event " + eventName);
275    autoquestRecordedEvents.push(eventObj);
276
277    if (autoquestRecordedEvents.length >= autoquestPackageSize) {
278        log("initiating sending events");
279        setTimeout(sendRequest, 100);
280    }
281    else if (eventType === "onunload") {
282        log("initiating sending events");
283        sendRequest();
284    }
285
286}
287
288/**
289 * sends the collected data to the server, named in the destination-variable. For this it generates
290 * a JSON formatted message and uses Ajax and the <code>autoquestDestination</code> to send it to
291 * the server
292 */
293function sendRequest() {
294    var eventList;
295    var message;
296    var clientId;
297    var i = 0;
298    var request;
299   
300    if (autoquestRecordedEvents.length > 0) {
301        log("creating message");
302        eventList = autoquestRecordedEvents;
303        autoquestRecordedEvents = [];
304       
305        message = "{\"message\":{\"clientInfos\":{";
306       
307        log("reading client id");
308        clientId = getClientId();
309        if ((clientId) && (clientId !== "")) {
310            message += "\"clientId\":\"" + clientId + "\",";
311        }
312       
313        log("adding other infos");
314        message += "\"userAgent\":\"" + navigator.userAgent + "\",";
315        message += "\"title\":\"" + document.title + "\",";
316        message += "\"url\":\"" + document.URL + "\"},";
317       
318       
319        message += "\"events\":[";
320       
321        for (i = 0; i < eventList.length; i++) {
322            if (i > 0) {
323                message += ",";
324            }
325            message += eventList[i].toJSON();
326        }
327       
328        message += "]}}";
329       
330        request = null;
331       
332        // Mozilla
333        if (window.XMLHttpRequest) {
334            request = new XMLHttpRequest();
335        }
336        // IE
337        else if (window.ActiveXObject) {
338            request = new ActiveXObject("Microsoft.XMLHTTP");
339        }
340       
341        request.open("POST", autoquestDestination, false);
342        request.setRequestHeader("Content-Type", "application/json");
343
344        log("sending " + message);
345        request.send(message);
346    }
347}
348
349/**
350 * determines the coordinates of an onclick or ondblclick event. If the coordinates to not come
351 * with the provided event, they are determined based on the surrounding object
352 *
353 * @param event the event to extract the coordinates of
354 *
355 * @returns the coordinates of the event as an array with x and y coordinate
356 */
357function getEventCoordinates(event) {
358    if (event.layerX) {
359        return [event.layerX, event.layerY];
360    }
361    else if (event.offsetX) {
362        return [event.offsetX, event.offsetY];
363    }
364
365    var obj = event.target || event.srcElement;
366    var objOffset = getPageOffset(obj);
367
368    return [(event.clientX - objOffset[0]), (event.clientY - objOffset[1])];
369}
370
371/**
372 * determines the page offset of an object using the parent objects offset
373 */
374function getPageOffset(object) {
375    var top = 0;
376    var left = 0;
377    var obj = object;
378
379    while (obj.offsetParent) {
380        left += obj.offsetLeft;
381        top += obj.offsetTop;
382        obj = obj.offsetParent;
383    }
384
385    return [left, top];
386}
387
388/**
389 * generates a client id based on several information retrieved from the environment. The client
390 * id is not always unique
391 *
392 * @returns the client id
393 */
394function getClientId() {
395    var clientIdStr;
396    var clientId;
397    var i = 0;
398   
399    if (!autoquestClientId) {
400        // create something like a more or less unique checksum.
401        clientIdStr =
402            navigator.appCodeName + navigator.appName + navigator.appVersion +
403            navigator.cookieEnabled + navigator.language + navigator.platform +
404            navigator.userAgent + navigator.javaEnabled() + window.location.protocol +
405            window.location.host + new Date().getTimezoneOffset();
406
407        clientId = clientIdStr.length;
408
409        for (i = 0; i < clientIdStr.length; i++) {
410            clientId += clientIdStr.charCodeAt(i);
411        }
412       
413        autoquestClientId = clientId;
414    }
415
416    return autoquestClientId;
417}
418
419/**
420 * performs a simple logging by adding a specific div to the HTML
421 *
422 * @param text the text to be logged
423 */
424function log(text) {
425    if (autoquestDoLog) {
426        var loggingInfo = document.getElementById("loggingInfoParagraph");
427       
428        if (!loggingInfo) {
429            var div = document.createElement("div");
430            div.setAttribute("style", "z-index:1000000; background-color:#afa; " +
431                             "border:1px black solid; position:absolute; " +
432                             "top:10px; right:10px; width:350px; height:auto");
433           
434            loggingInfo = document.createElement("div");
435            loggingInfo.id = "loggingInfoParagraph";
436            div.appendChild(loggingInfo);
437           
438            var body = document.getElementsByTagName("body")[0];
439            if (!body) {
440                alert("could not enable logging");
441            }
442            else {
443                body.appendChild(div);
444            }
445        }
446       
447        loggingInfo.appendChild(document.createTextNode(text));
448        loggingInfo.appendChild(document.createElement("br"));
449    }
450}
451
452/**
453 * this class represents a single event
454 *
455 * @param type     the type of the event
456 * @param nodePath the path through the HTML DOM to the event target
457 */
458function Event(type, nodePath) {
459    this.type = type;
460    this.nodePath = nodePath;
461    this.time = new Date().getTime();
462   
463    this.setClickCoordinates = function(coordinates) {
464          this.clickCoordinates = coordinates;
465    };
466   
467    this.setKey = function(key) {
468          this.key = key;
469    };
470   
471    this.setScrollPosition = function(scrollPosition) {
472          this.scrollPosition = scrollPosition;
473    };
474   
475    this.toJSON = function() {
476        var eventInJSON =
477            "{\"path\":\"" + this.nodePath + "\",\"time\":" + this.time + ",\"eventType\":\"" +
478            this.type + "\"";
479
480        if ((this.clickCoordinates) && (!isNaN(this.clickCoordinates[0]))) {
481            eventInJSON += ",\"coordinates\":[" + this.clickCoordinates + "]";
482        }
483
484        if (this.key) {
485            eventInJSON += ",\"key\":" + this.key;
486        }
487       
488        if (this.scrollPosition) {
489            eventInJSON += ",\"scrollPosition\":" + this.scrollPosition;
490        }
491
492        eventInJSON += "}";
493       
494        return eventInJSON;
495    };
496}
497
Note: See TracBrowser for help on using the repository browser.