diff --git a/core/Common.php b/core/Common.php index ecfefabcec0e4d2cf3f9438192e2d6a0c5ca5e04..5d5c35c764a7cb4dd81f600f5b8f14af0a751f3a 100644 --- a/core/Common.php +++ b/core/Common.php @@ -574,6 +574,18 @@ class Common return self::hex2bin($id); } + /** + * Converts a User ID string to the Visitor ID Binary representation. + * + * @param $userId + * @return string + */ + public static function convertUserIdToVisitorIdBin($userId) + { + $userIdHashed = \PiwikTracker::getUserIdHashed($userId); + return self::convertVisitorIdToBin($userIdHashed); + } + /** * Convert IP address (in network address format) to presentation format. * This is a backward compatibility function for code that only expects diff --git a/core/Tracker/Request.php b/core/Tracker/Request.php index eddc99786e476f88a48960bab588a9becd3d70e1..239feb7ed71c30536f1d0385b5c6337f9c5186dc 100644 --- a/core/Tracker/Request.php +++ b/core/Tracker/Request.php @@ -450,8 +450,8 @@ class Request // If User ID is set it takes precedence $userId = $this->getForcedUserId(); if(strlen($userId) > 0) { - $idVisitor = md5($userId); - $idVisitor = $this->truncateIdAsVisitorId($idVisitor); + $userIdHashed = $this->getUserIdHashed($userId); + $idVisitor = $this->truncateIdAsVisitorId($userIdHashed); Common::printDebug("Request will be recorded for this user_id = " . $userId . " (idvisitor = $idVisitor)"); $found = true; } @@ -573,4 +573,15 @@ class Request { return substr($idVisitor, 0, Tracker::LENGTH_HEX_ID_STRING); } + + /** + * Matches implementation of PiwikTracker::getUserIdHashed + * + * @param $userId + * @return string + */ + private function getUserIdHashed($userId) + { + return sha1($userId); + } } diff --git a/lang/en.json b/lang/en.json index 312cb00d46685f2e10022205d23551c86b55c07e..5810c318967e339969fed978882b54949836a38b 100644 --- a/lang/en.json +++ b/lang/en.json @@ -405,6 +405,7 @@ "Unknown": "Unknown", "Upload": "Upload", "UsePlusMinusIconsDocumentation": "Use the plus and minus icons on the left to navigate.", + "UserId": "User ID", "Username": "Username", "UseSMTPServerForEmail": "Use SMTP server for e-mail", "Value": "Value", diff --git a/libs/PiwikTracker/PiwikTracker.php b/libs/PiwikTracker/PiwikTracker.php index 3ef734f3e88077eb957927df7f7bc65b56a27a3e..17899a87286f8d9d7d8eb540733250f49abdb230 100644 --- a/libs/PiwikTracker/PiwikTracker.php +++ b/libs/PiwikTracker/PiwikTracker.php @@ -49,8 +49,8 @@ * $t->setIp( "134.10.22.1" ); * $t->setForceVisitDateTime( '2011-04-05 23:55:02' ); * - * // if you wanted to force to record the page view or conversion to a specific visitorId - * // $t->setVisitorId( "33c31e01394bdc63" ); + * // if you wanted to force to record the page view or conversion to a specific User ID + * // $t->setUserId( "username@example.org" ); * // Mandatory: set the URL being tracked * $t->setUrl( $url = 'http://example.org/store/list-category-toys/' ); * @@ -948,17 +948,48 @@ class PiwikTracker } /** - * Forces the requests to be recorded for the specified Visitor ID - * rather than using the heuristics based on IP and other attributes. + * The User ID is a string representing a given user in your system. * - * Allowed only for Admin/Super User, must be used along with setTokenAuth(). + * A User ID can be a username, UUID or an email address, or any number or string that uniquely identifies a user or client. + * + * @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 === false) { + $this->setNewVisitorId(); + return; + } + if($userId === '') { + throw new Exception("User ID cannot be empty."); + } + $this->userId = $userId; + } + + /** + * Hash function used internally by Piwik to hash a User ID into the Visitor ID. + * + * @param $id + * @return string + */ + static public function getUserIdHashed($id) + { + return substr( sha1( $id ), 0, 16); + } + + + /** + * Forces the requests to be recorded for the specified Visitor ID. + * Note: it is recommended to use ->setUserId($userId); instead. * - * You may set the Visitor ID based on a user attribute, for example the user email: - * $v->setVisitorId( substr(md5( $userEmail ), 0, 16)); + * Rather than letting Piwik attribute the user with a heuristic based on IP and other user fingeprinting attributes, + * force the action to be recorded for a particular visitor. * + * If you use both setVisitorId and setUserId, setUserId will take precedence. * If not set, the visitor ID will be fetched from the 1st party cookie, or will be set to a random UUID. * - * @see setTokenAuth() + * @deprecated We recommend to use ->setUserId($userId). * @param string $visitorId 16 hexadecimal characters visitor ID, eg. "33c31e01394bdc63" * @throws Exception */ @@ -977,25 +1008,6 @@ 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). @@ -1011,7 +1023,7 @@ class PiwikTracker public function getVisitorId() { if (!empty($this->userId)) { - return $this->getIdHashed($this->userId); + return $this->getUserIdHashed($this->userId); } if (!empty($this->forcedVisitorId)) { return $this->forcedVisitorId; @@ -1022,6 +1034,13 @@ class PiwikTracker return $this->randomVisitorId; } + + /** + * Returns the User ID string, which may have been set via: + * $v->setUserId('username@example.org'); + * + * @return bool + */ public function getUserId() { return $this->userId; diff --git a/plugins/API/API.php b/plugins/API/API.php index e499a706b79a79ad041df8dd5a7d41618bfbf827..fd22791a2c57572f2361b2c7dc161d806a7ced3b 100644 --- a/plugins/API/API.php +++ b/plugins/API/API.php @@ -151,6 +151,20 @@ class API extends \Piwik\Plugin\API $isAuthenticatedWithViewAccess = Piwik::isUserHasViewAccess($idSites) && !Piwik::isUserIsAnonymous(); + $segments[] = array( + 'type' => 'dimension', + 'category' => Piwik::translate('General_Visit'), + 'name' => 'General_UserId', + 'segment' => 'userId', + 'acceptedValues' => 'any non empty unique string identifying the user (such as an email address or a username).', + 'sqlSegment' => 'log_visit.idvisitor', + 'sqlFilterValue' => array('Piwik\Common', 'convertUserIdToVisitorIdBin'), + 'permission' => $isAuthenticatedWithViewAccess, + + // TODO specify that this segment is not compatible with some operators +// 'unsupportedOperators' = array(MATCH_CONTAINS, MATCH_DOES_NOT_CONTAIN), + ); + $segments[] = array( 'type' => 'dimension', 'category' => Piwik::translate('General_Visit'), @@ -161,6 +175,7 @@ class API extends \Piwik\Plugin\API 'sqlFilterValue' => array('Piwik\Common', 'convertVisitorIdToBin'), 'permission' => $isAuthenticatedWithViewAccess, ); + $segments[] = array( 'type' => 'dimension', 'category' => Piwik::translate('General_Visit'), diff --git a/tests/PHPUnit/Fixtures/FewVisitsWithSetVisitorIdAndUserId.php b/tests/PHPUnit/Fixtures/FewVisitsWithSetVisitorIdAndUserId.php index ae87644c905fd40c92a9ddf5d681407efd6800b1..8544d1877d89b9f5b487d0971298bf057db44d24 100644 --- a/tests/PHPUnit/Fixtures/FewVisitsWithSetVisitorIdAndUserId.php +++ b/tests/PHPUnit/Fixtures/FewVisitsWithSetVisitorIdAndUserId.php @@ -113,8 +113,8 @@ class FewVisitsWithSetVisitorId extends Fixture // Change User ID -> This will create a new visit $t->setForceVisitDateTime(Date::factory($this->dateTime)->addHour(2.2)->getDatetime()); $t->setNewVisitorId(); - $anotherUserId = 'new-email@example.com'; - $t->setUserId($anotherUserId); + $secondUserId = 'new-email@example.com'; + $t->setUserId($secondUserId); self::checkResponse($t->doTrackPageView('a new user id was set -> new visit')); // A NEW VISIT BY THE SAME USER @@ -125,19 +125,29 @@ class FewVisitsWithSetVisitorId extends Fixture $t->setIp('67.51.31.21'); $t->setUserAgent("Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.9.2.6) Gecko/20100625 Firefox/3.6.6 (.NET CLR 3.5.30729)"); $t->setBrowserLanguage('fr'); - $t->setUserId($anotherUserId); + $t->setUserId($secondUserId); self::checkResponse($t->doTrackPageView('same user id was set -> this is the same unique user')); // Do not pass User ID in this request, it should still attribute to previous visit $t->setForceVisitDateTime(Date::factory($this->dateTime)->addHour(5.1)->getDatetime()); + self::checkResponse($t->doTrackPageView('second pageview - by this user id')); + + // Request from a different computer not yet logged in, this should not be added to our User ID session $t->setUserId(false); - self::checkResponse($t->doTrackPageView('second pageview by this user id')); + // make sure the Id is not so random as to not fail the test + $t->randomVisitorId = '5e15b4d842cc294d'; + + $t->setIp('1.2.4.7'); + $t->setUserAgent("New unique device"); + self::checkResponse($t->doTrackPageView('pageview - should not be tracked by our user id but in a new visit')); + + // User has now logged in so we measure her interactions to her User ID + $t->setUserId($secondUserId); // Trigger a goal conversion $t->setForceVisitDateTime(Date::factory($this->dateTime)->addHour(5.2)->getDatetime()); self::checkResponse($t->doTrackGoal(1)); - // An ecommerce add to cart // (helpful to test that &segment=userId==x will return all items purchased by a specific user ID $t->setForceVisitDateTime(Date::factory($this->dateTime)->addHour(5.3)->getDatetime()); diff --git a/tests/PHPUnit/Integration/TrackingAPISetVisitorIdTest.php b/tests/PHPUnit/Integration/TrackingAPISetVisitorIdTest.php index 45c6fd038b3e24cbb306f5d7712f8d945345073e..1e5ff550b885ca7668e546b25e0b84b16646f9c7 100644 --- a/tests/PHPUnit/Integration/TrackingAPISetVisitorIdTest.php +++ b/tests/PHPUnit/Integration/TrackingAPISetVisitorIdTest.php @@ -47,13 +47,15 @@ class TrackingAPISetVisitorIdTest extends IntegrationTestCase public function getApiForTesting() { return array( - array('VisitsSummary.get', array('idSite' => self::$fixture->idSite, + array('VisitsSummary.get', + array('idSite' => self::$fixture->idSite, 'date' => self::$fixture->dateTime, 'periods' => 'day', 'testSuffix' => '', )), - array('Live.getLastVisitsDetails', array('idSite' => self::$fixture->idSite, + array('Live.getLastVisitsDetails', + array('idSite' => self::$fixture->idSite, 'date' => self::$fixture->dateTime, 'periods' => 'day', 'keepLiveIds' => true, @@ -65,6 +67,25 @@ class TrackingAPISetVisitorIdTest extends IntegrationTestCase ) )), + // Testing userId segment matches both log_visits and log_conversion + array(array('VisitsSummary.get', 'Goals.get'), + array('idSite' => self::$fixture->idSite, + 'date' => self::$fixture->dateTime, + 'periods' => 'day', + 'segment' => 'userId==' . urlencode('new-email@example.com'), + 'testSuffix' => '_segmentUserId', + )), + + array('Goals.getItemsName', + array('idSite' => self::$fixture->idSite, + 'date' => self::$fixture->dateTime, + 'periods' => 'day', + 'segment' => 'visitEcommerceStatus==abandonedCart;userId==' . urlencode('new-email@example.com'), + 'testSuffix' => '_segmentUserIdAndCartAbandoned_getAbandonedCartItems', + 'otherRequestParameters' => array( + 'abandonedCarts' => 1 + ), + )), ); } } diff --git a/tests/PHPUnit/Integration/expected/test_ImportLogs__Live.getLastVisitsDetails_range.xml b/tests/PHPUnit/Integration/expected/test_ImportLogs__Live.getLastVisitsDetails_range.xml index 28929d67eeadee631fb92ed4566141ef45f65ddb..97b92bfe20fedc8ecf5bedc004d93b2935f218e8 100644 --- a/tests/PHPUnit/Integration/expected/test_ImportLogs__Live.getLastVisitsDetails_range.xml +++ b/tests/PHPUnit/Integration/expected/test_ImportLogs__Live.getLastVisitsDetails_range.xml @@ -84,10 +84,10 @@ <location>Raleigh, North Carolina, United States</location> <latitude>35.771999</latitude> <longitude>-78.639000</longitude> - <operatingSystem>Windows 8</operatingSystem> - <operatingSystemCode>WI8</operatingSystemCode> - <operatingSystemShortName>Win 8</operatingSystemShortName> - <operatingSystemIcon>plugins/UserSettings/images/os/WI8.gif</operatingSystemIcon> + <operatingSystem>Windows 8.1</operatingSystem> + <operatingSystemCode>W81</operatingSystemCode> + <operatingSystemShortName>W81</operatingSystemShortName> + <operatingSystemIcon>plugins/UserSettings/images/os/UNK.gif</operatingSystemIcon> <browserFamily>webkit</browserFamily> <browserFamilyDescription>WebKit (Safari, Chrome)</browserFamilyDescription> <browserName>Chrome 33.0</browserName> 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 index c5b8d054437b8799762439ce36c759a865a901b9..aff3cc6b6fd71e20ef1b4e64f668d4c9217648a0 100644 --- a/tests/PHPUnit/Integration/expected/test_TrackingAPI_SetVisitorId__Live.getLastVisitsDetails_day.xml +++ b/tests/PHPUnit/Integration/expected/test_TrackingAPI_SetVisitorId__Live.getLastVisitsDetails_day.xml @@ -50,7 +50,7 @@ <row> <idVisit>3</idVisit> <userId>email@example.com</userId> - <visitorId>5658ffccee7f0ebf</visitorId> + <visitorId>9395988394d4568d</visitorId> <actionDetails> <row> <type>action</type> @@ -79,7 +79,7 @@ <row> <idVisit>4</idVisit> <userId>new-email@example.com</userId> - <visitorId>4b60563d119613fb</visitorId> + <visitorId>c9ade7a5a103b2ed</visitorId> <actionDetails> <row> <type>action</type> @@ -94,4 +94,77 @@ <lastActionDateTime>2010-03-06 13:34:33</lastActionDateTime> <actions>1</actions> </row> + <row> + <idVisit>5</idVisit> + <userId>new-email@example.com</userId> + <visitorId>c9ade7a5a103b2ed</visitorId> + <actionDetails> + <row> + <type>action</type> + <url>http://localhost/piwik-master/</url> + <pageTitle>same user id was set -> this is the same unique user</pageTitle> + <pageIdAction>8</pageIdAction> + <serverTimePretty>Sat 6 Mar 16:22:33</serverTimePretty> + <pageId>7</pageId> + <timeSpent>360</timeSpent> + <timeSpentPretty>6 min 0s</timeSpentPretty> + <icon /> + </row> + <row> + <type>action</type> + <url>http://localhost/piwik-master/</url> + <pageTitle>second pageview - by this user id</pageTitle> + <pageIdAction>8</pageIdAction> + <serverTimePretty>Sat 6 Mar 16:28:33</serverTimePretty> + <pageId>8</pageId> + <icon /> + </row> + <row> + <type>goal</type> + <goalName>triggered js</goalName> + <goalId>1</goalId> + <revenue>0</revenue> + <goalPageId /> + <serverTimePretty>Sat 6 Mar 16:34:33</serverTimePretty> + <url>http://localhost/piwik-master/</url> + <icon>plugins/Morpheus/images/goal.png</icon> + </row> + <row> + <type>ecommerceAbandonedCart</type> + <revenue>10000000000</revenue> + <items>1</items> + <serverTimePretty>Sat 6 Mar 16:40:33</serverTimePretty> + <itemDetails> + <row> + <itemSKU>sku-007-PRISM</itemSKU> + <itemName>My secret spy tech</itemName> + <itemCategory>Surveillance</itemCategory> + <price>10000000000</price> + <quantity>1</quantity> + </row> + </itemDetails> + <icon>plugins/Morpheus/images/ecommerceAbandonedCart.gif</icon> + </row> + </actionDetails> + <lastActionDateTime>2010-03-06 16:40:33</lastActionDateTime> + <actions>2</actions> + </row> + <row> + <idVisit>6</idVisit> + <userId /> + <visitorId>5e15b4d842cc294d</visitorId> + <actionDetails> + <row> + <type>action</type> + <url>http://localhost/piwik-master/</url> + <pageTitle>pageview - should not be tracked by our user id but in a new visit</pageTitle> + <pageIdAction>8</pageIdAction> + <serverTimePretty>Sat 6 Mar 16:28:33</serverTimePretty> + <pageId>9</pageId> + <icon /> + </row> + </actionDetails> + <lastActionDateTime>2010-03-06 16:28: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 db02e17f0af5d001b436dfe32247caddd8e33944..cb863c1626d1920e75fd7dd416fb4e7a96b38f1a 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>4</nb_uniq_visitors> - <nb_visits>4</nb_visits> - <nb_actions>6</nb_actions> - <nb_visits_converted>0</nb_visits_converted> - <bounce_count>2</bounce_count> - <sum_visit_length>542</sum_visit_length> + <nb_uniq_visitors>5</nb_uniq_visitors> + <nb_visits>6</nb_visits> + <nb_actions>9</nb_actions> + <nb_visits_converted>1</nb_visits_converted> + <bounce_count>3</bounce_count> + <sum_visit_length>1623</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>136</avg_time_on_site> + <avg_time_on_site>271</avg_time_on_site> </result> \ No newline at end of file diff --git a/tests/PHPUnit/Integration/expected/test_TrackingAPI_SetVisitorId_segmentUserIdAndCartAbandoned_getAbandonedCartItems__Goals.getItemsName_day.xml b/tests/PHPUnit/Integration/expected/test_TrackingAPI_SetVisitorId_segmentUserIdAndCartAbandoned_getAbandonedCartItems__Goals.getItemsName_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..39c7de375fbd8bd0e86c77fbf3886470f8f9ffa0 --- /dev/null +++ b/tests/PHPUnit/Integration/expected/test_TrackingAPI_SetVisitorId_segmentUserIdAndCartAbandoned_getAbandonedCartItems__Goals.getItemsName_day.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row> + <label>My secret spy tech</label> + <revenue>10000000000</revenue> + <quantity>1</quantity> + <abandoned_carts>1</abandoned_carts> + <avg_price>10000000000</avg_price> + <avg_quantity>1</avg_quantity> + <conversion_rate>0%</conversion_rate> + </row> +</result> \ No newline at end of file diff --git a/tests/PHPUnit/Integration/expected/test_TrackingAPI_SetVisitorId_segmentUserId__Goals.get_day.xml b/tests/PHPUnit/Integration/expected/test_TrackingAPI_SetVisitorId_segmentUserId__Goals.get_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..a43cbc433e552caa08cd0b25bafd106da63b7fea --- /dev/null +++ b/tests/PHPUnit/Integration/expected/test_TrackingAPI_SetVisitorId_segmentUserId__Goals.get_day.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <nb_conversions>1</nb_conversions> + <nb_visits_converted>1</nb_visits_converted> + <conversion_rate>50</conversion_rate> + <revenue>0</revenue> +</result> \ No newline at end of file diff --git a/tests/PHPUnit/Integration/expected/test_TrackingAPI_SetVisitorId_segmentUserId__VisitsSummary.get_day.xml b/tests/PHPUnit/Integration/expected/test_TrackingAPI_SetVisitorId_segmentUserId__VisitsSummary.get_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..50f9c1b3f7e7b31ef36fed0ddc88ab5dadfdcabe --- /dev/null +++ b/tests/PHPUnit/Integration/expected/test_TrackingAPI_SetVisitorId_segmentUserId__VisitsSummary.get_day.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <nb_uniq_visitors>1</nb_uniq_visitors> + <nb_visits>2</nb_visits> + <nb_actions>3</nb_actions> + <nb_visits_converted>1</nb_visits_converted> + <bounce_count>1</bounce_count> + <sum_visit_length>1081</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>541</avg_time_on_site> +</result> \ No newline at end of file