Segmentation, * you will be able to request visits filtered by any criteria. * * The method "getLastVisitsDetails" will return extensive data for each visit, which includes: server time, visitId, visitorId, * visitorType (new or returning), number of pages, list of all pages (and events, file downloaded and outlinks clicked), * custom variables names and values set to this visit, number of goal conversions (and list of all Goal conversions for this visit, * with time of conversion, revenue, URL, etc.), but also other attributes such as: days since last visit, days since first visit, * country, continent, visitor IP, * provider, referrer used (referrer name, keyword if it was a search engine, full URL), campaign name and keyword, operating system, * browser, type of screen, resolution, supported browser plugins (flash, java, silverlight, pdf, etc.), various dates & times format to make * it easier for API users... and more! * * With the parameter '&segment=' you can filter the * returned visits by any criteria (visitor IP, visitor ID, country, keyword used, time of day, etc.). * * The method "getCounters" is used to return a simple counter: visits, number of actions, number of converted visits, in the last N minutes. * * See also the documentation about Real time widget and visitor level reports in Piwik. * @method static \Piwik\Plugins\Live\API getInstance() */ class API extends \Piwik\Plugin\API { const VISITOR_PROFILE_MAX_VISITS_TO_AGGREGATE = 100; const VISITOR_PROFILE_MAX_VISITS_TO_SHOW = 10; const VISITOR_PROFILE_DATE_FORMAT = '%day% %shortMonth% %longYear%'; /** * This will return simple counters, for a given website ID, for visits over the last N minutes * * @param int $idSite Id Site * @param int $lastMinutes Number of minutes to look back at * @param bool|string $segment * @return array( visits => N, actions => M, visitsConverted => P ) */ public function getCounters($idSite, $lastMinutes, $segment = false) { Piwik::checkUserHasViewAccess($idSite); $lastMinutes = (int)$lastMinutes; $select = "count(*) as visits, SUM(log_visit.visit_total_actions) as actions, SUM(log_visit.visit_goal_converted) as visitsConverted, COUNT(DISTINCT log_visit.idvisitor) as visitors"; $from = "log_visit"; list($whereIdSites, $idSites) = $this->getIdSitesWhereClause($idSite); $where = $whereIdSites . "AND log_visit.visit_last_action_time >= ?"; $bind = $idSites; $bind[] = Date::factory(time() - $lastMinutes * 60)->toString('Y-m-d H:i:s'); $segment = new Segment($segment, $idSite); $query = $segment->getSelectQuery($select, $from, $where, $bind); $data = Db::fetchAll($query['sql'], $query['bind']); // These could be unset for some reasons, ensure they are set to 0 if (empty($data[0]['actions'])) { $data[0]['actions'] = 0; } if (empty($data[0]['visitsConverted'])) { $data[0]['visitsConverted'] = 0; } return $data; } /** * The same functionnality can be obtained using segment=visitorId==$visitorId with getLastVisitsDetails * * @deprecated * @ignore * @param int $visitorId * @param int $idSite * @param int $filter_limit * @param bool $flat Whether to flatten the visitor details array * * @return DataTable */ public function getLastVisitsForVisitor($visitorId, $idSite, $filter_limit = 10, $flat = false) { Piwik::checkUserHasViewAccess($idSite); $countVisitorsToFetch = $filter_limit; $table = $this->loadLastVisitorDetailsFromDatabase($idSite, $period = false, $date = false, $segment = false, $countVisitorsToFetch, $visitorId); $this->addFilterToCleanVisitors($table, $idSite, $flat); return $table; } /** * Returns the last visits tracked in the specified website * You can define any number of filters: none, one, many or all parameters can be defined * * @param int $idSite Site ID * @param bool|string $period Period to restrict to when looking at the logs * @param bool|string $date Date to restrict to * @param bool|int $segment (optional) Number of visits rows to return * @param bool|int $countVisitorsToFetch (optional) Only return the last X visits. By default the last GET['filter_offset']+GET['filter_limit'] are returned. * @param bool|int $minTimestamp (optional) Minimum timestamp to restrict the query to (useful when paginating or refreshing visits) * @param bool $flat * @param bool $doNotFetchActions * @return DataTable */ public function getLastVisitsDetails($idSite, $period = false, $date = false, $segment = false, $countVisitorsToFetch = false, $minTimestamp = false, $flat = false, $doNotFetchActions = false) { if (false === $countVisitorsToFetch) { $filter_limit = Common::getRequestVar('filter_limit', 10, 'int'); $filter_offset = Common::getRequestVar('filter_offset', 0, 'int'); $countVisitorsToFetch = $filter_limit + $filter_offset; } Piwik::checkUserHasViewAccess($idSite); $dataTable = $this->loadLastVisitorDetailsFromDatabase($idSite, $period, $date, $segment, $countVisitorsToFetch, $visitorId = false, $minTimestamp); $this->addFilterToCleanVisitors($dataTable, $idSite, $flat, $doNotFetchActions); return $dataTable; } /** * Returns an array describing a visitor using her last visits (uses a maximum of 100). * * @param int $idSite Site ID * @param bool|false|string $visitorId The ID of the visitor whose profile to retrieve. * @param bool|false|string $segment * @param bool $checkForLatLong If true, hasLatLong will appear in the output and be true if * one of the first 100 visits has a latitude/longitude. * @return array */ public function getVisitorProfile($idSite, $visitorId = false, $segment = false, $checkForLatLong = false) { Piwik::checkUserHasViewAccess($idSite); if ($visitorId === false) { $visitorId = $this->getMostRecentVisitorId($idSite, $segment); } $newSegment = ($segment === false ? '' : $segment . ';') . 'visitorId==' . $visitorId; $visits = $this->loadLastVisitorDetailsFromDatabase($idSite, $period = false, $date = false, $newSegment, $numVisitorsToFetch = self::VISITOR_PROFILE_MAX_VISITS_TO_AGGREGATE, $overrideVisitorId = false, $minTimestamp = false); $this->addFilterToCleanVisitors($visits, $idSite, $flat = false, $doNotFetchActions = false, $filterNow = true); if ($visits->getRowsCount() == 0) { return array(); } $isEcommerceEnabled = Site::isEcommerceEnabledFor($idSite); $result = array(); $result['totalVisits'] = 0; $result['totalVisitDuration'] = 0; $result['totalActions'] = 0; $result['totalSearches'] = 0; $result['totalPageViews'] = 0; $result['totalGoalConversions'] = 0; $result['totalConversionsByGoal'] = array(); if ($isEcommerceEnabled) { $result['totalEcommerceConversions'] = 0; $result['totalEcommerceRevenue'] = 0; $result['totalEcommerceItems'] = 0; $result['totalAbandonedCarts'] = 0; $result['totalAbandonedCartsRevenue'] = 0; $result['totalAbandonedCartsItems'] = 0; } $countries = array(); $continents = array(); $cities = array(); $siteSearchKeywords = array(); $pageGenerationTimeTotal = 0; // aggregate all requested visits info for total_* info foreach ($visits->getRows() as $visit) { ++$result['totalVisits']; $result['totalVisitDuration'] += $visit->getColumn('visitDuration'); $result['totalActions'] += $visit->getColumn('actions'); $result['totalGoalConversions'] += $visit->getColumn('goalConversions'); // individual goal conversions are stored in action details foreach ($visit->getColumn('actionDetails') as $action) { if ($action['type'] == 'goal') { // handle goal conversion $idGoal = $action['goalId']; $idGoalKey = 'idgoal=' . $idGoal; if (!isset($result['totalConversionsByGoal'][$idGoalKey])) { $result['totalConversionsByGoal'][$idGoalKey] = 0; } ++$result['totalConversionsByGoal'][$idGoalKey]; if (!empty($action['revenue'])) { if (!isset($result['totalRevenueByGoal'][$idGoalKey])) { $result['totalRevenueByGoal'][$idGoalKey] = 0; } $result['totalRevenueByGoal'][$idGoalKey] += $action['revenue']; } } else if ($action['type'] == Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER // handle ecommerce order && $isEcommerceEnabled ) { ++$result['totalEcommerceConversions']; $result['totalEcommerceRevenue'] += $action['revenue']; $result['totalEcommerceItems'] += $action['items']; } else if ($action['type'] == Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_CART // handler abandoned cart && $isEcommerceEnabled ) { ++$result['totalAbandonedCarts']; $result['totalAbandonedCartsRevenue'] += $action['revenue']; $result['totalAbandonedCartsItems'] += $action['items']; } if (isset($action['siteSearchKeyword'])) { $keyword = $action['siteSearchKeyword']; if (!isset($siteSearchKeywords[$keyword])) { $siteSearchKeywords[$keyword] = 0; ++$result['totalSearches']; } ++$siteSearchKeywords[$keyword]; } if (isset($action['generationTime'])) { $pageGenerationTimeTotal += $action['generationTime']; ++$result['totalPageViews']; } } $countryCode = $visit->getColumn('countryCode'); if (!isset($countries[$countryCode])) { $countries[$countryCode] = 0; } ++$countries[$countryCode]; $continentCode = $visit->getColumn('continentCode'); if (!isset($continents[$continentCode])) { $continents[$continentCode] = 0; } ++$continents[$continentCode]; if (!array_key_exists($countryCode, $cities)) { $cities[$countryCode] = array(); } $city = $visit->getColumn('city'); if(!empty($city)) { $cities[$countryCode][] = $city; } } // sort countries/continents/search keywords by visit/action asort($countries); asort($continents); arsort($siteSearchKeywords); // transform country/continents/search keywords into something that will look good in XML $result['countries'] = $result['continents'] = $result['searches'] = array(); foreach ($countries as $countryCode => $nbVisits) { $countryInfo = array('country' => $countryCode, 'nb_visits' => $nbVisits, 'flag' => \Piwik\Plugins\UserCountry\getFlagFromCode($countryCode), 'prettyName' => \Piwik\Plugins\UserCountry\countryTranslate($countryCode)); if(!empty($cities[$countryCode])) { $countryInfo['cities'] = array_unique($cities[$countryCode]); } $result['countries'][] = $countryInfo; } foreach ($continents as $continentCode => $nbVisits) { $result['continents'][] = array('continent' => $continentCode, 'nb_visits' => $nbVisits, 'prettyName' => \Piwik\Plugins\UserCountry\continentTranslate($continentCode)); } foreach ($siteSearchKeywords as $keyword => $searchCount) { $result['searches'][] = array('keyword' => $keyword, 'searches' => $searchCount); } if ($result['totalPageViews']) { $result['averagePageGenerationTime'] = round($pageGenerationTimeTotal / $result['totalPageViews'], $precision = 2); } $result['totalVisitDurationPretty'] = MetricsFormatter::getPrettyTimeFromSeconds($result['totalVisitDuration']); // use requested visits for first/last visit info $rows = $visits->getRows(); $result['firstVisit'] = $this->getVisitorProfileVisitSummary(end($rows)); $result['lastVisit'] = $this->getVisitorProfileVisitSummary(reset($rows)); // check if requested visits have lat/long if ($checkForLatLong) { $result['hasLatLong'] = false; foreach ($rows as $visit) { if ($visit->getColumn('latitude') !== false) { // realtime map only checks for latitude $result['hasLatLong'] = true; break; } } } // save count of visits we queries $result['visitsAggregated'] = count($rows); // use N most recent visits for last_visits $visits->deleteRowsOffset(self::VISITOR_PROFILE_MAX_VISITS_TO_SHOW); $result['lastVisits'] = $visits; // use the right date format for the pretty server date $timezone = Site::getTimezoneFor($idSite); foreach ($result['lastVisits']->getRows() as $visit) { $dateTimeVisitFirstAction = Date::factory($visit->getColumn('firstActionTimestamp'), $timezone); $datePretty = $dateTimeVisitFirstAction->getLocalized(self::VISITOR_PROFILE_DATE_FORMAT); $visit->setColumn('serverDatePrettyFirstAction', $datePretty); $dateTimePretty = $datePretty . ' ' . $visit->getColumn('serverTimePrettyFirstAction'); $visit->setColumn('serverDateTimePrettyFirstAction', $dateTimePretty); } // get visitor IDs that are adjacent to this one in log_visit // TODO: make sure order of visitor ids is not changed if a returning visitor visits while the user is // looking at the popup. $latestVisitTime = reset($rows)->getColumn('lastActionDateTime'); $result['nextVisitorId'] = $this->getAdjacentVisitorId($idSite, $visitorId, $latestVisitTime, $segment, $getNext = true); $result['previousVisitorId'] = $this->getAdjacentVisitorId($idSite, $visitorId, $latestVisitTime, $segment, $getNext = false); /** * Triggered in the Live.getVisitorProfile API method. Plugins can use this event * to discover and add extra data to visitor profiles. * * For example, if an email address is found in a custom variable, a plugin could load the * gravatar for the email and add it to the visitor profile, causing it to display in the * visitor profile popup. * * The following visitor profile elements can be set to augment the visitor profile popup: * * - **visitorAvatar**: A URL to an image to display in the top left corner of the popup. * - **visitorDescription**: Text to be used as the tooltip of the avatar image. * * @param array &$visitorProfile The unaugmented visitor profile info. */ Piwik::postEvent('Live.getExtraVisitorDetails', array(&$result)); return $result; } /** * Returns the visitor ID of the most recent visit. * * @param int $idSite * @param bool|string $segment * @return string */ public function getMostRecentVisitorId($idSite, $segment = false) { Piwik::checkUserHasViewAccess($idSite); $dataTable = $this->loadLastVisitorDetailsFromDatabase( $idSite, $period = false, $date = false, $segment, $numVisitorsToFetch = 1, $visitorId = false, $minTimestamp = false ); if (0 >= $dataTable->getRowsCount()) { return false; } $visitDetails = $dataTable->getFirstRow()->getColumns(); $visitor = new Visitor($visitDetails); return $visitor->getVisitorId(); } /** * Returns the ID of a visitor that is adjacent to another visitor (by time of last action) * in the log_visit table. * * @param int $idSite The ID of the site whose visits should be looked at. * @param string $visitorId The ID of the visitor to get an adjacent visitor for. * @param string $visitLastActionTime The last action time of the latest visit for $visitorId. * @param string $segment * @param bool $getNext Whether to retrieve the next visitor or the previous visitor. The next * visitor will be the visitor that appears chronologically later in the * log_visit table. The previous visitor will be the visitor that appears * earlier. * @return string The hex visitor ID. */ private function getAdjacentVisitorId($idSite, $visitorId, $visitLastActionTime, $segment, $getNext) { if ($getNext) { $visitLastActionTimeCondition = "sub.visit_last_action_time <= ?"; $orderByDir = "DESC"; } else { $visitLastActionTimeCondition = "sub.visit_last_action_time >= ?"; $orderByDir = "ASC"; } $visitLastActionDate = Date::factory($visitLastActionTime); $dateOneDayAgo = $visitLastActionDate->subDay(1); $dateOneDayInFuture = $visitLastActionDate->addDay(1); $select = "log_visit.idvisitor, MAX(log_visit.visit_last_action_time) as visit_last_action_time"; $from = "log_visit"; $where = "log_visit.idsite = ? AND log_visit.idvisitor <> ? AND visit_last_action_time >= ? and visit_last_action_time <= ?"; $whereBind = array($idSite, @Common::hex2bin($visitorId), $dateOneDayAgo->toString('Y-m-d H:i:s'), $dateOneDayInFuture->toString('Y-m-d H:i:s')); $orderBy = "MAX(log_visit.visit_last_action_time) $orderByDir"; $groupBy = "log_visit.idvisitor"; $segment = new Segment($segment, $idSite); $queryInfo = $segment->getSelectQuery($select, $from, $where, $whereBind, $orderBy, $groupBy); $sql = "SELECT sub.idvisitor, sub.visit_last_action_time FROM ({$queryInfo['sql']}) as sub WHERE $visitLastActionTimeCondition LIMIT 1"; $bind = array_merge($queryInfo['bind'], array($visitLastActionTime)); $visitorId = Db::fetchOne($sql, $bind); if (!empty($visitorId)) { $visitorId = bin2hex($visitorId); } return $visitorId; } /** * Returns a summary for an important visit. Used to describe the first & last visits of a visitor. * * @param Row $visit * @return array */ private function getVisitorProfileVisitSummary($visit) { $today = Date::today(); $serverDate = $visit->getColumn('serverDate'); return array( 'date' => $serverDate, 'prettyDate' => Date::factory($serverDate)->getLocalized(self::VISITOR_PROFILE_DATE_FORMAT), 'daysAgo' => (int)Date::secondsToDays($today->getTimestamp() - Date::factory($serverDate)->getTimestamp()), 'referrerType' => $visit->getColumn('referrerType'), 'referralSummary' => self::getReferrerSummaryForVisit($visit), ); } /** * Returns a summary for a visit's referral. * * @param Row $visit * @return bool|mixed|string * @ignore */ public static function getReferrerSummaryForVisit($visit) { $referrerType = $visit->getColumn('referrerType'); if ($referrerType === false || $referrerType == 'direct' ) { $result = Piwik::translate('Referrers_DirectEntry'); } else if ($referrerType == 'search') { $result = $visit->getColumn('referrerName'); $keyword = $visit->getColumn('referrerKeyword'); if ($keyword !== false && $keyword != APIReferrers::getKeywordNotDefinedString() ) { $result .= ' (' . $keyword . ')'; } } else if ($referrerType == 'campaign') { $result = Piwik::translate('Referrers_ColumnCampaign') . ' (' . $visit->getColumn('referrerName') . ')'; } else { $result = $visit->getColumn('referrerName'); } return $result; } /** * @deprecated */ public function getLastVisits($idSite, $filter_limit = 10, $minTimestamp = false) { return $this->getLastVisitsDetails($idSite, $period = false, $date = false, $segment = false, $countVisitorsToFetch = $filter_limit, $minTimestamp, $flat = false); } /** * For an array of visits, query the list of pages for this visit * as well as make the data human readable * @param DataTable $dataTable * @param int $idSite * @param bool $flat whether to flatten the array (eg. 'customVariables' names/values will appear in the root array rather than in 'customVariables' key * @param bool $doNotFetchActions If set to true, we only fetch visit info and not actions (much faster) * @param bool $filterNow If true, the visitors will be cleaned immediately */ private function addFilterToCleanVisitors(DataTable $dataTable, $idSite, $flat = false, $doNotFetchActions = false, $filterNow = false) { $filter = 'queueFilter'; if ($filterNow) { $filter = 'filter'; } $dataTable->$filter(function ($table) use ($idSite, $flat, $doNotFetchActions) { /** @var DataTable $table */ $actionsLimit = (int)Config::getInstance()->General['visitor_log_maximum_actions_per_visit']; $site = new Site($idSite); $timezone = $site->getTimezone(); $currencies = APISitesManager::getInstance()->getCurrencySymbols(); foreach ($table->getRows() as $visitorDetailRow) { $visitorDetailsArray = Visitor::cleanVisitorDetails($visitorDetailRow->getColumns()); $visitor = new Visitor($visitorDetailsArray); $visitorDetailsArray = $visitor->getAllVisitorDetails(); $visitorDetailsArray['siteCurrency'] = $site->getCurrency(); $visitorDetailsArray['siteCurrencySymbol'] = @$currencies[$site->getCurrency()]; $visitorDetailsArray['serverTimestamp'] = $visitorDetailsArray['lastActionTimestamp']; $dateTimeVisit = Date::factory($visitorDetailsArray['lastActionTimestamp'], $timezone); $visitorDetailsArray['serverTimePretty'] = $dateTimeVisit->getLocalized('%time%'); $visitorDetailsArray['serverDatePretty'] = $dateTimeVisit->getLocalized(Piwik::translate('CoreHome_ShortDateFormat')); $dateTimeVisitFirstAction = Date::factory($visitorDetailsArray['firstActionTimestamp'], $timezone); $visitorDetailsArray['serverDatePrettyFirstAction'] = $dateTimeVisitFirstAction->getLocalized(Piwik::translate('CoreHome_ShortDateFormat')); $visitorDetailsArray['serverTimePrettyFirstAction'] = $dateTimeVisitFirstAction->getLocalized('%time%'); $visitorDetailsArray['actionDetails'] = array(); if (!$doNotFetchActions) { $visitorDetailsArray = Visitor::enrichVisitorArrayWithActions($visitorDetailsArray, $actionsLimit, $timezone); } if ($flat) { $visitorDetailsArray = Visitor::flattenVisitorDetailsArray($visitorDetailsArray); } $visitorDetailRow->setColumns($visitorDetailsArray); } }); } private function loadLastVisitorDetailsFromDatabase($idSite, $period, $date, $segment = false, $countVisitorsToFetch = 100, $visitorId = false, $minTimestamp = false) { $where = $whereBind = array(); list($whereClause, $idSites) = $this->getIdSitesWhereClause($idSite); $where[] = $whereClause; $whereBind = $idSites; $orderBy = "idsite, visit_last_action_time DESC"; $orderByParent = "sub.visit_last_action_time DESC"; if (!empty($visitorId)) { $where[] = "log_visit.idvisitor = ? "; $whereBind[] = @Common::hex2bin($visitorId); } if (!empty($minTimestamp)) { $where[] = "log_visit.visit_last_action_time > ? "; $whereBind[] = date("Y-m-d H:i:s", $minTimestamp); } // If no other filter, only look at the last 24 hours of stats if (empty($visitorId) && empty($countVisitorsToFetch) && empty($period) && empty($date) ) { $period = 'day'; $date = 'yesterdaySameTime'; } // SQL Filter with provided period if (!empty($period) && !empty($date)) { $currentSite = new Site($idSite); $currentTimezone = $currentSite->getTimezone(); $dateString = $date; if ($period == 'range') { $processedPeriod = new Range('range', $date); if ($parsedDate = Range::parseDateRange($date)) { $dateString = $parsedDate[2]; } } else { $processedDate = Date::factory($date); if ($date == 'today' || $date == 'now' || $processedDate->toString() == Date::factory('now', $currentTimezone)->toString() ) { $processedDate = $processedDate->subDay(1); } $processedPeriod = Period::factory($period, $processedDate); } $dateStart = $processedPeriod->getDateStart()->setTimezone($currentTimezone); $where[] = "log_visit.visit_last_action_time >= ?"; $whereBind[] = $dateStart->toString('Y-m-d H:i:s'); if (!in_array($date, array('now', 'today', 'yesterdaySameTime')) && strpos($date, 'last') === false && strpos($date, 'previous') === false && Date::factory($dateString)->toString('Y-m-d') != Date::factory('now', $currentTimezone)->toString() ) { $dateEnd = $processedPeriod->getDateEnd()->setTimezone($currentTimezone); $where[] = " log_visit.visit_last_action_time <= ?"; $dateEndString = $dateEnd->addDay(1)->toString('Y-m-d H:i:s'); $whereBind[] = $dateEndString; } } if (count($where) > 0) { $where = join(" AND ", $where); } else { $where = false; } $segment = new Segment($segment, $idSite); // Subquery to use the indexes for ORDER BY $select = "log_visit.*"; $from = "log_visit"; $subQuery = $segment->getSelectQuery($select, $from, $where, $whereBind, $orderBy); $sqlLimit = $countVisitorsToFetch >= 1 ? " LIMIT 0, " . (int)$countVisitorsToFetch : ""; // Group by idvisit so that a visitor converting 2 goals only appears once $sql = " SELECT sub.* FROM ( " . $subQuery['sql'] . " $sqlLimit ) AS sub GROUP BY sub.idvisit ORDER BY $orderByParent "; try { $data = Db::fetchAll($sql, $subQuery['bind']); } catch (Exception $e) { echo $e->getMessage(); exit; } $dataTable = new DataTable(); $dataTable->addRowsFromSimpleArray($data); return $dataTable; } /** * @param $idSite * @return array */ private function getIdSitesWhereClause($idSite) { $idSites = array($idSite); Piwik::postEvent('Live.API.getIdSitesString', array(&$idSites)); $idSitesBind = Common::getSqlStringFieldsArray($idSites); $whereClause = "log_visit.idsite in ($idSitesBind) "; return array($whereClause, $idSites); } }