diff --git a/js/piwik.js b/js/piwik.js
index 8f9fb8bebd1b8df987985d2bd1b928bc032f78d7..b01a61dc6c84b59b432584aecf775a27cade32bb 100644
--- a/js/piwik.js
+++ b/js/piwik.js
@@ -29,7 +29,7 @@
  * @version 2012-10-08
  * @link http://www.JSON.org/js.html
  ************************************************************/
-/*jslint evil: true, regexp: false, bitwise: true*/
+/*jslint evil: true, regexp: false, bitwise: true, white: true */
 /*global JSON2:true */
 /*members "", "\b", "\t", "\n", "\f", "\r", "\"", "\\", apply,
     call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
@@ -451,7 +451,8 @@ if (typeof JSON2 !== 'object') {
     getTrackedContentImpressions, getCurrentlyVisibleContentImpressionsRequestsIfNotTrackedYet,
     contentInteractionTrackingSetupDone, contains, match, pathname, piece, trackContentInteractionNode,
     trackContentInteractionNode, trackContentImpressionsWithinNode, trackContentImpression,
-    enableTrackOnlyVisibleContent, trackContentInteraction
+    enableTrackOnlyVisibleContent, trackContentInteraction, clearEnableTrackOnlyVisibleContent,
+    trackVisibleContentImpressions
  */
 /*global _paq:true */
 /*members push */
