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 Matomo.
* @method static \Piwik\Plugins\Live\API getInstance()
*/
class API extends \Piwik\Plugin\API
{
/**
* @var LoggerInterface
*/
private $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
/**
* 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
* @param array $showColumns The columns to show / not to request. Eg 'visits', 'actions', ...
* @param array $hideColumns The columns to hide / not to request. Eg 'visits', 'actions', ...
* @return array( visits => N, actions => M, visitsConverted => P )
*/
public function getCounters($idSite, $lastMinutes, $segment = false, $showColumns = array(), $hideColumns = array())
{
Piwik::checkUserHasViewAccess($idSite);
$model = new Model();
if (is_string($showColumns)) {
$showColumns = explode(',', $showColumns);
}
if (is_string($hideColumns)) {
$hideColumns = explode(',', $hideColumns);
}
$counters = array();
$hasVisits = true;
if ($this->shouldColumnBePresentInResponse('visits', $showColumns, $hideColumns)) {
$counters['visits'] = $model->getNumVisits($idSite, $lastMinutes, $segment);
$hasVisits = !empty($counters['visits']);
}
if ($this->shouldColumnBePresentInResponse('actions', $showColumns, $hideColumns)) {
if ($hasVisits) {
$counters['actions'] = $model->getNumActions($idSite, $lastMinutes, $segment);
} else {
$counters['actions'] = 0;
}
}
if ($this->shouldColumnBePresentInResponse('visitors', $showColumns, $hideColumns)) {
if ($hasVisits) {
$counters['visitors'] = $model->getNumVisitors($idSite, $lastMinutes, $segment);
} else {
$counters['visitors'] = 0;
}
}
if ($this->shouldColumnBePresentInResponse('visitsConverted', $showColumns, $hideColumns)) {
if ($hasVisits) {
$counters['visitsConverted'] = $model->getNumVisitsConverted($idSite, $lastMinutes, $segment);
} else {
$counters['visitsConverted'] = 0;
}
}
return array($counters);
}
private function shouldColumnBePresentInResponse($column, $showColumns, $hideColumns)
{
$show = (empty($showColumns) || in_array($column, $showColumns));
$hide = in_array($column, $hideColumns);
return $show && !$hide;
}
/**
* 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 DEPRECATED (optional) Only return the last X visits. Please use the API paramaeter 'filter_offset' and 'filter_limit' instead.
* @param bool|int $minTimestamp (optional) Minimum timestamp to restrict the query to (useful when paginating or refreshing visits)
* @param bool $flat
* @param bool $doNotFetchActions
* @param bool $enhanced for plugins that want to expose additional information
* @return DataTable
*/
public function getLastVisitsDetails($idSite, $period = false, $date = false, $segment = false, $countVisitorsToFetch = false, $minTimestamp = false, $flat = false, $doNotFetchActions = false, $enhanced = false)
{
Piwik::checkUserHasViewAccess($idSite);
$idSites = Site::getIdSitesFromIdSitesString($idSite);
if (is_array($idSites) && count($idSites) === 1) {
$idSites = array_shift($idSites);
}
Piwik::checkUserHasViewAccess($idSites);
if (is_numeric($minTimestamp)) {
$minTimestamp = (int) $minTimestamp;
} else {
$minTimestamp = false;
}
if (Request::isCurrentApiRequestTheRootApiRequest() || !in_array(Request::getRootApiRequestMethod(), ['API.getSuggestedValuesForSegment', 'PrivacyManager.findDataSubjects'])) {
if (is_array($idSites)) {
$filteredSites = array_filter($idSites, function($idSite) {
return Live::isVisitorLogEnabled($idSite);
});
if (empty($filteredSites)) {
throw new Exception('Visits log is deactivated for all given websites (idSite='.$idSite.').');
}
} else {
Live::checkIsVisitorLogEnabled($idSites);
}
}
if ($countVisitorsToFetch !== false) {
$filterLimit = (int) $countVisitorsToFetch;
$filterOffset = 0;
} else {
$filterLimit = Common::getRequestVar('filter_limit', 10, 'int');
$filterOffset = Common::getRequestVar('filter_offset', 0, 'int');
}
$filterSortOrder = Common::getRequestVar('filter_sort_order', false, 'string');
$dataTable = $this->loadLastVisitsDetailsFromDatabase($idSites, $period, $date, $segment, $filterOffset, $filterLimit, $minTimestamp, $filterSortOrder, $visitorId = false);
$this->addFilterToCleanVisitors($dataTable, $idSites, $flat, $doNotFetchActions);
$filterSortColumn = Common::getRequestVar('filter_sort_column', false, 'string');
if ($filterSortColumn) {
$this->logger->warning('Sorting the API method "Live.getLastVisitDetails" by column is currently not supported. To avoid this warning remove the URL parameter "filter_sort_column" from your API request.');
}
// Usually one would Sort a DataTable and then apply a Limit. In this case we apply a Limit first in SQL
// for fast offset usage see https://github.com/piwik/piwik/issues/7458. Sorting afterwards would lead to a
// wrong sorting result as it would only sort the limited results. Therefore we do not support a Sort for this
// API
$dataTable->disableFilter('Sort');
$dataTable->disableFilter('Limit'); // limit is already applied here
return $dataTable;
}
/**
* Returns an array describing a visitor using their 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|false|int $limitVisits
* @return array
*/
public function getVisitorProfile($idSite, $visitorId = false, $segment = false, $limitVisits = false)
{
Piwik::checkUserHasViewAccess($idSite);
Live::checkIsVisitorProfileEnabled($idSite);
if (!is_numeric($limitVisits) || $limitVisits <= 0) {
$limitVisits = VisitorProfile::VISITOR_PROFILE_MAX_VISITS_TO_SHOW;
} else {
$limitVisits = (int) $limitVisits;
}
if ($visitorId === false) {
$visitorId = $this->getMostRecentVisitorId($idSite, $segment);
}
$limit = Config::getInstance()->General['live_visitor_profile_max_visits_to_aggregate'];
$visits = $this->loadLastVisitsDetailsFromDatabase($idSite, $period = false, $date = false, $segment,
$offset = 0, $limit, false, false, $visitorId);
$this->addFilterToCleanVisitors($visits, $idSite, $flat = false, $doNotFetchActions = false, $filterNow = true);
if ($visits->getRowsCount() == 0) {
return array();
}
$profile = new VisitorProfile($idSite);
$result = $profile->makeVisitorProfile($visits, $visitorId, $segment, $limitVisits);
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);
// for faster performance search for a visitor within the last 7 days first
$minTimestamp = Date::now()->subDay(7)->getTimestamp();
$dataTable = $this->loadLastVisitsDetailsFromDatabase(
$idSite, $period = false, $date = false, $segment, $offset = 0, $limit = 1, $minTimestamp
);
if (0 >= $dataTable->getRowsCount()) {
$minTimestamp = Date::now()->subYear(1)->getTimestamp();
// no visitor found in last 7 days, look further back for up to 1 year. This query will be slower
$dataTable = $this->loadLastVisitsDetailsFromDatabase(
$idSite, $period = false, $date = false, $segment, $offset = 0, $limit = 1, $minTimestamp
);
}
if (0 >= $dataTable->getRowsCount()) {
// no visitor found in last year, look over all logs. This query might be quite slow
$dataTable = $this->loadLastVisitsDetailsFromDatabase(
$idSite, $period = false, $date = false, $segment, $offset = 0, $limit = 1
);
}
if (0 >= $dataTable->getRowsCount()) {
return false;
}
$visitorFactory = new VisitorFactory();
$visitDetails = $dataTable->getFirstRow()->getColumns();
$visitor = $visitorFactory->create($visitDetails);
return $visitor->getVisitorId();
}
/**
* Returns the very first visit for the given visitorId
*
* @internal
*
* @param $idSite
* @param $visitorId
*
* @return DataTable
*/
public function getFirstVisitForVisitorId($idSite, $visitorId)
{
Piwik::checkUserHasSomeViewAccess();
Live::checkIsVisitorProfileEnabled($idSite);
if (empty($visitorId)) {
return new DataTable();
}
$model = new Model();
$data = $model->queryLogVisits($idSite, false, false, false, 0, 1, $visitorId, false, 'ASC');
$dataTable = $this->makeVisitorTableFromArray($data);
$this->addFilterToCleanVisitors($dataTable, $idSite, false, true);
return $dataTable;
}
/**
* 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 */
$visitorFactory = new VisitorFactory();
// live api is not summable, prevents errors like "Unexpected ECommerce status value"
$table->deleteRow(DataTable::ID_SUMMARY_ROW);
$actionsByVisitId = array();
if (!$doNotFetchActions) {
$visitIds = $table->getColumn('idvisit');
$visitorDetailsManipulators = Visitor::getAllVisitorDetailsInstances();
foreach ($visitorDetailsManipulators as $instance) {
$instance->provideActionsForVisitIds($actionsByVisitId, $visitIds);
}
}
foreach ($table->getRows() as $visitorDetailRow) {
$visitorDetailsArray = Visitor::cleanVisitorDetails($visitorDetailRow->getColumns());
$visitor = $visitorFactory->create($visitorDetailsArray);
$visitorDetailsArray = $visitor->getAllVisitorDetails();
$visitorDetailsArray['actionDetails'] = array();
if (!$doNotFetchActions) {
$bulkFetchedActions = isset($actionsByVisitId[$visitorDetailsArray['idVisit']]) ? $actionsByVisitId[$visitorDetailsArray['idVisit']] : array();
$visitorDetailsArray = Visitor::enrichVisitorArrayWithActions($visitorDetailsArray, $bulkFetchedActions);
}
if ($flat) {
$visitorDetailsArray = Visitor::flattenVisitorDetailsArray($visitorDetailsArray);
}
$visitorDetailRow->setColumns($visitorDetailsArray);
}
});
}
private function loadLastVisitsDetailsFromDatabase($idSite, $period, $date, $segment = false, $offset = 0, $limit = 100, $minTimestamp = false, $filterSortOrder = false, $visitorId = false)
{
$model = new Model();
[$data, $hasMoreVisits] = $model->queryLogVisits($idSite, $period, $date, $segment, $offset, $limit, $visitorId, $minTimestamp, $filterSortOrder, true);
return $this->makeVisitorTableFromArray($data, $hasMoreVisits);
}
/**
* @param $data
* @param $hasMoreVisits
* @return DataTable
* @throws Exception
*/
private function makeVisitorTableFromArray($data, $hasMoreVisits=null)
{
$dataTable = new DataTable();
$dataTable->addRowsFromSimpleArray($data);
if (!empty($data[0])) {
$columnsToNotAggregate = array_map(function () {
return 'skip';
}, $data[0]);
$dataTable->setMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME, $columnsToNotAggregate);
}
if (null !== $hasMoreVisits) {
$dataTable->setMetadata('hasMoreVisits', $hasMoreVisits);
}
return $dataTable;
}
}