From d903c1f0be860bae5e48031baa6d0512a917dcbb Mon Sep 17 00:00:00 2001
From: mattab <matthieu.aubry@gmail.com>
Date: Mon, 1 Sep 2014 15:33:07 +0200
Subject: [PATCH] Refs #3490 User ID Tracker is now working: - new user_id
 field to contain the raw user id value - new &uid= tracker api parameter -
 add userId to Live API output - added integration test

---
 core/Db/Schema/Mysql.php                      |   1 +
 core/Tracker/Request.php                      |  48 +++++-
 core/Tracker/Visit.php                        |   6 +
 core/Tracker/Visitor.php                      |   6 +
 core/Updates/2.7.0-b2.php                     |  35 +++++
 core/Version.php                              |   2 +-
 libs/PiwikTracker/PiwikTracker.php            |  36 ++++-
 plugins/Live/Visitor.php                      |   9 ++
 .../Fixtures/FewVisitsWithSetVisitorId.php    |  95 ------------
 .../FewVisitsWithSetVisitorIdAndUserId.php    | 145 ++++++++++++++++++
 tests/PHPUnit/Impl/ApiTestConfig.php          |  10 +-
 tests/PHPUnit/Impl/TestRequestResponse.php    |  23 ++-
 .../TrackingAPISetVisitorIdTest.php           |  15 +-
 ...sitorId__Live.getLastVisitsDetails_day.xml |  97 ++++++++++++
 ...PI_SetVisitorId__VisitsSummary.get_day.xml |  12 +-
 15 files changed, 422 insertions(+), 118 deletions(-)
 create mode 100644 core/Updates/2.7.0-b2.php
 delete mode 100644 tests/PHPUnit/Fixtures/FewVisitsWithSetVisitorId.php
 create mode 100644 tests/PHPUnit/Fixtures/FewVisitsWithSetVisitorIdAndUserId.php
 create mode 100644 tests/PHPUnit/Integration/expected/test_TrackingAPI_SetVisitorId__Live.getLastVisitsDetails_day.xml

diff --git a/core/Db/Schema/Mysql.php b/core/Db/Schema/Mysql.php
index 43a42f167d..1130c1b6bf 100644
--- a/core/Db/Schema/Mysql.php
+++ b/core/Db/Schema/Mysql.php
@@ -149,6 +149,7 @@ class Mysql implements SchemaInterface
 							  idvisitor BINARY(8) NOT NULL,
 							  visit_last_action_time DATETIME NOT NULL,
 							  config_id BINARY(8) NOT NULL,
+							  user_id varchar(200) NULL,
 							  location_ip VARBINARY(16) NOT NULL,
 							  PRIMARY KEY(idvisit),
 							  INDEX index_idsite_config_datetime (idsite, config_id, visit_last_action_time),
diff --git a/core/Tracker/Request.php b/core/Tracker/Request.php
index 98057f6ccc..3ddd52951b 100644
--- a/core/Tracker/Request.php
+++ b/core/Tracker/Request.php
@@ -277,6 +277,7 @@ class Request
             'cip'          => array(false, 'string'),
             'cdt'          => array(false, 'string'),
             'cid'          => array(false, 'string'),
+            'uid'          => array(false, 'string'),
 
             // Actions / pages
             'cs'           => array(false, 'string'),