@@ -1131,7 +1132,8 @@ if (typeof Piwik !== 'object') {
          ************************************************************/
 
         var query = {
-            htmlCollectionToArray: function (foundNodes) {
+            htmlCollectionToArray: function (foundNodes)
+            {
                 var nodes = [], index;
 
                 if (!foundNodes || !foundNodes.length) {
@@ -1174,7 +1176,8 @@ if (typeof Piwik !== 'object') {
 
                 return nodes;
             },
-            findNodesByTagName: function (node, tagName) {
+            findNodesByTagName: function (node, tagName)
+            {
                 if (!node || !tagName || !node.getElementsByTagName) {
                     return [];
                 }
@@ -1218,7 +1221,8 @@ if (typeof Piwik !== 'object') {
 
                 return nodes;
             },
-            getAttributeValueFromNode: function (node, attributeName) {
+            getAttributeValueFromNode: function (node, attributeName)
+            {
                 if (!this.hasNodeAttribute(node, attributeName)) {
                     return;
                 }
@@ -1259,13 +1263,14 @@ if (typeof Piwik !== 'object') {
 
                 return null;
             },
-            hasNodeAttributeWithValue: function (node, attributeName) {
-
+            hasNodeAttributeWithValue: function (node, attributeName)
+            {
                 var value = this.getAttributeValueFromNode(node, attributeName);
 
                 return !!value;
             },
-            hasNodeAttribute: function (node, attributeName) {
+            hasNodeAttribute: function (node, attributeName)
+            {
                 if (node && node.hasAttribute) {
                     return node.hasAttribute(attributeName);
                 }
@@ -1277,7 +1282,8 @@ if (typeof Piwik !== 'object') {
 
                 return false;
             },
-            hasNodeCssClass: function (node, className) {
+            hasNodeCssClass: function (node, className)
+            {
                 if (node && className && node.className) {
                     var classes = node.className.split(' ');
                     if (-1 !== classes.indexOf(className)) {
@@ -1287,7 +1293,8 @@ if (typeof Piwik !== 'object') {
 
                 return false;
             },
-            findNodesHavingAttribute: function (nodeToSearch, attributeName, nodes) {
+            findNodesHavingAttribute: function (nodeToSearch, attributeName, nodes)
+            {
                 if (!nodes) {
                     nodes = [];
                 }
@@ -1308,7 +1315,8 @@ if (typeof Piwik !== 'object') {
 
                 return nodes;
             },
-            findFirstNodeHavingAttribute: function (node, attributeName) {
+            findFirstNodeHavingAttribute: function (node, attributeName)
+            {
                 if (!node || !attributeName) {
                     return;
                 }
@@ -1323,7 +1331,8 @@ if (typeof Piwik !== 'object') {
                     return nodes[0];
                 }
             },
-            findFirstNodeHavingAttributeWithValue: function (node, attributeName) {
+            findFirstNodeHavingAttributeWithValue: function (node, attributeName)
+            {
                 if (!node || !attributeName) {
                     return;
                 }
@@ -1345,7 +1354,8 @@ if (typeof Piwik !== 'object') {
                     }
                 }
             },
-            findNodesHavingCssClass: function (nodeToSearch, className, nodes) {
+            findNodesHavingCssClass: function (nodeToSearch, className, nodes)
+            {
                 if (!nodes) {
                     nodes = [];
                 }
@@ -1375,7 +1385,8 @@ if (typeof Piwik !== 'object') {
 
                 return nodes;
             },
-            findFirstNodeHavingClass: function (node, className) {
+            findFirstNodeHavingClass: function (node, className)
+            {
                 if (!node || !className) {
                     return;
                 }
@@ -1390,7 +1401,8 @@ if (typeof Piwik !== 'object') {
                     return nodes[0];
                 }
             },
-            isLinkElement: function (node) {
+            isLinkElement: function (node)
+            {
                 if (!node) {
                     return false;
                 }
@@ -1419,7 +1431,8 @@ if (typeof Piwik !== 'object') {
             CONTENT_IGNOREINTERACTION_CLASS: 'piwikContentIgnoreInteraction',
             location: undefined,
 
-            findContentNodes: function () {
+            findContentNodes: function ()
+            {
 
                 var cssSelector  = '.' + this.CONTENT_CLASS;
                 var attrSelector = '[' + this.CONTENT_ATTR + ']';
@@ -1427,7 +1440,8 @@ if (typeof Piwik !== 'object') {
 
                 return contentNodes;
             },
-            findContentNodesWithinNode: function (node) {
+            findContentNodesWithinNode: function (node)
+            {
                 if (!node) {
                     return [];
                 }
@@ -1454,7 +1468,8 @@ if (typeof Piwik !== 'object') {
 
                 return nodes1;
             },
-            findParentContentNode: function (anyNode) {
+            findParentContentNode: function (anyNode)
+            {
                 if (!anyNode) {
                     return;
                 }
@@ -1478,7 +1493,8 @@ if (typeof Piwik !== 'object') {
                     counter++;
                 }
             },
-            findPieceNode: function (node) {
+            findPieceNode: function (node)
+            {
                 var contentPiece;
 
                 contentPiece = query.findFirstNodeHavingAttribute(node, this.CONTENT_PIECE_ATTR);
@@ -1600,7 +1616,8 @@ if (typeof Piwik !== 'object') {
                     return this.toAbsoluteUrl(href);
                 }
             },
-            isSameDomain: function (url) {
+            isSameDomain: function (url)
+            {
                 if (!url || !url.indexOf) {
                     return false;
                 }
@@ -1616,14 +1633,18 @@ if (typeof Piwik !== 'object') {
 
                 return false;
             },
-            removeDomainIfIsInLink: function (text) {
+            removeDomainIfIsInLink: function (text)
+            {
                 // we will only remove if domain === location.origin meaning is not an outlink
+                var regexContainsProtocol = '^https?:\/\/[^\/]+';
+                var regexReplaceDomain = '^.*\/\/[^\/]+';
+
                 if (text &&
                     text.search &&
-                    -1 !== text.search(/^https?:\/\/[^\/]+/)
+                    -1 !== text.search(new RegExp(regexContainsProtocol))
                     && this.isSameDomain(text)) {
 
-                    text = text.replace(/^.*\/\/[^\/]+/, '');
+                    text = text.replace(new RegExp(regexReplaceDomain), '');
                     if (!text) {
                         text = '/';
                     }
@@ -1673,15 +1694,16 @@ if (typeof Piwik !== 'object') {
                     }
                 }
             },
-            trim: function (text) {
+            trim: function (text)
+            {
                 if (text && String(text) === text) {
                     return text.replace(/^\s+|\s+$/g, '');
                 }
 
                 return text;
             },
-            isOrWasNodeInViewport: function (node) {
-
+            isOrWasNodeInViewport: function (node)
+            {
                 if (!node || !node.getBoundingClientRect || node.nodeType !== 1) {
                     return true;
                 }
@@ -1701,8 +1723,11 @@ if (typeof Piwik !== 'object') {
                     ((rect.top   < (windowAlias.innerHeight || html.clientHeight)) || wasVisible) // rect.top < 0 we assume user has seen all the ones that are above the current viewport
                 );
             },
-            isNodeVisible: function (node) {
-                return isVisible(node) && this.isOrWasNodeInViewport(node);
+            isNodeVisible: function (node)
+            {
+                var isItVisible  = isVisible(node);
+                var isInViewport = this.isOrWasNodeInViewport(node);
+                return isItVisible && isInViewport;
             },
             buildInteractionRequestParams: function (interaction, name, piece, target)
             {
@@ -1781,13 +1806,16 @@ if (typeof Piwik !== 'object') {
 
                 return contents;
             },
-            setLocation: function (location) {
+            setLocation: function (location)
+            {
                 this.location = location;
             },
-            getLocation: function () {
+            getLocation: function ()
+            {
                 return this.location || windowAlias.location;
             },
-            toAbsoluteUrl: function (url) {
+            toAbsoluteUrl: function (url)
+            {
                 if ((!url || String(url) !== url) && url !== '') {
                     // we only handle strings
                     return url;
@@ -1828,7 +1856,8 @@ if (typeof Piwik !== 'object') {
                 }
 
                 // Eg test.jpg
-                var base = this.getLocation().origin + this.getLocation().pathname.match(/(.*\/)/)[0];
+                var regexMatchDir = '(.*\/)';
+                var base = this.getLocation().origin + this.getLocation().pathname.match(new RegExp(regexMatchDir))[0];
                 return base + url;
             },
             setHrefAttribute: function (node, url)
@@ -1843,7 +1872,8 @@ if (typeof Piwik !== 'object') {
                     node.href = url;
                 }
             },
-            shouldIgnoreInteraction: function (targetNode) {
+            shouldIgnoreInteraction: function (targetNode)
+            {
                 var hasAttr  = query.hasNodeAttribute(targetNode, this.CONTENT_IGNOREINTERACTION_ATTR);
                 var hasClass = query.hasNodeCssClass(targetNode, this.CONTENT_IGNOREINTERACTION_CLASS);
                 return hasAttr || hasClass;
@@ -2300,8 +2330,10 @@ if (typeof Piwik !== 'object') {
                     return;
                 }
 
+                // here we have to prevent 414 request uri too long error in case someone tracks like 1000
+
                 var now  = new Date();
-                var bulk = '{"requests":["?' + requests.join('",?"') + '"]}';
+                var bulk = '{"requests":["?' + requests.join('","?') + '"]}';
 
                 sendXmlHttpRequest(bulk);
 
@@ -3050,7 +3082,7 @@ if (typeof Piwik !== 'object') {
                 return content.buildInteractionRequestParams(interaction, contentBlock.name, contentBlock.piece, contentBlock.target);
             }
 
-            function wasContentImpressionAlreadyTracked(content)
+            function wasContentImpressionAlreadyTracked(contentBlock)
             {
                 if (!trackedContentImpressions || !trackedContentImpressions.length) {
                     return false;
@@ -3062,9 +3094,9 @@ if (typeof Piwik !== 'object') {
                     trackedContent = trackedContentImpressions[index];
 
                     if (trackedContent &&
-                        trackedContent.name === content.name &&
-                        trackedContent.piece === content.piece &&
-                        trackedContent.target === content.target) {
+                        trackedContent.name === contentBlock.name &&
+                        trackedContent.piece === contentBlock.piece &&
+                        trackedContent.target === contentBlock.target) {
                         return true;
                     }
                 }
@@ -3072,8 +3104,8 @@ if (typeof Piwik !== 'object') {
                 return false;
             }
 
-            function trackContentImpressionClickInteraction (targetNode) {
-
+            function trackContentImpressionClickInteraction (targetNode)
+            {
                 return function () {
 
                     if (!targetNode || content.shouldIgnoreInteraction(targetNode)) {
@@ -3140,8 +3172,8 @@ if (typeof Piwik !== 'object') {
             /*
              * Log all content pieces
              */
-            function buildContentImpressionsRequests(contents, contentNodes) {
-
+            function buildContentImpressionsRequests(contents, contentNodes)
+            {
                 if (!contents || !contents.length) {
                     return [];
                 }
@@ -3152,6 +3184,7 @@ if (typeof Piwik !== 'object') {
 
                     if (wasContentImpressionAlreadyTracked(contents[index])) {
                         contents.splice(index, 1);
+                        index--;
                     } else {
                         trackedContentImpressions.push(contents[index]);
                     }
@@ -3182,8 +3215,8 @@ if (typeof Piwik !== 'object') {
             /*
              * Log all content pieces
              */
-            function getContentImpressionsRequestsFromNodes(contentNodes) {
-
+            function getContentImpressionsRequestsFromNodes(contentNodes)
+            {
                 var contents = content.collectContent(contentNodes);
 
                 return buildContentImpressionsRequests(contents, contentNodes);
@@ -3192,8 +3225,8 @@ if (typeof Piwik !== 'object') {
             /*
              * Log currently visible content pieces
              */
-            function getCurrentlyVisibleContentImpressionsRequestsIfNotTrackedYet(contentNodes) {
-
+            function getCurrentlyVisibleContentImpressionsRequestsIfNotTrackedYet(contentNodes)
+            {
                 if (!contentNodes || !contentNodes.length) {
                     return [];
                 }
@@ -3203,6 +3236,7 @@ if (typeof Piwik !== 'object') {
                 for (index = 0; index < contentNodes.length; index++) {
                     if (!content.isNodeVisible(contentNodes[index])) {
                         contentNodes.splice(index, 1);
+                        index--;
                     }
                 }
 
@@ -3222,6 +3256,10 @@ if (typeof Piwik !== 'object') {
 
             function buildContentInteractionRequestNode(node, contentInteraction)
             {
+                if (!node) {
+                    return;
+                }
+
                 var contentNode  = content.findParentContentNode(node);
                 var contentBlock = content.buildContentBlock(contentNode);
 
@@ -3247,7 +3285,8 @@ if (typeof Piwik !== 'object') {
             /*
              * Log the event
              */
-            function logEvent(category, action, name, value, customData) {
+            function logEvent(category, action, name, value, customData)
+            {
                 // Category and Action are required parameters
                 if (String(category).length === 0 || String(action).length === 0) {
                     return false;
@@ -3352,11 +3391,10 @@ if (typeof Piwik !== 'object') {
                 callback();
             }
 
-
             function trackCallbackOnLoad(callback)
             {
                 if (documentAlias.readyState === 'complete') {
-                    setTimeout(callback, 1); // Handle async to allow delaying ready
+                    callback();
                 } else if (windowAlias.addEventListener) {
                     windowAlias.addEventListener('load', callback);
                 } else if (windowAlias.attachEvent) {
@@ -3375,7 +3413,7 @@ if (typeof Piwik !== 'object') {
                 }
 
                 if (loaded) {
-                    setTimeout(callback, 1); // Handle async to allow delaying ready
+                    callback();
                 } else if (documentAlias.addEventListener) {
                     documentAlias.addEventListener('DOMContentLoaded', callback);
                 } else if (documentAlias.attachEvent) {
@@ -3607,6 +3645,9 @@ if (typeof Piwik !== 'object') {
                 getTrackerUrl: function () {
                     return configTrackerUrl;
                 },
+                clearEnableTrackOnlyVisibleContent: function () {
+                    enableTrackOnlyVisibleContent = false;
+                },
 
 /*</DEBUG>*/
 
@@ -4319,28 +4360,6 @@ if (typeof Piwik !== 'object') {
                     }
                 },
 
-                trackContentImpressions: function () {
-                    trackCallback(function () {
-                        if (enableTrackOnlyVisibleContent) {
-                            trackCallbackOnLoad(function () {
-                                // we have to wait till CSS parsed and applied
-                                var contentNodes = content.findContentNodes();
-
-                                var requests = getCurrentlyVisibleContentImpressionsRequestsIfNotTrackedYet(contentNodes);
-                                sendBulkRequest(requests, configTrackerPause);
-                            });
-                        } else {
-                            trackCallbackOnReady(function () {
-                                // we have to wait till DOM ready
-                                var contentNodes = content.findContentNodes();
-
-                                var requests = getContentImpressionsRequestsFromNodes(contentNodes);
-                                sendBulkRequest(requests, configTrackerPause);
-                            });
-                        }
-                    });
-                },
-
                 enableTrackOnlyVisibleContent: function (checkOnSroll, timeIntervalInMs) {
                     var self      = this;
                     var didScroll = false;
@@ -4354,6 +4373,14 @@ if (typeof Piwik !== 'object') {
 
                     enableTrackOnlyVisibleContent = true;
 
+                    if (!isDefined(checkOnSroll)) {
+                        checkOnSroll = true;
+                    }
+
+                    if (!isDefined(timeIntervalInMs)) {
+                        timeIntervalInMs = 750;
+                    }
+
                     trackCallbackOnLoad(function () {
                         var events, index;
 
@@ -4400,13 +4427,47 @@ if (typeof Piwik !== 'object') {
                     });
                 },
 
+                trackContentImpressions: function () {
+                    trackCallback(function () {
+                        if (enableTrackOnlyVisibleContent) {
+                            trackCallbackOnLoad(function () {
+                                // we have to wait till CSS parsed and applied
+                                var contentNodes = content.findContentNodes();
+
+                                var requests = getCurrentlyVisibleContentImpressionsRequestsIfNotTrackedYet(contentNodes);
+                                sendBulkRequest(requests, configTrackerPause);
+                            });
+                        } else {
+                            trackCallbackOnReady(function () {
+                                // we have to wait till DOM ready
+                                var contentNodes = content.findContentNodes();
+
+                                var requests = getContentImpressionsRequestsFromNodes(contentNodes);
+                                sendBulkRequest(requests, configTrackerPause);
+                            });
+                        }
+                    });
+                },
+
+                trackVisibleContentImpressions: function (checkOnSroll, timeIntervalInMs) {
+                    this.enableTrackOnlyVisibleContent(checkOnSroll, timeIntervalInMs);
+                    this.trackContentImpressions();
+                },
+
                 // it must be a node that is set to .piwikTrackContent or [data-track-content] or one of its parents nodes
                 trackContentImpression: function (contentName, contentPiece, contentTarget) {
+                    if (!contentName) {
+                        return;
+                    }
+
+                    contentPiece = contentPiece || 'Unknown';
+
                     trackCallback(function () {
                         var request = buildContentImpressionRequest(contentName, contentPiece, contentTarget);
                         sendRequest(request, configTrackerPause);
                     });
                 },
+
                 // it must be a node that is set to .piwikTrackContent or [data-track-content] or one of its parents nodes
                 // we might remove this method again
                 trackContentImpressionsWithinNode: function (domNode) {
@@ -4433,8 +4494,14 @@ if (typeof Piwik !== 'object') {
 
                 // name and piece has to be same as previously used on an impression
                 trackContentInteraction: function (contentInteraction, contentName, contentPiece, contentTarget) {
+                    if (!contentInteraction || !contentName) {
+                        return;
+                    }
+
+                    contentPiece = contentPiece || 'Unknown';
+
                     trackCallback(function () {
-                        var request = buildContentInteractionRequest(contentName, contentPiece, contentInteraction, contentTarget);
+                        var request = buildContentInteractionRequest(contentInteraction, contentName, contentPiece, contentTarget);
                         sendRequest(request, configTrackerPause);
                     });
                 },
diff --git a/misc/internal-docs/content-tracking.md b/misc/internal-docs/content-tracking.md
index bcaa6ddb0e4ba5423d966448934460fbade396c8..19c950af5a9d1cf7e2d047a29bd63369be98d653 100644
--- a/misc/internal-docs/content-tracking.md
+++ b/misc/internal-docs/content-tracking.md
@@ -300,7 +300,7 @@ A typical example for a content block that displays a text ad.
 ## Tracking the content programmatically
 
 
-There are several ways to track a content impression and/or interaction manually, semi-automatically and automatically.
+There are several ways to track a content impression and/or interaction manually, semi-automatically and automatically. Please be aware that content impressions will be tracked using bulk tracking which will always send a `POST` request, even if `GET` is configured which is the default.
 
 #### `trackContentImpressions()`
 
@@ -330,13 +330,21 @@ Please note: In case you have enabled to only track visible content blocks we wi
 If you enable to track only visible content we will only track an impression if a content block is actually visible. With visible we mean the content block has been in the view port, it is actually in the DOM and is not hidden via CSS (opacity, visibility, display, ...).
 
 * Optionally you can tell us to rescan the DOM automatically after each scroll event by passing `checkOnSroll=true`. We will then check whether the previously hidden content blocks are visible now and if so track the impression.
+  * Parameter defaults to boolean `true` if not specified.
+  * As the scroll event is triggered after each pixel scrolling would be very slow when checking for new visible content blocks each time the event is triggered. Instead we are checking every 100ms whether a scroll event was triggered and if so we scan the DOM for new visible content blocks
+  * Note: If a content block is placed within a scrollable element (`overflow: scroll`), we do currently not attach an event in case the user scrolls within this element. This means we would not detect that such an element becomes visible.
 * Optionally you can tell us to rescan the entire DOM for new impressions every X milliseconds by passing `timeIntervalInMs=500` (rescan DOM every 500ms).
-  * Note: Rescanning the entire DOM and detecting the visible state of content blocks can take a while depending on the browser and amount of content
-  * Note: We do not really rescan every X milliseconds. We will schedule the next rescan after a previous scan has finished. So if it takes 20ms to scan the DOM and you tell us to rescan every 50ms it can actually take 70ms.
-  * Note: In case your frames per second goes down you might want to increase this value
+  * If parameter is not set, a default interval sof 750ms will be used.
+  * Rescanning the entire DOM and detecting the visible state of content blocks can take a while depending on the browser and amount of content
+  * We do not really rescan every X milliseconds. We will schedule the next rescan after a previous scan has finished. So if it takes 20ms to scan the DOM and you tell us to rescan every 50ms it can actually take 70ms.
+  * In case your frames per second goes down you might want to increase this value
 * If you do not want us to perform any checks you can either call `trackContentImpressions()` manually at any time to rescan the entire DOM or `trackContentImpressionsWithinNode()` to check only a specific part of the DOM for visible content blocks.
 
-It is recommended to call this method once before any call to `trackContentImpressions` or `trackContentImpressionsWithinNode()`
+It is recommended to call this method once before any call to `trackContentImpressions` or `trackContentImpressionsWithinNode()`. If you call this method more than once it does not have any effect.
+
+#### `trackVisibleContentImpressions(checkOnSroll, timeIntervalInMs)`
+
+Is a shorthand for calling `enableTrackOnlyVisibleContent()` and `trackContentImpressions()`.
 
 #### trackContentInteractionNode(domNode, contentInteraction)
 
diff --git a/tests/javascript/index.php b/tests/javascript/index.php
index 0e65c112a3e3e2798a7c43666e0569eb8080c0c7..6d9a5e55ee14c8b0b35ce6a7e2adb7ed735c8c93 100644
--- a/tests/javascript/index.php
+++ b/tests/javascript/index.php
@@ -15,6 +15,9 @@ if(file_exists("stub.tpl")) {
 function getToken() {
     return "<?php $token = md5(uniqid(mt_rand(), true)); echo $token; ?>";
 }
+function getContentToken() {
+    return "<?php $token = md5(uniqid(mt_rand(), true)); echo $token; ?>";
+}
 <?php
 $sqlite = false;
 if (file_exists("enable_sqlite")) {
@@ -73,6 +76,15 @@ function _e(id){
         return document.all[id];
 }
 
+function _s(selector) { // select node within content test scope
+ $nodes = $('#contenttest ' + selector);
+ if ($nodes.length) {
+     return $nodes[0];
+ } else {
+     ok(false, 'selector not found but should: #contenttest ' + selector);
+ }
+}
+
 function loadJash() {
     var jashDiv = _e('jashDiv');
 
@@ -80,6 +92,11 @@ function loadJash() {
     document.body.appendChild(document.createElement('script')).src='jash/Jash.js';
 }
 
+ function scrollToTop()
+ {
+     window.scroll(0, 0);
+ }
+
 function triggerEvent(element, type) {
  if ( document.createEvent ) {
      event = document.createEvent( "MouseEvents" );
@@ -91,7 +108,29 @@ function triggerEvent(element, type) {
  }
 }
 
-function dropCookie(cookieName, path, domain) {
+ function wait(msecs)
+ {
+     var start = new Date().getTime();
+     var cur = start
+     while(cur - start < msecs)
+     {
+         cur = new Date().getTime();
+     }
+ }
+
+ function fetchTrackedRequests(token)
+ {
+     var xhr = window.XMLHttpRequest ? new window.XMLHttpRequest() :
+         window.ActiveXObject ? new ActiveXObject("Microsoft.XMLHTTP") :
+             null;
+
+     xhr.open("GET", "piwik.php?requests=" + token, false);
+     xhr.send(null);
+
+     return xhr.responseText;
+ }
+
+ function dropCookie(cookieName, path, domain) {
     var expiryDate = new Date();
 
     expiryDate.setTime(expiryDate.getTime() - 3600);
@@ -170,6 +209,11 @@ function deleteCookies() {
 
 var contentTestHtml = {};
 
+ function removeContentTrackingFixture()
+ {
+     $('#contenttest').remove();
+ }
+
 function setupContentTrackingFixture(name, targetNode) {
     var url = 'content-fixtures/' + name + '.html'
 
@@ -184,7 +228,7 @@ function setupContentTrackingFixture(name, targetNode) {
 
     var newNode = $('<div id="contenttest">' + contentTestHtml[name] + '</div>');
 
-    $('#contenttest').remove();
+    removeContentTrackingFixture();
 
     if (targetNode) {
         $(targetNode).prepend(newNode);
@@ -673,10 +717,6 @@ function PiwikTest() {
     });
 
     test("contentFindContentBlock", function() {
-        function _s(selector) { // select node within content test scope
-            $nodes = $('#contenttest ' + selector);
-            if ($nodes.length) return $nodes[0];
-        }
 
         var tracker = Piwik.getTracker();
         var content = tracker.getContent();
@@ -726,7 +766,11 @@ function PiwikTest() {
     test("contentFindContentNodes", function() {
         function ex(testNumber) { // select node within content test scope
             $nodes = $('#contenttest #ex' + testNumber);
-            if ($nodes.length) return $nodes[0];
+            if ($nodes.length) {
+                return $nodes[0];
+            } else {
+                ok(false, 'selector was not found but should be "#contenttest #ex' + selector + '"')
+            }
         }
 
         var tracker = Piwik.getTracker();
@@ -1030,21 +1074,13 @@ function PiwikTest() {
         var content = tracker.getContent();
         var actual;
 
-        function _s(selector) { // select node within content test scope
-            $nodes = $('#contenttest ' + selector);
-            if ($nodes.length) {
-                return $nodes[0];
-            } else {
-                ok(false, 'selector was not found but should be "#contenttest ' + selector + '"')
-            }
-        }
         function _ex(testnumber) { // select node within content test scope
             return _s('#ex' + testnumber);
         }
 
         function assertContentNodeVisible(node, message)
         {
-            window.scroll(0,0); // make sure content nodes are actually in view port
+            scrollToTop(); // make sure content nodes are actually in view port
 
             if (!message) {
                 message = '';
@@ -1054,7 +1090,7 @@ function PiwikTest() {
 
         function assertContentNodeNotVisible(node, message)
         {
-            window.scroll(0,0); // make sure content nodes are actually in view port
+            scrollToTop(); // make sure content nodes are actually in view port
 
             if (!message) {
                 message = '';
@@ -1064,7 +1100,7 @@ function PiwikTest() {
 
         function assertInternalNodeVisible(node, message)
         {
-            window.scroll(0,0); // make sure content nodes are actually in view port
+            scrollToTop(); // make sure content nodes are actually in view port
 
             if (!message) {
                 message = '';
@@ -1074,7 +1110,7 @@ function PiwikTest() {
 
         function assertInternalNodeNotVisible(node, message)
         {
-            window.scroll(0,0); // make sure content nodes are actually in view port
+            scrollToTop(); // make sure content nodes are actually in view port
 
             if (!message) {
                 message = '';
@@ -1084,7 +1120,7 @@ function PiwikTest() {
 
         function assertNodeNotInViewport(node, message)
         {
-            window.scroll(0,0); // make sure content nodes are actually in view port
+            scrollToTop(); // make sure content nodes are actually in view port
 
             if (!message) {
                 message = '';
@@ -1094,7 +1130,7 @@ function PiwikTest() {
 
         function assertNodeIsInViewport(node, message)
         {
-            window.scroll(0,0); // make sure content nodes are actually in view port
+            scrollToTop(); // make sure content nodes are actually in view port
 
             if (!message) {
                 message = '';
@@ -1178,10 +1214,6 @@ function PiwikTest() {
     });
 
     test("contentFindContentValues", function() {
-        function _s(selector) { // make sure to select node within content test scope
-            $nodes = $('#contenttest '  + selector);
-            if ($nodes.length) return $nodes[0];
-        }
 
         function _st(id) {
             return id && (''+id) === id ? _s('#' + id) : id;
@@ -1323,15 +1355,6 @@ function PiwikTest() {
         var origin = location.origin;
         var originEncoded = window.encodeURIComponent(origin);
 
-        function _s(selector) { // make sure to select node within content test scope
-            $nodes = $('#contenttest '  + selector);
-            if ($nodes.length) {
-                return $nodes[0];
-            } else {
-                ok(false, 'selector #contenttest '  + selector + ' is not found but should');
-            }
-        }
-
         function assertTrackingRequest(actual, expectedStartsWith, message)
         {
             if (!message) {
@@ -1549,7 +1572,7 @@ function PiwikTest() {
         var impression2 = {name: 'name2', piece: 'piece2', target: 'http://www.example.com'};
         var impression3 = {name: 'name3', piece: 'piece3', target: 'Anything'};
 
-        actual = tracker.buildContentImpressionsRequests([impression, impression2, impression, impression3], [_s('#ex101')]);
+        actual = tracker.buildContentImpressionsRequests([impression, impression, impression2, impression, impression3], [_s('#ex101')]);
         strictEqual(actual.length, 3, 'buildContentImpressionsRequests, should be only 3 requests as one impression was there twice and should be ignored once');
         assertTrackingRequest(actual[0], 'c_n=name&c_p=5&c_t=target');
         assertTrackingRequest(actual[1], 'c_n=name2&c_p=piece2&c_t=http%3A%2F%2Fwww.example.com');
@@ -1587,16 +1610,23 @@ function PiwikTest() {
         propEqual(actual, [], 'getVisibleImpressions, no nodes set');
 
         _s('#ex115').scrollIntoView(true);
+        tracker.clearTrackedContentImpressions();
         actual = tracker.getCurrentlyVisibleContentImpressionsRequestsIfNotTrackedYet([_s('#ex116_hidden')]);
         propEqual(actual, [], 'getVisibleImpressions, if all are hidden should not return anything');
 
         _s('#ex115').scrollIntoView(true);
+        tracker.clearTrackedContentImpressions();
         actual = tracker.getCurrentlyVisibleContentImpressionsRequestsIfNotTrackedYet([_s('#ex115'),_s('#ex115'),  _s('#ex116_hidden')]);
         strictEqual(actual.length, 1, 'getVisibleImpressions, should not ignore the found requests but the visible ones, should not add the same one twice');
         assertTrackingRequest(actual[0], 'c_n=img115.jpg&c_p=img115.jpg&c_t=http%3A%2F%2Fwww.example.com');
 
-        $('#contenttest').remove();
+        _s('#ex115').scrollIntoView(true);
+        tracker.clearTrackedContentImpressions();
+        actual = tracker.getCurrentlyVisibleContentImpressionsRequestsIfNotTrackedYet([_s('#ex116_hidden'), _s('#ex116_hidden'), _s('#ex115'),_s('#ex115')]);
+        strictEqual(actual.length, 1, 'getVisibleImpressions, two hidden ones before a visible ones to make sure removing hidden content block from array works and does not ignore one');
+        assertTrackingRequest(actual[0], 'c_n=img115.jpg&c_p=img115.jpg&c_t=http%3A%2F%2Fwww.example.com');
 
+        removeContentTrackingFixture();
     });
 
     test("Basic requirements", function() {
@@ -1622,7 +1652,7 @@ function PiwikTest() {
     });
 
     test("API methods", function() {
-        expect(57);
+        expect(64);
 
         equal( typeof Piwik.addPlugin, 'function', 'addPlugin' );
         equal( typeof Piwik.getTracker, 'function', 'getTracker' );
@@ -1684,6 +1714,14 @@ function PiwikTest() {
         equal( typeof tracker.trackGoal, 'function', 'trackGoal' );
         equal( typeof tracker.trackLink, 'function', 'trackLink' );
         equal( typeof tracker.trackPageView, 'function', 'trackPageView' );
+        // content
+        equal( typeof tracker.enableTrackOnlyVisibleContent, 'function', 'enableTrackOnlyVisibleContent' );
+        equal( typeof tracker.trackContentImpressions, 'function', 'trackContentImpressions' );
+        equal( typeof tracker.trackVisibleContentImpressions, 'function', 'trackVisibleContentImpressions' );
+        equal( typeof tracker.trackContentImpression, 'function', 'trackContentImpression' );
+        equal( typeof tracker.trackContentImpressionsWithinNode, 'function', 'trackContentImpressionsWithinNode' );
+        equal( typeof tracker.trackContentInteraction, 'function', 'trackContentInteraction' );
+        equal( typeof tracker.trackContentInteractionNode, 'function', 'trackContentInteractionNode' );
         // ecommerce
         equal( typeof tracker.setEcommerceView, 'function', 'setEcommerceView' );
         equal( typeof tracker.addEcommerceItem, 'function', 'addEcommerceItem' );
@@ -2270,16 +2308,6 @@ if ($sqlite) {
         tracker.setTrackerUrl("piwik.php");
         tracker.setSiteId(1);
 
-        function wait(msecs)
-        {
-            var start = new Date().getTime();
-            var cur = start
-            while(cur - start < msecs)
-            {
-                cur = new Date().getTime();
-            }
-        }
-
         var visitorIdStart = tracker.getVisitorId();
         // need to wait at least 1 second so that the cookie would be different, if it wasnt persisted
         wait(2000);
@@ -2620,6 +2648,240 @@ if ($sqlite) {
             start();
         }, 5000);
     });
+
+    test("trackingContent", function() {
+        expect(65);
+
+        function assertTrackingRequest(actual, expectedStartsWith, message)
+        {
+            if (!message) {
+                message = '';
+            } else {
+                message += ', ';
+            }
+
+            expectedStartsWith = '<span>/tests/javascript/piwik.php?' + expectedStartsWith;
+
+            strictEqual(actual.indexOf(expectedStartsWith), 0, message +  actual + ' should start with ' + expectedStartsWith);
+            strictEqual(actual.indexOf('&idsite=1&rec=1'), expectedStartsWith.length);
+        }
+
+        var token = getContentToken();
+
+        var tracker = Piwik.getTracker();
+        tracker.setTrackerUrl("piwik.php");
+        tracker.setSiteId(1);
+        tracker.setCustomData({ "token" : token });
+        tracker.clearEnableTrackOnlyVisibleContent();
+        tracker.clearTrackedContentImpressions();
+
+        var visitorIdStart = tracker.getVisitorId();
+        // need to wait at least 1 second so that the cookie would be different, if it wasnt persisted
+        wait(2000);
+
+        var actual, expected, trackerUrl;
+
+        tracker.trackContentImpressions();
+        strictEqual(tracker.getTrackedContentImpressions().length, 0, 'getTrackedContentImpressions, there is no content block to track');
+        tracker.trackContentImpressionsWithinNode(_e('other'));
+        strictEqual(tracker.getTrackedContentImpressions().length, 0, 'getTrackedContentImpressionsWithinNode, there is no content block to track');
+        tracker.trackContentInteractionNode();
+        strictEqual(tracker.getTrackedContentImpressions().length, 0, 'trackContentInteractionNode, no node given should not track anything');
+
+        setupContentTrackingFixture('trackingContent', document.body);
+
+        tracker.trackContentImpressions();
+        strictEqual(tracker.getTrackedContentImpressions().length, 7, 'should mark 7 content blocks as tracked');
+
+        wait(500);
+
+        var token2 = '2' + token;
+        tracker.clearTrackedContentImpressions();
+        tracker.setCustomData('token', token2);
+        tracker.trackContentImpressionsWithinNode(_s('#block1'));
+        strictEqual(tracker.getTrackedContentImpressions().length, 3, 'should mark 3 content blocks as tracked');
+
+        tracker.clearTrackedContentImpressions();
+        tracker.trackContentImpressionsWithinNode(_e('click1'));
+        strictEqual(tracker.getTrackedContentImpressions().length, 0, 'should not track anything as does not contain content block');
+
+        wait(500);
+
+        var token3 = '3' + token;
+        tracker.clearTrackedContentImpressions();
+        tracker.setCustomData('token', token3);
+        tracker.trackContentImpression(); // should not track anything as name is required
+        tracker.trackContentImpression('MyName'); // piece should default to Unknown
+        wait(500);
+        tracker.trackContentImpression('Any://Name', 'AnyPiece?', 'http://www.example.com');
+        strictEqual(tracker.getTrackedContentImpressions().length, 0, 'manual impression call should not be marked as already tracked');
+
+        wait(500);
+
+        var token4 = '4' + token;
+        tracker.setCustomData('token', token4);
+        tracker.trackContentInteraction(); // should not track anything as interaction and name is required
+        tracker.trackContentInteraction('Clicki'); // should not track anything as interaction and name is required
+        tracker.trackContentInteraction('Clicke', 'IntName'); // should use default for piece and ignore target as it is not set
+        wait(500);
+        tracker.trackContentInteraction('Clicki', 'IntN:/ame', 'IntPiece?', 'http://int.example.com');
+
+        wait(500);
+
+        var token5 = '5' + token;
+        tracker.clearTrackedContentImpressions();
+        tracker.setCustomData('token', token5);
+        tracker.trackContentInteractionNode(_s('#ex5'), 'Clicki?iii');
+
+        wait(500);
+
+        var token6 = '6' + token;
+        tracker.clearTrackedContentImpressions();
+        tracker.enableTrackOnlyVisibleContent(false, 0);
+        tracker.setCustomData('token', token6);
+        scrollToTop();
+        tracker.trackContentImpressions();
+        strictEqual(tracker.getTrackedContentImpressions().length, 3, 'should only track visible ones this time');
+
+        var token7 = '7' + token;
+        tracker.clearTrackedContentImpressions();
+        tracker.enableTrackOnlyVisibleContent();
+        tracker.setCustomData('token', token7);
+        scrollToTop();
+        tracker.trackContentImpressionsWithinNode();
+        strictEqual(tracker.getTrackedContentImpressions().length, 0, 'should not track anything, no node provided');
+        tracker.trackContentImpressionsWithinNode(_s('#block1'));
+        strictEqual(tracker.getTrackedContentImpressions().length, 0, 'should not track any block since all not visible');
+        tracker.trackContentImpressionsWithinNode(_s('#block2'));
+        strictEqual(tracker.getTrackedContentImpressions().length, 2, 'should track the two visible ones');
+
+        wait(500);
+
+        var token8 = '8' + token;
+        tracker.clearTrackedContentImpressions();
+        tracker.clearEnableTrackOnlyVisibleContent();
+        tracker.setCustomData('token', token8);
+        scrollToTop();
+        tracker.trackVisibleContentImpressions(false, 0);
+        strictEqual(tracker.getTrackedContentImpressions().length, 3, 'should only track all visible impressions');
+
+
+        removeContentTrackingFixture();
+
+        stop();
+        setTimeout(function() {
+
+            // trackContentImpressions()
+            var results = fetchTrackedRequests(token);
+            equal( (/<span\>([0-9]+)\<\/span\>/.exec(results))[1], "7", "count trackContentImpressions requests. all content blocks should be tracked" );
+
+            var requests = results.match(/<span\>(.*?)\<\/span\>/g);
+            requests.shift();
+
+            assertTrackingRequest(requests[0], 'c_n=img1-en.jpg&c_p=img1-en.jpg');
+            assertTrackingRequest(requests[1], 'c_n=My%20Ad%205&c_p=http%3A%2F%2Fimg5.example.com%2Fpath%2Fxyz.jpg&c_t=http%3A%2F%2Fapache.piwik%2Fanylink5');
+            assertTrackingRequest(requests[2], 'c_n=http%3A%2F%2Fwww.example.com%2Fpath%2Fxyz.jpg&c_p=http%3A%2F%2Fwww.example.com%2Fpath%2Fxyz.jpg&c_t=http%3A%2F%2Fimg6.example.com');
+            assertTrackingRequest(requests[3], 'c_n=My%20Ad%207&c_p=Unknown&c_t=http%3A%2F%2Fimg7.example.com');
+            assertTrackingRequest(requests[4], 'c_n=img.jpg&c_p=img.jpg&c_t=http%3A%2F%2Fimg2.example.com');
+            assertTrackingRequest(requests[5], 'c_n=img3-en.jpg&c_p=img3-en.jpg&c_t=http%3A%2F%2Fimg3.example.com');
+            assertTrackingRequest(requests[6], 'c_n=My%20content%204&c_p=My%20content%204&c_t=http%3A%2F%2Fimg4.example.com');
+
+
+            // trackContentImpressionsWithinNode()
+            results = fetchTrackedRequests(token2);
+            equal( (/<span\>([0-9]+)\<\/span\>/.exec(results))[1], "3", "count trackContentImpressionsWithinNode requests. should track only content blocks within node" );
+
+            requests = results.match(/<span\>(.*?)\<\/span\>/g);
+            requests.shift();
+
+            assertTrackingRequest(requests[0], 'c_n=img.jpg&c_p=img.jpg&c_t=http%3A%2F%2Fimg2.example.com');
+
+            // trackContentImpression()
+            results = fetchTrackedRequests(token3);
+            equal( (/<span\>([0-9]+)\<\/span\>/.exec(results))[1], "2", "count trackContentImpression requests. " );
+
+            requests = results.match(/<span\>(.*?)\<\/span\>/g);
+            requests.shift();
+
+            var firstRequest  = 0;
+            var secondRequest = 1;
+            if (-1 === requests[0].indexOf('MyName')) {
+                firstRequest  = 1;
+                secondRequest = 0;
+            }
+
+            assertTrackingRequest(requests[firstRequest], 'c_n=MyName&c_p=Unknown');
+            assertTrackingRequest(requests[secondRequest], 'c_n=Any%3A%2F%2FName&c_p=AnyPiece%3F&c_t=http%3A%2F%2Fwww.example.com');
+
+
+            // trackContentInteraction()
+            results = fetchTrackedRequests(token4);
+            equal( (/<span\>([0-9]+)\<\/span\>/.exec(results))[1], "2", "count trackContentInteraction requests." );
+
+            requests = results.match(/<span\>(.*?)\<\/span\>/g);
+            requests.shift();
+
+            firstRequest  = 0;
+            secondRequest = 1;
+            if (-1 === requests[0].indexOf('IntName')) {
+                firstRequest  = 1;
+                secondRequest = 0;
+            }
+
+            assertTrackingRequest(requests[firstRequest], 'c_i=Clicke&c_n=IntName&c_p=Unknown');
+            assertTrackingRequest(requests[secondRequest], 'c_i=Clicki&c_n=IntN%3A%2Fame&c_p=IntPiece%3F&c_t=http%3A%2F%2Fint.example.com');
+
+
+            // trackContentInteractionNode()
+            results = fetchTrackedRequests(token5);
+            equal( (/<span\>([0-9]+)\<\/span\>/.exec(results))[1], "1", "count trackContentInteractionNode requests." );
+
+            requests = results.match(/<span\>(.*?)\<\/span\>/g);
+            requests.shift();
+
+            assertTrackingRequest(requests[0], 'c_i=Clicki%3Fiii&c_n=My%20Ad%205&c_p=http%3A%2F%2Fimg5.example.com%2Fpath%2Fxyz.jpg&c_t=http%3A%2F%2Fapache.piwik%2Fanylink5');
+
+
+            // enableTrackOnlyVisibleContent() && trackContentImpressions()
+            results = fetchTrackedRequests(token6);
+            equal( (/<span\>([0-9]+)\<\/span\>/.exec(results))[1], "3", "count enabledVisibleContentImpressions requests." );
+
+            requests = results.match(/<span\>(.*?)\<\/span\>/g);
+            requests.shift();
+
+            assertTrackingRequest(requests[0], 'c_n=img1-en.jpg&c_p=img1-en.jpg');
+            assertTrackingRequest(requests[1], 'c_n=My%20Ad%205&c_p=http%3A%2F%2Fimg5.example.com%2Fpath%2Fxyz.jpg&c_t=http%3A%2F%2Fapache.piwik%2Fanylink5');
+            assertTrackingRequest(requests[2], 'c_n=http%3A%2F%2Fwww.example.com%2Fpath%2Fxyz.jpg&c_p=http%3A%2F%2Fwww.example.com%2Fpath%2Fxyz.jpg&c_t=http%3A%2F%2Fimg6.example.com');
+
+
+            // enableTrackOnlyVisibleContent() && trackContentImpressionsWithinNode()
+            results = fetchTrackedRequests(token7);
+            equal( (/<span\>([0-9]+)\<\/span\>/.exec(results))[1], "2", "count enabledVisibleContentImpressionsWithinNode requests." );
+
+            requests = results.match(/<span\>(.*?)\<\/span\>/g);
+            requests.shift();
+
+            assertTrackingRequest(requests[0], 'c_n=My%20Ad%205&c_p=http%3A%2F%2Fimg5.example.com%2Fpath%2Fxyz.jpg&c_t=http%3A%2F%2Fapache.piwik%2Fanylink5');
+            assertTrackingRequest(requests[1], 'c_n=http%3A%2F%2Fwww.example.com%2Fpath%2Fxyz.jpg&c_p=http%3A%2F%2Fwww.example.com%2Fpath%2Fxyz.jpg&c_t=http%3A%2F%2Fimg6.example.com');
+
+
+            // trackVisibleContentImpressions()
+            results = fetchTrackedRequests(token8);
+            equal( (/<span\>([0-9]+)\<\/span\>/.exec(results))[1], "3", "count enabledVisibleContentImpressions requests." );
+
+            requests = results.match(/<span\>(.*?)\<\/span\>/g);
+            requests.shift();
+
+            assertTrackingRequest(requests[0], 'c_n=img1-en.jpg&c_p=img1-en.jpg');
+            assertTrackingRequest(requests[1], 'c_n=My%20Ad%205&c_p=http%3A%2F%2Fimg5.example.com%2Fpath%2Fxyz.jpg&c_t=http%3A%2F%2Fapache.piwik%2Fanylink5');
+            assertTrackingRequest(requests[2], 'c_n=http%3A%2F%2Fwww.example.com%2Fpath%2Fxyz.jpg&c_p=http%3A%2F%2Fwww.example.com%2Fpath%2Fxyz.jpg&c_t=http%3A%2F%2Fimg6.example.com');
+
+            start();
+        }, 5000);
+
+// enableTrackOnlyVisibleContent (checkOnSroll, timeIntervalInMs)
+    });
+
     <?php
 }
 ?>
diff --git a/tests/javascript/piwik.php b/tests/javascript/piwik.php
index 856193e330364ade9aa7e6068da5a97472923f16..e38102545e41eaf33dc8675594c00b579795f4d8 100644
--- a/tests/javascript/piwik.php
+++ b/tests/javascript/piwik.php
@@ -9,6 +9,11 @@ function sendWebBug() {
 	print(base64_decode($trans_gif_64));
 }
 
+function isPost()
+{
+    return $_SERVER['REQUEST_METHOD'] == 'POST';
+}
+
 if (!file_exists("enable_sqlite")) {
 	sendWebBug();
 	exit;
@@ -35,6 +40,22 @@ if (filesize(dirname(__FILE__).'/unittest.dbf') == 0)
 	}
 }
 
+function logRequest($sqlite, $uri, $data) {
+    $ip = $_SERVER['REMOTE_ADDR'];
+    $ts = $_SERVER['REQUEST_TIME'];
+
+//		$uri = htmlspecialchars($uri);
+
+    $referrer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '';
+    $ua = $_SERVER['HTTP_USER_AGENT'];
+
+    $token = isset($data['token']) ? $data['token'] : '';
+
+    $query = $sqlite->exec("INSERT INTO requests (token, ip, ts, uri, referer, ua) VALUES (\"$token\", \"$ip\", \"$ts\", \"$uri\", \"$referrer\", \"$ua\")");
+
+    return $query;
+}
+
 if (isset($_GET['requests'])) {
 	$token = get_magic_quotes_gpc() ? stripslashes($_GET['requests']) : $_GET['requests'];
 	$ua = $_SERVER['HTTP_USER_AGENT'];
@@ -56,25 +77,31 @@ if (isset($_GET['requests'])) {
 
 	echo "</body></html>\n";
 } else {
+
 	if (!isset($_REQUEST['data'])) {
-		header("HTTP/1.0 400 Bad Request");
+        header("HTTP/1.0 400 Bad Request");
 	} else {
-		$ip = $_SERVER['REMOTE_ADDR'];
-		$ts = $_SERVER['REQUEST_TIME'];
 
-		$uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '';
-		if($_SERVER['REQUEST_METHOD'] == 'POST') {
-			$uri .= '?' . file_get_contents('php://input');
-		}
-//		$uri = htmlspecialchars($uri);
+        $uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '';
+
+        $input    = file_get_contents("php://input");
+        $requests = @json_decode($input, true);
+        $data     = json_decode(get_magic_quotes_gpc() ? stripslashes($_REQUEST['data']) : $_REQUEST['data'], true);
+
+        if (!empty($requests) && isPost()) {
+            $query = true;
+            foreach ($requests['requests'] as $request) {
+                $query = $query && logRequest($sqlite, $uri . $request, $data);
+            }
+        } else {
 
-		$referrer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '';
-		$ua = $_SERVER['HTTP_USER_AGENT'];
+            if (isPost()) {
+                $uri .= '?' . file_get_contents('php://input');
+            }
 
-		$data = json_decode(get_magic_quotes_gpc() ? stripslashes($_REQUEST['data']) : $_REQUEST['data'], true);
-		$token = isset($data['token']) ? $data['token'] : '';
+            $query = logRequest($sqlite, $uri, $data);
+        }
 
-		$query = $sqlite->exec("INSERT INTO requests (token, ip, ts, uri, referer, ua) VALUES (\"$token\", \"$ip\", \"$ts\", \"$uri\", \"$referrer\", \"$ua\")");
 		if (!$query) {
 			header("HTTP/1.0 500 Internal Server Error");
 		} else {
diff --git a/tests/javascript/testrunner.js b/tests/javascript/testrunner.js
index 61ca183f9760a9ebcff97ed1db0b465ee9a4ae70..5b333a8a29f7683071dd5084a38e4d8c3765e435 100644
--- a/tests/javascript/testrunner.js
+++ b/tests/javascript/testrunner.js
@@ -22,10 +22,10 @@
 // IN THE SOFTWARE
 
 var fs  = require("fs");
-var url = 'http://localhost/tests/javascript';
+var url = 'http://localhost/tests/javascript/';
 
 function printError(message) {
-    fs.write("/dev/stderr", message + "\n", "w");
+   fs.write("/dev/stderr", message + "\n", "w");
 }
 
 var page = require("webpage").create();
@@ -38,7 +38,7 @@ page.onResourceReceived = function() {
     page.evaluate(function() {
         if (!window.QUnit || window.phantomAttached) return;
 
-        QUnit.config.done.push(function(obj) {
+        QUnit.done(function(obj) {
             console.log("Tests passed: " + obj.passed);
             console.log("Tests failed: " + obj.failed);
             console.log("Total tests:  " + obj.total);
@@ -47,6 +47,8 @@ page.onResourceReceived = function() {
             window.phantomResults = obj;
         });
 
+        window.phantomAttached = true;
+
         QUnit.log(function(obj) {
             if (!obj.result) {
                 var errorMessage = "Test failed in module " + obj.module + ": '" + obj.name + "' \nError: " + obj.message;
@@ -64,8 +66,6 @@ page.onResourceReceived = function() {
                 console.log(errorMessage);
             }
         });
-
-        window.phantomAttached = true;
     });
 }