@@ -436,21 +437,37 @@ class Request
     }
 
     /**
-     * Is the request for a known VisitorId, based on 1st party, 3rd party (optional) cookies or Tracking API forced Visitor ID
+     * Returns the ID from  the request in this order:
+     * return from a given User ID,
+     * or from a Tracking API forced Visitor ID,
+     * or from a Visitor ID from 3rd party (optional) cookies,
+     * or from a given Visitor Id from 1st party?
+     *
      * @throws Exception
      */
     public function getVisitorId()
     {
         $found = false;
 
+        // If User ID is set it takes precedence
+        $userId = $this->getForcedUserId();
+        if(strlen($userId) > 0) {
+            $idVisitor = md5($userId);
+            $idVisitor = $this->truncateIdAsVisitorId($idVisitor);
+            Common::printDebug("Request will be recorded for this user_id = " . $userId . " (idvisitor = $idVisitor)");
+            $found = true;
+        }
+
         // Was a Visitor ID "forced" (@see Tracking API setVisitorId()) for this request?
-        $idVisitor = $this->getForcedVisitorId();
-        if (!empty($idVisitor)) {
-            if (strlen($idVisitor) != Tracker::LENGTH_HEX_ID_STRING) {
-                throw new Exception("Visitor ID (cid) $idVisitor must be " . Tracker::LENGTH_HEX_ID_STRING . " characters long");
+        if (!$found) {
+            $idVisitor = $this->getForcedVisitorId();
+            if (!empty($idVisitor)) {
+                if (strlen($idVisitor) != Tracker::LENGTH_HEX_ID_STRING) {
+                    throw new Exception("Visitor ID (cid) $idVisitor must be " . Tracker::LENGTH_HEX_ID_STRING . " characters long");
+                }
+                Common::printDebug("Request will be recorded for this idvisitor = " . $idVisitor);
+                $found = true;
             }
-            Common::printDebug("Request will be recorded for this idvisitor = " . $idVisitor);
-            $found = true;
         }
 
         // - If set to use 3rd party cookies for Visit ID, read the cookie
@@ -467,6 +484,7 @@ class Request
                 }
             }
         }
+
         // If a third party cookie was not found, we default to the first party cookie
         if (!$found) {
             $idVisitor = Common::getRequestVar('_id', '', 'string', $this->params);
@@ -474,7 +492,7 @@ class Request
         }
 
         if ($found) {
-            $truncated = substr($idVisitor, 0, Tracker::LENGTH_HEX_ID_STRING);
+            $truncated = $this->truncateIdAsVisitorId($idVisitor);
             $binVisitorId = @Common::hex2bin($truncated);
             if (!empty($binVisitorId)) {
                 return $binVisitorId;
@@ -523,6 +541,11 @@ class Request
         return $this->forcedVisitorId;
     }
 
+    public function getForcedUserId()
+    {
+        return $this->getParam('uid');
+    }
+
     public function getPlugins()
     {
         static $pluginsInOrder = array('fla', 'java', 'dir', 'qt', 'realp', 'pdf', 'wma', 'gears', 'ag', 'cookie');
@@ -550,4 +573,13 @@ class Request
         }
         return false;
     }
+
+    /**
+     * @param $idVisitor
+     * @return string
+     */
+    private function truncateIdAsVisitorId($idVisitor)
+    {
+        return substr($idVisitor, 0, Tracker::LENGTH_HEX_ID_STRING);
+    }
 }
diff --git a/core/Tracker/Visit.php b/core/Tracker/Visit.php
index 90b58a88b0..793cfefb1c 100644
--- a/core/Tracker/Visit.php
+++ b/core/Tracker/Visit.php
@@ -497,6 +497,7 @@ class Visit implements VisitInterface
             'idvisitor'   => $this->getVisitorIdcookie($visitor),
             'config_id'   => $this->getSettingsObject()->getConfigId(),
             'location_ip' => $this->getVisitorIp(),
+            'user_id'     => $this->request->getForcedUserId(),
         );
     }
 
@@ -518,6 +519,11 @@ class Visit implements VisitInterface
             $visitor->setVisitorColumn('idvisitor', $this->visitorInfo['idvisitor']);
         }
 
+        if (strlen($this->request->getForcedUserId()) > 0) {
+            $valuesToUpdate['user_id'] = $this->request->getForcedUserId();
+            $visitor->setVisitorColumn('user_id', $valuesToUpdate['user_id']);
+        }
+
         $dimensions     = $this->getAllVisitDimensions();
         $valuesToUpdate = $this->triggerHookOnDimensions($dimensions, 'onExistingVisit', $visitor, $action, $valuesToUpdate);
 
diff --git a/core/Tracker/Visitor.php b/core/Tracker/Visitor.php
index 70fb165f9b..3cc2275853 100644
--- a/core/Tracker/Visitor.php
+++ b/core/Tracker/Visitor.php
@@ -236,8 +236,12 @@ class Visitor
         // If a &cid= was set, we force to select this visitor (or create a new one)
         $isForcedVisitorIdMustMatch = ($this->request->getForcedVisitorId() != null);
 
+        // if &iud was set, we force to select this visitor (or create new one)
+        $isForcedUserIdMustMatch = ($this->request->getForcedUserId() != null);
+
         $shouldMatchOneFieldOnly = (($isVisitorIdToLookup && $trustCookiesOnly)
             || $isForcedVisitorIdMustMatch
+            || $isForcedUserIdMustMatch
             || !$isVisitorIdToLookup);
         return $shouldMatchOneFieldOnly;
     }
@@ -250,6 +254,8 @@ class Visitor
         $fields = array(
             'idvisitor',
             'idvisit',
+            'user_id',
+
             'visit_exit_idaction_url',
             'visit_exit_idaction_name',
             'visitor_returning',
diff --git a/core/Updates/2.7.0-b2.php b/core/Updates/2.7.0-b2.php
new file mode 100644
index 0000000000..914d2ab65e
--- /dev/null
+++ b/core/Updates/2.7.0-b2.php
@@ -0,0 +1,35 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+
+namespace Piwik\Updates;
+
+use Piwik\Common;
+use Piwik\Updater;
+use Piwik\Updates;
+
+/**
+ */
+class Updates_2_7_0_b2 extends Updates
+{
+    static function getSql()
+    {
+        return array(
+            'ALTER TABLE `' . Common::prefixTable('log_visit') . '`
+			    ADD `user_id` varchar(200) NULL AFTER `config_id`
+			   ' => array(1060),
+        );
+    }
+
+    static function update()
+    {
+        // Run the SQL
+        Updater::updateDatabase(__FILE__, self::getSql());
+    }
+}
+
diff --git a/core/Version.php b/core/Version.php
index 843a22004b..b91bb28685 100644
--- a/core/Version.php
+++ b/core/Version.php
@@ -21,5 +21,5 @@ final class Version
      * The current Piwik version.
      * @var string
      */
-    const VERSION = '2.6.0-b1';
+    const VERSION = '2.7.0-b2';
 }
diff --git a/libs/PiwikTracker/PiwikTracker.php b/libs/PiwikTracker/PiwikTracker.php
index 3afc2db93a..02788cf224 100644
--- a/libs/PiwikTracker/PiwikTracker.php
+++ b/libs/PiwikTracker/PiwikTracker.php
@@ -196,6 +196,7 @@ class PiwikTracker
         $this->configReferralCookieTimeout = 15768000; // 6 months
 
         // Visitor Ids in order
+        $this->userId = false;
         $this->forcedVisitorId = false;
         $this->cookieVisitorId = false;
         $this->randomVisitorId = false;
@@ -380,6 +381,7 @@ class PiwikTracker
     public function setNewVisitorId()
     {
         $this->randomVisitorId = substr(md5(uniqid(rand(), true)), 0, self::LENGTH_VISITOR_ID);
+        $this->userId = false;
         $this->forcedVisitorId = false;
         $this->cookieVisitorId = false;
     }
@@ -975,6 +977,25 @@ class PiwikTracker
         $this->forcedVisitorId = $visitorId;
     }
 
+
+    /**
+     *
+     * @param string $userId  Any user ID string (eg. email address, ID, username). Must be non empty. Set to false to de-assign a user id previously set.
+     * @throws Exception
+     */
+    public function setUserId($userId)
+    {
+        if($userId === '') {
+            throw new Exception("User ID cannot be empty.");
+        }
+        $this->userId = $userId;
+    }
+
+    static public function getIdHashed($id)
+    {
+        return substr(md5( $id ), 0, 16);
+    }
+
     /**
      * If the user initiating the request has the Piwik first party cookie,
      * this function will try and return the ID parsed from this first party cookie (found in $_COOKIE).
@@ -989,13 +1010,21 @@ class PiwikTracker
      */
     public function getVisitorId()
     {
+        if (!empty($this->userId)) {
+            return $this->getIdHashed($this->userId);
+        }
         if (!empty($this->forcedVisitorId)) {
             return $this->forcedVisitorId;
-        } else if ($this->loadVisitorIdCookie()) {
+        }
+        if ($this->loadVisitorIdCookie()) {
             return $this->cookieVisitorId;
-        } else {
-            return $this->randomVisitorId;
         }
+        return $this->randomVisitorId;
+    }
+
+    public function getUserId()
+    {
+        return $this->userId;
     }
 
     /**
@@ -1308,6 +1337,7 @@ class PiwikTracker
 
             // Only allowed for Super User, token_auth required,
             (!empty($this->ip) ? '&cip=' . $this->ip : '') .
+            (!empty($this->userId) ? '&uid=' . $this->userId : '') .
             (!empty($this->forcedVisitorId) ? '&cid=' . $this->forcedVisitorId : '&_id=' . $this->getVisitorId()) .
             (!empty($this->forcedDatetime) ? '&cdt=' . urlencode($this->forcedDatetime) : '') .
             (!empty($this->forcedNewVisit) ? '&new_visit=1' : '') .
diff --git a/plugins/Live/Visitor.php b/plugins/Live/Visitor.php
index b03d37e650..b9153a374d 100644
--- a/plugins/Live/Visitor.php
+++ b/plugins/Live/Visitor.php
@@ -39,6 +39,7 @@ class Visitor implements VisitorInterface
             'idSite'                      => $this->getIdSite(),
             'idVisit'                     => $this->getIdVisit(),
             'visitIp'                     => $this->getIp(),
+            'userId'                      => $this->getUserId(),
             'visitorId'                   => $this->getVisitorId(),
 
             // => false are placeholders to be filled in API later
@@ -84,6 +85,14 @@ class Visitor implements VisitorInterface
         return false;
     }
 
+    function getUserId()
+    {
+        if (!is_null($this->details['user_id'])) {
+            return $this->details['user_id'];
+        }
+        return false;
+    }
+
     function getVisitServerHour()
     {
         return date('G', strtotime($this->details['visit_last_action_time']));
diff --git a/tests/PHPUnit/Fixtures/FewVisitsWithSetVisitorId.php b/tests/PHPUnit/Fixtures/FewVisitsWithSetVisitorId.php
deleted file mode 100644
index 8d895228b4..0000000000
--- a/tests/PHPUnit/Fixtures/FewVisitsWithSetVisitorId.php
+++ /dev/null
@@ -1,95 +0,0 @@
-<?php
-/**
- * Piwik - free/libre analytics platform
- *
- * @link http://piwik.org
- * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
- */
-namespace Piwik\Tests\Fixtures;
-
-use Piwik\Date;
-use Piwik\Tracker\Visit;
-use Piwik\Tests\Fixture;
-use PiwikTracker;
-use Exception;
-
-/**
- * Adds one site and tracks a couple visits using a custom visitor ID.
- */
-class FewVisitsWithSetVisitorId extends Fixture
-{
-    public $idSite = 1;
-    public $dateTime = '2010-03-06 11:22:33';
-
-    public function setUp()
-    {
-        $this->setUpWebsitesAndGoals();
-        $this->trackVisits();
-    }
-
-    public function tearDown()
-    {
-        // empty
-    }
-
-    private function setUpWebsitesAndGoals()
-    {
-        // tests run in UTC, the Tracker in UTC
-        if (!self::siteCreated($idSite = 1)) {
-            self::createWebsite($this->dateTime);
-        }
-    }
-
-    private function trackVisits()
-    {
-        $dateTime = $this->dateTime;
-        $idSite = $this->idSite;
-
-        $t = self::getTracker($idSite, $dateTime, $defaultInit = true);
-
-        // First, some basic tests
-        self::settingInvalidVisitorIdShouldThrow($t);
-
-        // We create VISITOR A
-        $t->setUrl('http://example.org/index.htm');
-        $t->setVisitorId(Visit::generateUniqueVisitorId());
-        self::checkResponse($t->doTrackPageView('incredible title!'));
-
-        // VISITOR B: few minutes later, we trigger the same tracker but with a custom visitor ID,
-        // => this will create a new visit B
-        $t->setForceVisitDateTime(Date::factory($dateTime)->addHour(0.05)->getDatetime());
-        $t->setUrl('http://example.org/index2.htm');
-        $t->setVisitorId(Visit::generateUniqueVisitorId());
-        self::checkResponse($t->doTrackPageView('incredible title!'));
-
-        // This new visit B will have 2 page views
-        $t->setForceVisitDateTime(Date::factory($dateTime)->addHour(0.1)->getDatetime());
-        $t->setUrl('http://example.org/index3.htm');
-        self::checkResponse($t->doTrackPageView('incredible title!'));
-
-        // total = 2 visitors, 3 page views
-
-    }
-
-    private static function settingInvalidVisitorIdShouldThrow(PiwikTracker $t)
-    {
-        try {
-            $t->setVisitorId('test');
-            $this->fail('should throw');
-        } catch (Exception $e) {
-            //OK
-        }
-        try {
-            $t->setVisitorId('61e8');
-            $this->fail('should throw');
-        } catch (Exception $e) {
-            //OK
-        }
-        try {
-            $t->setVisitorId('61e8cc2d51fea26dabcabcabc');
-            $this->fail('should throw');
-        } catch (Exception $e) {
-            //OK
-        }
-    }
-}
\ No newline at end of file
diff --git a/tests/PHPUnit/Fixtures/FewVisitsWithSetVisitorIdAndUserId.php b/tests/PHPUnit/Fixtures/FewVisitsWithSetVisitorIdAndUserId.php
new file mode 100644
index 0000000000..5fd4260539
--- /dev/null
+++ b/tests/PHPUnit/Fixtures/FewVisitsWithSetVisitorIdAndUserId.php
@@ -0,0 +1,145 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Tests\Fixtures;
+
+use Piwik\Date;
+use Piwik\Tracker\Visit;
+use Piwik\Tests\Fixture;
+use PiwikTracker;
+use Exception;
+
+/**
+ * Adds one site and tracks a couple visits using a custom visitor ID.
+ */
+class FewVisitsWithSetVisitorId extends Fixture
+{
+    public $idSite = 1;
+    public $dateTime = '2010-03-06 11:22:33';
+
+    public function setUp()
+    {
+        $this->setUpWebsitesAndGoals();
+        $this->trackVisits_setVisitorId();
+        $this->trackVisits_setUserId();
+    }
+
+    public function tearDown()
+    {
+        // empty
+    }
+
+    private function setUpWebsitesAndGoals()
+    {
+        // tests run in UTC, the Tracker in UTC
+        if (!self::siteCreated($idSite = 1)) {
+            self::createWebsite($this->dateTime);
+        }
+    }
+
+    private function trackVisits_setVisitorId()
+    {
+        // total = 2 visitors, 3 page views
+        $t = self::getTracker($this->idSite, $this->dateTime, $defaultInit = true);
+
+        // First, some basic tests
+        self::settingInvalidVisitorIdShouldThrow($t);
+
+        // We create VISITOR A
+        $t->setUrl('http://example.org/index.htm');
+        $t->setVisitorId('a13b7c5a62f72dea');
+        self::checkResponse($t->doTrackPageView('incredible title!'));
+
+        // VISITOR B: few minutes later, we trigger the same tracker but with a custom visitor ID,
+        // => this will create a new visit B
+        $t->setForceVisitDateTime(Date::factory($this->dateTime)->addHour(0.05)->getDatetime());
+        $t->setUrl('http://example.org/index2.htm');
+        $t->setVisitorId('f66bc315f2a01a79');
+        self::checkResponse($t->doTrackPageView('incredible title!'));
+
+        // This new visit B will have 2 page views
+        $t->setForceVisitDateTime(Date::factory($this->dateTime)->addHour(0.1)->getDatetime());
+        $t->setUrl('http://example.org/index3.htm');
+        self::checkResponse($t->doTrackPageView('incredible title!'));
+
+    }
+
+    private function trackVisits_setUserId()
+    {
+        // total = 2 visitors, 3 page views
+        $t = self::getTracker($this->idSite, $this->dateTime, $defaultInit = true);
+
+        // First, some basic tests
+        self::settingInvalidUserIdShouldThrow($t);
+
+        // A NEW VISIT
+        // Setting both Visitor ID and User ID
+        // -> User ID takes precedence
+        $t->setForceVisitDateTime(Date::factory($this->dateTime)->addHour(2)->getDatetime());
+        $t->setUrl('http://example.org/index.htm');
+
+        // Set Visitor ID first.
+        $generatedVisitorId = '6ccebef4faef4969';
+        $t->setVisitorId($generatedVisitorId);
+        $this->assertEquals($generatedVisitorId, $t->getVisitorId());
+
+        // Set User ID
+        $userId = 'email@example.com';
+        $t->setUserId($userId);
+        $this->assertEquals($userId, $t->getUserId());
+
+        // User ID takes precedence over any previously set Visitor ID
+        $hashUserId = $t->getIdHashed($userId);
+        $this->assertEquals($hashUserId, $t->getVisitorId());
+
+        // Track a pageview with this user id
+        self::checkResponse($t->doTrackPageView('incredible title!'));
+
+        // Track another pageview
+        $t->setForceVisitDateTime(Date::factory($this->dateTime)->addHour(2.1)->getDatetime());
+        self::checkResponse($t->doTrackPageView('second page'));
+
+
+        // A NEW VISIT
+        // Change User ID -> This will create a new visit
+        $t->setForceVisitDateTime(Date::factory($this->dateTime)->addHour(2.2)->getDatetime());
+        $t->setUserId('new-email@example.com');
+        self::checkResponse($t->doTrackPageView('a new user id was set -> new visit'));
+    }
+
+    private static function settingInvalidVisitorIdShouldThrow(PiwikTracker $t)
+    {
+        try {
+            $t->setVisitorId('test');
+            $this->fail('should throw');
+        } catch (Exception $e) {
+            //OK
+        }
+        try {
+            $t->setVisitorId('61e8');
+            $this->fail('should throw');
+        } catch (Exception $e) {
+            //OK
+        }
+        try {
+            $t->setVisitorId('61e8cc2d51fea26dabcabcabc');
+            $this->fail('should throw');
+        } catch (Exception $e) {
+            //OK
+        }
+    }
+
+    private static function settingInvalidUserIdShouldThrow(PiwikTracker $t)
+    {
+        try {
+            $t->setUserId('');
+            $this->fail('should throw');
+        } catch (Exception $e) {
+            //OK
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/PHPUnit/Impl/ApiTestConfig.php b/tests/PHPUnit/Impl/ApiTestConfig.php
index ecb778f536..a2dfab24d4 100644
--- a/tests/PHPUnit/Impl/ApiTestConfig.php
+++ b/tests/PHPUnit/Impl/ApiTestConfig.php
@@ -176,13 +176,21 @@ class ApiTestConfig
     public $xmlFieldsToRemove = false;
 
     /**
-     * If true, XML fields that change on each request for Live API methods are retained.
+     * If true, Date times XML fields that change on each request for Live API methods are retained.
      * Normally, they are removed before comparing the API response w/ expected.
      *
      * @param bool
      */
     public $keepLiveDates = false;
 
+    /**
+     * If true, ID visitors/User ID/other IDs that change on each request for Live API methods are retained.
+     * Normally, they are removed before comparing the API response w/ expected.
+     *
+     * @param bool
+     */
+    public $keepLiveIds = false;
+
     /**
      * Constructor. Sets class properties using an associative array mapping property names w/ values.
      *
diff --git a/tests/PHPUnit/Impl/TestRequestResponse.php b/tests/PHPUnit/Impl/TestRequestResponse.php
index 9ce1a3da76..923e53088e 100644
--- a/tests/PHPUnit/Impl/TestRequestResponse.php
+++ b/tests/PHPUnit/Impl/TestRequestResponse.php
@@ -81,6 +81,10 @@ class TestRequestResponse
 
     private function normalizeApiResponse($apiResponse)
     {
+        if ($this->shouldDeleteLiveIds()) {
+            $this->removeAllIdsFromXml($apiResponse);
+        }
+
         if ($this->shouldDeleteLiveDates()) {
             $apiResponse = $this->removeAllLiveDatesFromXml($apiResponse);
         } else if ($this->requestHasNonDeterministicDate()) {
@@ -123,6 +127,17 @@ class TestRequestResponse
         return preg_replace("/idSubtable=[0-9]+/", 'idSubtable=', $apiResponse);
     }
 
+    private function removeAllIdsFromXml($apiResponse)
+    {
+        $toRemove = array(
+            'visitorId',
+            'nextVisitorId',
+            'previousVisitorId',
+        );
+
+        return $this->removeXmlFields($apiResponse, $toRemove);
+    }
+
     private function removeAllLiveDatesFromXml($apiResponse)
     {
         $toRemove = array(
@@ -137,9 +152,6 @@ class TestRequestResponse
             'serverTimePrettyFirstAction',
             'goalTimePretty',
             'serverTimePretty',
-            'visitorId',
-            'nextVisitorId',
-            'previousVisitorId',
             'visitServerHour',
             'date',
             'prettyDate',
@@ -211,6 +223,11 @@ class TestRequestResponse
             || strpos($dateTime, 'now') !== false;
     }
 
+    private function shouldDeleteLiveIds()
+    {
+        return empty($this->params['keepLiveDates']);
+    }
+
     private function shouldDeleteLiveDates()
     {
         return empty($this->params['keepLiveDates'])
diff --git a/tests/PHPUnit/Integration/TrackingAPISetVisitorIdTest.php b/tests/PHPUnit/Integration/TrackingAPISetVisitorIdTest.php
index 86885f513d..45c6fd038b 100644
--- a/tests/PHPUnit/Integration/TrackingAPISetVisitorIdTest.php
+++ b/tests/PHPUnit/Integration/TrackingAPISetVisitorIdTest.php
@@ -51,7 +51,20 @@ class TrackingAPISetVisitorIdTest extends IntegrationTestCase
                                              'date'       => self::$fixture->dateTime,
                                              'periods'    => 'day',
                                              'testSuffix' => '',
-            ))
+            )),
+
+            array('Live.getLastVisitsDetails', array('idSite'  => self::$fixture->idSite,
+                                                     'date'    => self::$fixture->dateTime,
+                                                     'periods' => 'day',
+                                                     'keepLiveIds' => true,
+                                                     'keepLiveDates' => true,
+                                                     'otherRequestParameters' => array(
+                                                         'showColumns' => 'idVisit,visitorId,userId,lastActionDateTime,actions,actionDetails',
+                                                         'filter_sort_column' => 'idVisit',
+                                                         'filter_sort_order' => 'asc',
+                                                     )
+            )),
+
         );
     }
 }
diff --git a/tests/PHPUnit/Integration/expected/test_TrackingAPI_SetVisitorId__Live.getLastVisitsDetails_day.xml b/tests/PHPUnit/Integration/expected/test_TrackingAPI_SetVisitorId__Live.getLastVisitsDetails_day.xml
new file mode 100644
index 0000000000..c5b8d05443
--- /dev/null
+++ b/tests/PHPUnit/Integration/expected/test_TrackingAPI_SetVisitorId__Live.getLastVisitsDetails_day.xml
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<result>
+	<row>
+		<idVisit>1</idVisit>
+		<userId />
+		<visitorId>a13b7c5a62f72dea</visitorId>
+		<actionDetails>
+			<row>
+				<type>action</type>
+				<url>http://example.org/index.htm</url>
+				<pageTitle>incredible title!</pageTitle>
+				<pageIdAction>2</pageIdAction>
+				<serverTimePretty>Sat 6 Mar 11:22:33</serverTimePretty>
+				<pageId>1</pageId>
+				<icon />
+			</row>
+		</actionDetails>
+		<lastActionDateTime>2010-03-06 11:22:33</lastActionDateTime>
+		<actions>1</actions>
+	</row>
+	<row>
+		<idVisit>2</idVisit>
+		<userId />
+		<visitorId>f66bc315f2a01a79</visitorId>
+		<actionDetails>
+			<row>
+				<type>action</type>
+				<url>http://example.org/index2.htm</url>
+				<pageTitle>incredible title!</pageTitle>
+				<pageIdAction>3</pageIdAction>
+				<serverTimePretty>Sat 6 Mar 11:25:33</serverTimePretty>
+				<pageId>2</pageId>
+				<timeSpent>180</timeSpent>
+				<timeSpentPretty>3 min 0s</timeSpentPretty>
+				<icon />
+			</row>
+			<row>
+				<type>action</type>
+				<url>http://example.org/index3.htm</url>
+				<pageTitle>incredible title!</pageTitle>
+				<pageIdAction>4</pageIdAction>
+				<serverTimePretty>Sat 6 Mar 11:28:33</serverTimePretty>
+				<pageId>3</pageId>
+				<icon />
+			</row>
+		</actionDetails>
+		<lastActionDateTime>2010-03-06 11:28:33</lastActionDateTime>
+		<actions>2</actions>
+	</row>
+	<row>
+		<idVisit>3</idVisit>
+		<userId>email@example.com</userId>
+		<visitorId>5658ffccee7f0ebf</visitorId>
+		<actionDetails>
+			<row>
+				<type>action</type>
+				<url>http://example.org/index.htm</url>
+				<pageTitle>incredible title!</pageTitle>
+				<pageIdAction>2</pageIdAction>
+				<serverTimePretty>Sat 6 Mar 13:22:33</serverTimePretty>
+				<pageId>4</pageId>
+				<timeSpent>360</timeSpent>
+				<timeSpentPretty>6 min 0s</timeSpentPretty>
+				<icon />
+			</row>
+			<row>
+				<type>action</type>
+				<url>http://example.org/index.htm</url>
+				<pageTitle>second page</pageTitle>
+				<pageIdAction>2</pageIdAction>
+				<serverTimePretty>Sat 6 Mar 13:28:33</serverTimePretty>
+				<pageId>5</pageId>
+				<icon />
+			</row>
+		</actionDetails>
+		<lastActionDateTime>2010-03-06 13:28:33</lastActionDateTime>
+		<actions>2</actions>
+	</row>
+	<row>
+		<idVisit>4</idVisit>
+		<userId>new-email@example.com</userId>
+		<visitorId>4b60563d119613fb</visitorId>
+		<actionDetails>
+			<row>
+				<type>action</type>
+				<url>http://example.org/index.htm</url>
+				<pageTitle>a new user id was set -&gt; new visit</pageTitle>
+				<pageIdAction>2</pageIdAction>
+				<serverTimePretty>Sat 6 Mar 13:34:33</serverTimePretty>
+				<pageId>6</pageId>
+				<icon />
+			</row>
+		</actionDetails>
+		<lastActionDateTime>2010-03-06 13:34:33</lastActionDateTime>
+		<actions>1</actions>
+	</row>
+</result>
\ No newline at end of file
diff --git a/tests/PHPUnit/Integration/expected/test_TrackingAPI_SetVisitorId__VisitsSummary.get_day.xml b/tests/PHPUnit/Integration/expected/test_TrackingAPI_SetVisitorId__VisitsSummary.get_day.xml
index 1076ef51ea..db02e17f0a 100644
--- a/tests/PHPUnit/Integration/expected/test_TrackingAPI_SetVisitorId__VisitsSummary.get_day.xml
+++ b/tests/PHPUnit/Integration/expected/test_TrackingAPI_SetVisitorId__VisitsSummary.get_day.xml
@@ -1,13 +1,13 @@
 <?xml version="1.0" encoding="utf-8" ?>
 <result>
-	<nb_uniq_visitors>2</nb_uniq_visitors>
-	<nb_visits>2</nb_visits>
-	<nb_actions>3</nb_actions>
+	<nb_uniq_visitors>4</nb_uniq_visitors>
+	<nb_visits>4</nb_visits>
+	<nb_actions>6</nb_actions>
 	<nb_visits_converted>0</nb_visits_converted>
-	<bounce_count>1</bounce_count>
-	<sum_visit_length>181</sum_visit_length>
+	<bounce_count>2</bounce_count>
+	<sum_visit_length>542</sum_visit_length>
 	<max_actions>2</max_actions>
 	<bounce_rate>50%</bounce_rate>
 	<nb_actions_per_visit>1.5</nb_actions_per_visit>
-	<avg_time_on_site>91</avg_time_on_site>
+	<avg_time_on_site>136</avg_time_on_site>
 </result>
\ No newline at end of file
-- 
GitLab