hide map for Character groups Quest Stations when there are no stations
This commit is contained in:
commit
df14dfafc3
4371 changed files with 1220224 additions and 0 deletions
360
www/analytics/core/DataAccess/ArchiveSelector.php
Normal file
360
www/analytics/core/DataAccess/ArchiveSelector.php
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataAccess;
|
||||
|
||||
use Exception;
|
||||
use Piwik\ArchiveProcessor\Rules;
|
||||
use Piwik\ArchiveProcessor;
|
||||
use Piwik\Common;
|
||||
use Piwik\Date;
|
||||
use Piwik\Db;
|
||||
use Piwik\Log;
|
||||
|
||||
use Piwik\Period;
|
||||
use Piwik\Period\Range;
|
||||
use Piwik\Piwik;
|
||||
use Piwik\Segment;
|
||||
use Piwik\Site;
|
||||
|
||||
/**
|
||||
* Data Access object used to query archives
|
||||
*
|
||||
* A record in the Database for a given report is defined by
|
||||
* - idarchive = unique ID that is associated to all the data of this archive (idsite+period+date)
|
||||
* - idsite = the ID of the website
|
||||
* - date1 = starting day of the period
|
||||
* - date2 = ending day of the period
|
||||
* - period = integer that defines the period (day/week/etc.). @see period::getId()
|
||||
* - ts_archived = timestamp when the archive was processed (UTC)
|
||||
* - name = the name of the report (ex: uniq_visitors or search_keywords_by_search_engines)
|
||||
* - value = the actual data (a numeric value, or a blob of compressed serialized data)
|
||||
*
|
||||
*/
|
||||
class ArchiveSelector
|
||||
{
|
||||
const NB_VISITS_RECORD_LOOKED_UP = "nb_visits";
|
||||
|
||||
const NB_VISITS_CONVERTED_RECORD_LOOKED_UP = "nb_visits_converted";
|
||||
|
||||
static public function getArchiveIdAndVisits(ArchiveProcessor\Parameters $params, $minDatetimeArchiveProcessedUTC)
|
||||
{
|
||||
$dateStart = $params->getPeriod()->getDateStart();
|
||||
$bindSQL = array($params->getSite()->getId(),
|
||||
$dateStart->toString('Y-m-d'),
|
||||
$params->getPeriod()->getDateEnd()->toString('Y-m-d'),
|
||||
$params->getPeriod()->getId(),
|
||||
);
|
||||
|
||||
$timeStampWhere = '';
|
||||
if ($minDatetimeArchiveProcessedUTC) {
|
||||
$timeStampWhere = " AND ts_archived >= ? ";
|
||||
$bindSQL[] = Date::factory($minDatetimeArchiveProcessedUTC)->getDatetime();
|
||||
}
|
||||
|
||||
$requestedPlugin = $params->getRequestedPlugin();
|
||||
$segment = $params->getSegment();
|
||||
$isSkipAggregationOfSubTables = $params->isSkipAggregationOfSubTables();
|
||||
|
||||
$plugins = array("VisitsSummary", $requestedPlugin);
|
||||
$sqlWhereArchiveName = self::getNameCondition($plugins, $segment, $isSkipAggregationOfSubTables);
|
||||
|
||||
$sqlQuery = " SELECT idarchive, value, name, date1 as startDate
|
||||
FROM " . ArchiveTableCreator::getNumericTable($dateStart) . "``
|
||||
WHERE idsite = ?
|
||||
AND date1 = ?
|
||||
AND date2 = ?
|
||||
AND period = ?
|
||||
AND ( ($sqlWhereArchiveName)
|
||||
OR name = '" . self::NB_VISITS_RECORD_LOOKED_UP . "'
|
||||
OR name = '" . self::NB_VISITS_CONVERTED_RECORD_LOOKED_UP . "')
|
||||
$timeStampWhere
|
||||
ORDER BY idarchive DESC";
|
||||
$results = Db::fetchAll($sqlQuery, $bindSQL);
|
||||
if (empty($results)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$idArchive = self::getMostRecentIdArchiveFromResults($segment, $requestedPlugin, $isSkipAggregationOfSubTables, $results);
|
||||
$idArchiveVisitsSummary = self::getMostRecentIdArchiveFromResults($segment, "VisitsSummary", $isSkipAggregationOfSubTables, $results);
|
||||
|
||||
list($visits, $visitsConverted) = self::getVisitsMetricsFromResults($idArchive, $idArchiveVisitsSummary, $results);
|
||||
|
||||
if ($visits === false
|
||||
&& $idArchive === false
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return array($idArchive, $visits, $visitsConverted);
|
||||
}
|
||||
|
||||
protected static function getVisitsMetricsFromResults($idArchive, $idArchiveVisitsSummary, $results)
|
||||
{
|
||||
$visits = $visitsConverted = false;
|
||||
$archiveWithVisitsMetricsWasFound = ($idArchiveVisitsSummary !== false);
|
||||
if ($archiveWithVisitsMetricsWasFound) {
|
||||
$visits = $visitsConverted = 0;
|
||||
}
|
||||
foreach ($results as $result) {
|
||||
if (in_array($result['idarchive'], array($idArchive, $idArchiveVisitsSummary))) {
|
||||
$value = (int)$result['value'];
|
||||
if (empty($visits)
|
||||
&& $result['name'] == self::NB_VISITS_RECORD_LOOKED_UP
|
||||
) {
|
||||
$visits = $value;
|
||||
}
|
||||
if (empty($visitsConverted)
|
||||
&& $result['name'] == self::NB_VISITS_CONVERTED_RECORD_LOOKED_UP
|
||||
) {
|
||||
$visitsConverted = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return array($visits, $visitsConverted);
|
||||
}
|
||||
|
||||
protected static function getMostRecentIdArchiveFromResults(Segment $segment, $requestedPlugin, $isSkipAggregationOfSubTables, $results)
|
||||
{
|
||||
$idArchive = false;
|
||||
$namesRequestedPlugin = Rules::getDoneFlags(array($requestedPlugin), $segment, $isSkipAggregationOfSubTables);
|
||||
foreach ($results as $result) {
|
||||
if ($idArchive === false
|
||||
&& in_array($result['name'], $namesRequestedPlugin)
|
||||
) {
|
||||
$idArchive = $result['idarchive'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $idArchive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries and returns archive IDs for a set of sites, periods, and a segment.
|
||||
*
|
||||
* @param array $siteIds
|
||||
* @param array $periods
|
||||
* @param Segment $segment
|
||||
* @param array $plugins List of plugin names for which data is being requested.
|
||||
* @param bool $isSkipAggregationOfSubTables Whether we are selecting an archive that may be partial (no sub-tables)
|
||||
* @return array Archive IDs are grouped by archive name and period range, ie,
|
||||
* array(
|
||||
* 'VisitsSummary.done' => array(
|
||||
* '2010-01-01' => array(1,2,3)
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
static public function getArchiveIds($siteIds, $periods, $segment, $plugins, $isSkipAggregationOfSubTables = false)
|
||||
{
|
||||
$getArchiveIdsSql = "SELECT idsite, name, date1, date2, MAX(idarchive) as idarchive
|
||||
FROM %s
|
||||
WHERE %s
|
||||
AND " . self::getNameCondition($plugins, $segment, $isSkipAggregationOfSubTables) . "
|
||||
AND idsite IN (" . implode(',', $siteIds) . ")
|
||||
GROUP BY idsite, date1, date2";
|
||||
|
||||
$monthToPeriods = array();
|
||||
foreach ($periods as $period) {
|
||||
/** @var Period $period */
|
||||
$table = ArchiveTableCreator::getNumericTable($period->getDateStart());
|
||||
$monthToPeriods[$table][] = $period;
|
||||
}
|
||||
|
||||
// for every month within the archive query, select from numeric table
|
||||
$result = array();
|
||||
foreach ($monthToPeriods as $table => $periods) {
|
||||
$firstPeriod = reset($periods);
|
||||
|
||||
$bind = array();
|
||||
|
||||
if ($firstPeriod instanceof Range) {
|
||||
$dateCondition = "period = ? AND date1 = ? AND date2 = ?";
|
||||
$bind[] = $firstPeriod->getId();
|
||||
$bind[] = $firstPeriod->getDateStart()->toString('Y-m-d');
|
||||
$bind[] = $firstPeriod->getDateEnd()->toString('Y-m-d');
|
||||
} else {
|
||||
// we assume there is no range date in $periods
|
||||
$dateCondition = '(';
|
||||
|
||||
foreach ($periods as $period) {
|
||||
if (strlen($dateCondition) > 1) {
|
||||
$dateCondition .= ' OR ';
|
||||
}
|
||||
|
||||
$dateCondition .= "(period = ? AND date1 = ? AND date2 = ?)";
|
||||
$bind[] = $period->getId();
|
||||
$bind[] = $period->getDateStart()->toString('Y-m-d');
|
||||
$bind[] = $period->getDateEnd()->toString('Y-m-d');
|
||||
}
|
||||
|
||||
$dateCondition .= ')';
|
||||
}
|
||||
|
||||
$sql = sprintf($getArchiveIdsSql, $table, $dateCondition);
|
||||
|
||||
// get the archive IDs
|
||||
foreach (Db::fetchAll($sql, $bind) as $row) {
|
||||
$archiveName = $row['name'];
|
||||
|
||||
//FIXMEA duplicate with Archive.php
|
||||
$dateStr = $row['date1'] . "," . $row['date2'];
|
||||
|
||||
$result[$archiveName][$dateStr][] = $row['idarchive'];
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries and returns archive data using a set of archive IDs.
|
||||
*
|
||||
* @param array $archiveIds The IDs of the archives to get data from.
|
||||
* @param array $recordNames The names of the data to retrieve (ie, nb_visits, nb_actions, etc.)
|
||||
* @param string $archiveDataType The archive data type (either, 'blob' or 'numeric').
|
||||
* @param bool $loadAllSubtables Whether to pre-load all subtables
|
||||
* @throws Exception
|
||||
* @return array
|
||||
*/
|
||||
static public function getArchiveData($archiveIds, $recordNames, $archiveDataType, $loadAllSubtables)
|
||||
{
|
||||
// create the SQL to select archive data
|
||||
$inNames = Common::getSqlStringFieldsArray($recordNames);
|
||||
if ($loadAllSubtables) {
|
||||
$name = reset($recordNames);
|
||||
|
||||
// select blobs w/ name like "$name_[0-9]+" w/o using RLIKE
|
||||
$nameEnd = strlen($name) + 2;
|
||||
$whereNameIs = "(name = ?
|
||||
OR (name LIKE ?
|
||||
AND SUBSTRING(name, $nameEnd, 1) >= '0'
|
||||
AND SUBSTRING(name, $nameEnd, 1) <= '9') )";
|
||||
$bind = array($name, $name . '%');
|
||||
} else {
|
||||
$whereNameIs = "name IN ($inNames)";
|
||||
$bind = array_values($recordNames);
|
||||
}
|
||||
|
||||
$getValuesSql = "SELECT value, name, idsite, date1, date2, ts_archived
|
||||
FROM %s
|
||||
WHERE idarchive IN (%s)
|
||||
AND " . $whereNameIs;
|
||||
|
||||
// get data from every table we're querying
|
||||
$rows = array();
|
||||
foreach ($archiveIds as $period => $ids) {
|
||||
if (empty($ids)) {
|
||||
throw new Exception("Unexpected: id archive not found for period '$period' '");
|
||||
}
|
||||
// $period = "2009-01-04,2009-01-04",
|
||||
$date = Date::factory(substr($period, 0, 10));
|
||||
if ($archiveDataType == 'numeric') {
|
||||
$table = ArchiveTableCreator::getNumericTable($date);
|
||||
} else {
|
||||
$table = ArchiveTableCreator::getBlobTable($date);
|
||||
}
|
||||
$sql = sprintf($getValuesSql, $table, implode(',', $ids));
|
||||
$dataRows = Db::fetchAll($sql, $bind);
|
||||
foreach ($dataRows as $row) {
|
||||
$rows[] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the SQL condition used to find successfully completed archives that
|
||||
* this instance is querying for.
|
||||
*
|
||||
* @param array $plugins
|
||||
* @param Segment $segment
|
||||
* @param bool $isSkipAggregationOfSubTables
|
||||
* @return string
|
||||
*/
|
||||
static private function getNameCondition(array $plugins, Segment $segment, $isSkipAggregationOfSubTables)
|
||||
{
|
||||
// the flags used to tell how the archiving process for a specific archive was completed,
|
||||
// if it was completed
|
||||
$doneFlags = Rules::getDoneFlags($plugins, $segment, $isSkipAggregationOfSubTables);
|
||||
|
||||
$allDoneFlags = "'" . implode("','", $doneFlags) . "'";
|
||||
|
||||
// create the SQL to find archives that are DONE
|
||||
return "((name IN ($allDoneFlags)) AND " .
|
||||
" (value = '" . ArchiveWriter::DONE_OK . "' OR " .
|
||||
" value = '" . ArchiveWriter::DONE_OK_TEMPORARY . "'))";
|
||||
}
|
||||
|
||||
static public function purgeOutdatedArchives(Date $dateStart)
|
||||
{
|
||||
$purgeArchivesOlderThan = Rules::shouldPurgeOutdatedArchives($dateStart);
|
||||
if (!$purgeArchivesOlderThan) {
|
||||
return;
|
||||
}
|
||||
|
||||
$idArchivesToDelete = self::getTemporaryArchiveIdsOlderThan($dateStart, $purgeArchivesOlderThan);
|
||||
if (!empty($idArchivesToDelete)) {
|
||||
self::deleteArchiveIds($dateStart, $idArchivesToDelete);
|
||||
}
|
||||
self::deleteArchivesWithPeriodRange($dateStart);
|
||||
|
||||
Log::debug("Purging temporary archives: done [ purged archives older than %s in %s ] [Deleted IDs: %s]",
|
||||
$purgeArchivesOlderThan, $dateStart->toString("Y-m"), implode(',', $idArchivesToDelete));
|
||||
}
|
||||
|
||||
/*
|
||||
* Deleting "Custom Date Range" reports after 1 day, since they can be re-processed and would take up un-necessary space
|
||||
*/
|
||||
protected static function deleteArchivesWithPeriodRange(Date $date)
|
||||
{
|
||||
$query = "DELETE FROM %s WHERE period = ? AND ts_archived < ?";
|
||||
|
||||
$yesterday = Date::factory('yesterday')->getDateTime();
|
||||
$bind = array(Piwik::$idPeriods['range'], $yesterday);
|
||||
$numericTable = ArchiveTableCreator::getNumericTable($date);
|
||||
Db::query(sprintf($query, $numericTable), $bind);
|
||||
Log::debug("Purging Custom Range archives: done [ purged archives older than %s from %s / blob ]", $yesterday, $numericTable);
|
||||
try {
|
||||
Db::query(sprintf($query, ArchiveTableCreator::getBlobTable($date)), $bind);
|
||||
} catch (Exception $e) {
|
||||
// Individual blob tables could be missing
|
||||
}
|
||||
}
|
||||
|
||||
protected static function deleteArchiveIds(Date $date, $idArchivesToDelete)
|
||||
{
|
||||
$query = "DELETE FROM %s WHERE idarchive IN (" . implode(',', $idArchivesToDelete) . ")";
|
||||
|
||||
Db::query(sprintf($query, ArchiveTableCreator::getNumericTable($date)));
|
||||
try {
|
||||
Db::query(sprintf($query, ArchiveTableCreator::getBlobTable($date)));
|
||||
} catch (Exception $e) {
|
||||
// Individual blob tables could be missing
|
||||
}
|
||||
}
|
||||
|
||||
protected static function getTemporaryArchiveIdsOlderThan(Date $date, $purgeArchivesOlderThan)
|
||||
{
|
||||
$query = "SELECT idarchive
|
||||
FROM " . ArchiveTableCreator::getNumericTable($date) . "
|
||||
WHERE name LIKE 'done%'
|
||||
AND (( value = " . ArchiveWriter::DONE_OK_TEMPORARY . "
|
||||
AND ts_archived < ?)
|
||||
OR value = " . ArchiveWriter::DONE_ERROR . ")";
|
||||
|
||||
$result = Db::fetchAll($query, array($purgeArchivesOlderThan));
|
||||
$idArchivesToDelete = array();
|
||||
if (!empty($result)) {
|
||||
foreach ($result as $row) {
|
||||
$idArchivesToDelete[] = $row['idarchive'];
|
||||
}
|
||||
}
|
||||
return $idArchivesToDelete;
|
||||
}
|
||||
}
|
||||
119
www/analytics/core/DataAccess/ArchiveTableCreator.php
Normal file
119
www/analytics/core/DataAccess/ArchiveTableCreator.php
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Piwik\DataAccess;
|
||||
|
||||
use Exception;
|
||||
use Piwik\Common;
|
||||
use Piwik\Date;
|
||||
use Piwik\Db;
|
||||
use Piwik\DbHelper;
|
||||
|
||||
class ArchiveTableCreator
|
||||
{
|
||||
const NUMERIC_TABLE = "numeric";
|
||||
|
||||
const BLOB_TABLE = "blob";
|
||||
|
||||
static public $tablesAlreadyInstalled = null;
|
||||
|
||||
static public function getNumericTable(Date $date)
|
||||
{
|
||||
return self::getTable($date, self::NUMERIC_TABLE);
|
||||
}
|
||||
|
||||
static public function getBlobTable(Date $date)
|
||||
{
|
||||
return self::getTable($date, self::BLOB_TABLE);
|
||||
}
|
||||
|
||||
static protected function getTable(Date $date, $type)
|
||||
{
|
||||
$tableNamePrefix = "archive_" . $type;
|
||||
$tableName = $tableNamePrefix . "_" . $date->toString('Y_m');
|
||||
$tableName = Common::prefixTable($tableName);
|
||||
self::createArchiveTablesIfAbsent($tableName, $tableNamePrefix);
|
||||
return $tableName;
|
||||
}
|
||||
|
||||
static protected function createArchiveTablesIfAbsent($tableName, $tableNamePrefix)
|
||||
{
|
||||
if (is_null(self::$tablesAlreadyInstalled)) {
|
||||
self::refreshTableList();
|
||||
}
|
||||
|
||||
if (!in_array($tableName, self::$tablesAlreadyInstalled)) {
|
||||
$db = Db::get();
|
||||
$sql = DbHelper::getTableCreateSql($tableNamePrefix);
|
||||
|
||||
// replace table name template by real name
|
||||
$tableNamePrefix = Common::prefixTable($tableNamePrefix);
|
||||
$sql = str_replace($tableNamePrefix, $tableName, $sql);
|
||||
try {
|
||||
$db->query($sql);
|
||||
} catch (Exception $e) {
|
||||
// accept mysql error 1050: table already exists, throw otherwise
|
||||
if (!$db->isErrNo($e, '1050')) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
self::$tablesAlreadyInstalled[] = $tableName;
|
||||
}
|
||||
}
|
||||
|
||||
static public function clear()
|
||||
{
|
||||
self::$tablesAlreadyInstalled = null;
|
||||
}
|
||||
|
||||
static public function refreshTableList($forceReload = false)
|
||||
{
|
||||
self::$tablesAlreadyInstalled = DbHelper::getTablesInstalled($forceReload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all table names archive_*
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
static public function getTablesArchivesInstalled()
|
||||
{
|
||||
if (is_null(self::$tablesAlreadyInstalled)) {
|
||||
self::refreshTableList();
|
||||
}
|
||||
|
||||
$archiveTables = array();
|
||||
foreach (self::$tablesAlreadyInstalled as $table) {
|
||||
if (strpos($table, 'archive_numeric_') !== false
|
||||
|| strpos($table, 'archive_blob_') !== false
|
||||
) {
|
||||
$archiveTables[] = $table;
|
||||
}
|
||||
}
|
||||
return $archiveTables;
|
||||
}
|
||||
|
||||
static public function getDateFromTableName($tableName)
|
||||
{
|
||||
$tableName = Common::unprefixTable($tableName);
|
||||
$date = str_replace(array('archive_numeric_', 'archive_blob_'), '', $tableName);
|
||||
return $date;
|
||||
}
|
||||
|
||||
static public function getTypeFromTableName($tableName)
|
||||
{
|
||||
if (strpos($tableName, 'archive_numeric_') !== false) {
|
||||
return self::NUMERIC_TABLE;
|
||||
}
|
||||
if (strpos($tableName, 'archive_blob_') !== false) {
|
||||
return self::BLOB_TABLE;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
317
www/analytics/core/DataAccess/ArchiveWriter.php
Normal file
317
www/analytics/core/DataAccess/ArchiveWriter.php
Normal file
|
|
@ -0,0 +1,317 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataAccess;
|
||||
|
||||
use Exception;
|
||||
use Piwik\ArchiveProcessor\Rules;
|
||||
use Piwik\ArchiveProcessor;
|
||||
use Piwik\Common;
|
||||
|
||||
use Piwik\Config;
|
||||
use Piwik\Db;
|
||||
use Piwik\Db\BatchInsert;
|
||||
use Piwik\Log;
|
||||
use Piwik\Period;
|
||||
use Piwik\Segment;
|
||||
use Piwik\SettingsPiwik;
|
||||
|
||||
/**
|
||||
* This class is used to create a new Archive.
|
||||
* An Archive is a set of reports (numeric and data tables).
|
||||
* New data can be inserted in the archive with insertRecord/insertBulkRecords
|
||||
*/
|
||||
class ArchiveWriter
|
||||
{
|
||||
const PREFIX_SQL_LOCK = "locked_";
|
||||
/**
|
||||
* Flag stored at the end of the archiving
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const DONE_OK = 1;
|
||||
/**
|
||||
* Flag stored at the start of the archiving
|
||||
* When requesting an Archive, we make sure that non-finished archive are not considered valid
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const DONE_ERROR = 2;
|
||||
/**
|
||||
* Flag indicates the archive is over a period that is not finished, eg. the current day, current week, etc.
|
||||
* Archives flagged will be regularly purged from the DB.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const DONE_OK_TEMPORARY = 3;
|
||||
|
||||
protected $fields = array('idarchive',
|
||||
'idsite',
|
||||
'date1',
|
||||
'date2',
|
||||
'period',
|
||||
'ts_archived',
|
||||
'name',
|
||||
'value');
|
||||
|
||||
public function __construct(ArchiveProcessor\Parameters $params, $isArchiveTemporary)
|
||||
{
|
||||
$this->idArchive = false;
|
||||
$this->idSite = $params->getSite()->getId();
|
||||
$this->segment = $params->getSegment();
|
||||
$this->period = $params->getPeriod();
|
||||
$idSites = array($this->idSite);
|
||||
$this->doneFlag = Rules::getDoneStringFlagFor($idSites, $this->segment, $this->period->getLabel(), $params->getRequestedPlugin(), $params->isSkipAggregationOfSubTables());
|
||||
$this->isArchiveTemporary = $isArchiveTemporary;
|
||||
|
||||
$this->dateStart = $this->period->getDateStart();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param string[] $values
|
||||
*/
|
||||
public function insertBlobRecord($name, $values)
|
||||
{
|
||||
if (is_array($values)) {
|
||||
$clean = array();
|
||||
foreach ($values as $id => $value) {
|
||||
// for the parent Table we keep the name
|
||||
// for example for the Table of searchEngines we keep the name 'referrer_search_engine'
|
||||
// but for the child table of 'Google' which has the ID = 9 the name would be 'referrer_search_engine_9'
|
||||
$newName = $name;
|
||||
if ($id != 0) {
|
||||
//FIXMEA: refactor
|
||||
$newName = $name . '_' . $id;
|
||||
}
|
||||
|
||||
$value = $this->compress($value);
|
||||
$clean[] = array($newName, $value);
|
||||
}
|
||||
$this->insertBulkRecords($clean);
|
||||
return;
|
||||
}
|
||||
|
||||
$values = $this->compress($values);
|
||||
$this->insertRecord($name, $values);
|
||||
}
|
||||
|
||||
public function getIdArchive()
|
||||
{
|
||||
if ($this->idArchive === false) {
|
||||
throw new Exception("Must call allocateNewArchiveId() first");
|
||||
}
|
||||
return $this->idArchive;
|
||||
}
|
||||
|
||||
public function initNewArchive()
|
||||
{
|
||||
$this->allocateNewArchiveId();
|
||||
$this->logArchiveStatusAsIncomplete();
|
||||
}
|
||||
|
||||
public function finalizeArchive()
|
||||
{
|
||||
$this->deletePreviousArchiveStatus();
|
||||
$this->logArchiveStatusAsFinal();
|
||||
}
|
||||
|
||||
static protected function compress($data)
|
||||
{
|
||||
if (Db::get()->hasBlobDataType()) {
|
||||
return gzcompress($data);
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function getArchiveLockName()
|
||||
{
|
||||
$numericTable = $this->getTableNumeric();
|
||||
$dbLockName = "allocateNewArchiveId.$numericTable";
|
||||
return $dbLockName;
|
||||
}
|
||||
|
||||
protected function acquireArchiveTableLock()
|
||||
{
|
||||
$dbLockName = $this->getArchiveLockName();
|
||||
if (Db::getDbLock($dbLockName, $maxRetries = 30) === false) {
|
||||
throw new Exception("allocateNewArchiveId: Cannot get named lock $dbLockName.");
|
||||
}
|
||||
}
|
||||
|
||||
protected function releaseArchiveTableLock()
|
||||
{
|
||||
$dbLockName = $this->getArchiveLockName();
|
||||
Db::releaseDbLock($dbLockName);
|
||||
}
|
||||
|
||||
protected function allocateNewArchiveId()
|
||||
{
|
||||
$this->idArchive = $this->insertNewArchiveId();
|
||||
return $this->idArchive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Locks the archive table to generate a new archive ID.
|
||||
*
|
||||
* We lock to make sure that
|
||||
* if several archiving processes are running at the same time (for different websites and/or periods)
|
||||
* then they will each use a unique archive ID.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function insertNewArchiveId()
|
||||
{
|
||||
$numericTable = $this->getTableNumeric();
|
||||
$idSite = $this->idSite;
|
||||
|
||||
$this->acquireArchiveTableLock();
|
||||
|
||||
$locked = self::PREFIX_SQL_LOCK . Common::generateUniqId();
|
||||
$date = date("Y-m-d H:i:s");
|
||||
$insertSql = "INSERT INTO $numericTable "
|
||||
. " SELECT IFNULL( MAX(idarchive), 0 ) + 1,
|
||||
'" . $locked . "',
|
||||
" . (int)$idSite . ",
|
||||
'" . $date . "',
|
||||
'" . $date . "',
|
||||
0,
|
||||
'" . $date . "',
|
||||
0 "
|
||||
. " FROM $numericTable as tb1";
|
||||
Db::get()->exec($insertSql);
|
||||
|
||||
$this->releaseArchiveTableLock();
|
||||
|
||||
$selectIdSql = "SELECT idarchive FROM $numericTable WHERE name = ? LIMIT 1";
|
||||
$id = Db::get()->fetchOne($selectIdSql, $locked);
|
||||
return $id;
|
||||
}
|
||||
|
||||
protected function logArchiveStatusAsIncomplete()
|
||||
{
|
||||
$statusWhileProcessing = self::DONE_ERROR;
|
||||
$this->insertRecord($this->doneFlag, $statusWhileProcessing);
|
||||
}
|
||||
|
||||
protected function deletePreviousArchiveStatus()
|
||||
{
|
||||
// without advisory lock here, the DELETE would acquire Exclusive Lock
|
||||
$this->acquireArchiveTableLock();
|
||||
|
||||
Db::query("DELETE FROM " . $this->getTableNumeric() . "
|
||||
WHERE idarchive = ? AND (name = '" . $this->doneFlag
|
||||
. "' OR name LIKE '" . self::PREFIX_SQL_LOCK . "%')",
|
||||
array($this->getIdArchive())
|
||||
);
|
||||
|
||||
$this->releaseArchiveTableLock();
|
||||
}
|
||||
|
||||
protected function logArchiveStatusAsFinal()
|
||||
{
|
||||
$status = self::DONE_OK;
|
||||
if ($this->isArchiveTemporary) {
|
||||
$status = self::DONE_OK_TEMPORARY;
|
||||
}
|
||||
$this->insertRecord($this->doneFlag, $status);
|
||||
}
|
||||
|
||||
protected function insertBulkRecords($records)
|
||||
{
|
||||
// Using standard plain INSERT if there is only one record to insert
|
||||
if ($DEBUG_DO_NOT_USE_BULK_INSERT = false
|
||||
|| count($records) == 1
|
||||
) {
|
||||
foreach ($records as $record) {
|
||||
$this->insertRecord($record[0], $record[1]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
$bindSql = $this->getInsertRecordBind();
|
||||
$values = array();
|
||||
|
||||
$valueSeen = false;
|
||||
foreach ($records as $record) {
|
||||
// don't record zero
|
||||
if (empty($record[1])) continue;
|
||||
|
||||
$bind = $bindSql;
|
||||
$bind[] = $record[0]; // name
|
||||
$bind[] = $record[1]; // value
|
||||
$values[] = $bind;
|
||||
|
||||
$valueSeen = $record[1];
|
||||
}
|
||||
if (empty($values)) return true;
|
||||
|
||||
$tableName = $this->getTableNameToInsert($valueSeen);
|
||||
BatchInsert::tableInsertBatch($tableName, $this->getInsertFields(), $values);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a record in the right table (either NUMERIC or BLOB)
|
||||
*
|
||||
* @param string $name
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function insertRecord($name, $value)
|
||||
{
|
||||
if ($this->isRecordZero($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$tableName = $this->getTableNameToInsert($value);
|
||||
|
||||
// duplicate idarchives are Ignored, see http://dev.piwik.org/trac/ticket/987
|
||||
$query = "INSERT IGNORE INTO " . $tableName . "
|
||||
(" . implode(", ", $this->getInsertFields()) . ")
|
||||
VALUES (?,?,?,?,?,?,?,?)";
|
||||
$bindSql = $this->getInsertRecordBind();
|
||||
$bindSql[] = $name;
|
||||
$bindSql[] = $value;
|
||||
Db::query($query, $bindSql);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function getInsertRecordBind()
|
||||
{
|
||||
return array($this->getIdArchive(),
|
||||
$this->idSite,
|
||||
$this->dateStart->toString('Y-m-d'),
|
||||
$this->period->getDateEnd()->toString('Y-m-d'),
|
||||
$this->period->getId(),
|
||||
date("Y-m-d H:i:s"));
|
||||
}
|
||||
|
||||
protected function getTableNameToInsert($value)
|
||||
{
|
||||
if (is_numeric($value)) {
|
||||
return $this->getTableNumeric();
|
||||
}
|
||||
return ArchiveTableCreator::getBlobTable($this->dateStart);
|
||||
}
|
||||
|
||||
protected function getTableNumeric()
|
||||
{
|
||||
return ArchiveTableCreator::getNumericTable($this->dateStart);
|
||||
}
|
||||
|
||||
protected function getInsertFields()
|
||||
{
|
||||
return $this->fields;
|
||||
}
|
||||
|
||||
protected function isRecordZero($value)
|
||||
{
|
||||
return ($value === '0' || $value === false || $value === 0 || $value === 0.0);
|
||||
}
|
||||
}
|
||||
880
www/analytics/core/DataAccess/LogAggregator.php
Normal file
880
www/analytics/core/DataAccess/LogAggregator.php
Normal file
|
|
@ -0,0 +1,880 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataAccess;
|
||||
|
||||
use PDOStatement;
|
||||
use Piwik\ArchiveProcessor\Parameters;
|
||||
use Piwik\Common;
|
||||
use Piwik\DataArray;
|
||||
use Piwik\Db;
|
||||
|
||||
use Piwik\Metrics;
|
||||
use Piwik\Tracker\GoalManager;
|
||||
|
||||
/**
|
||||
* Contains methods that calculate metrics by aggregating log data (visits, actions, conversions,
|
||||
* ecommerce items).
|
||||
*
|
||||
* You can use the methods in this class within {@link Piwik\Plugin\Archiver Archiver} descendants
|
||||
* to aggregate log data without having to write SQL queries.
|
||||
*
|
||||
* ### Aggregation Dimension
|
||||
*
|
||||
* All aggregation methods accept a **dimension** parameter. These parameters are important as
|
||||
* they control how rows in a table are aggregated together.
|
||||
*
|
||||
* A **_dimension_** is just a table column. Rows that have the same values for these columns are
|
||||
* aggregated together. The result of these aggregations is a set of metrics for every recorded value
|
||||
* of a **dimension**.
|
||||
*
|
||||
* _Note: A dimension is essentially the same as a **GROUP BY** field._
|
||||
*
|
||||
* ### Examples
|
||||
*
|
||||
* **Aggregating visit data**
|
||||
*
|
||||
* $archiveProcessor = // ...
|
||||
* $logAggregator = $archiveProcessor->getLogAggregator();
|
||||
*
|
||||
* // get metrics for every used browser language of all visits by returning visitors
|
||||
* $query = $logAggregator->queryVisitsByDimension(
|
||||
* $dimensions = array('log_visit.location_browser_lang'),
|
||||
* $where = 'log_visit.visitor_returning = 1',
|
||||
*
|
||||
* // also count visits for each browser language that are not located in the US
|
||||
* $additionalSelects = array('sum(case when log_visit.location_country <> 'us' then 1 else 0 end) as nonus'),
|
||||
*
|
||||
* // we're only interested in visits, unique visitors & actions, so don't waste time calculating anything else
|
||||
* $metrics = array(Metrics::INDEX_NB_UNIQ_VISITORS, Metrics::INDEX_NB_VISITS, Metrics::INDEX_NB_ACTIONS),
|
||||
* );
|
||||
* if ($query === false) {
|
||||
* return;
|
||||
* }
|
||||
*
|
||||
* while ($row = $query->fetch()) {
|
||||
* $uniqueVisitors = $row[Metrics::INDEX_NB_UNIQ_VISITORS];
|
||||
* $visits = $row[Metrics::INDEX_NB_VISITS];
|
||||
* $actions = $row[Metrics::INDEX_NB_ACTIONS];
|
||||
*
|
||||
* // ... do something w/ calculated metrics ...
|
||||
* }
|
||||
*
|
||||
* **Aggregating conversion data**
|
||||
*
|
||||
* $archiveProcessor = // ...
|
||||
* $logAggregator = $archiveProcessor->getLogAggregator();
|
||||
*
|
||||
* // get metrics for ecommerce conversions for each country
|
||||
* $query = $logAggregator->queryConversionsByDimension(
|
||||
* $dimensions = array('log_conversion.location_country'),
|
||||
* $where = 'log_conversion.idgoal = 0', // 0 is the special ecommerceOrder idGoal value in the table
|
||||
*
|
||||
* // also calculate average tax and max shipping per country
|
||||
* $additionalSelects = array(
|
||||
* 'AVG(log_conversion.revenue_tax) as avg_tax',
|
||||
* 'MAX(log_conversion.revenue_shipping) as max_shipping'
|
||||
* )
|
||||
* );
|
||||
* if ($query === false) {
|
||||
* return;
|
||||
* }
|
||||
*
|
||||
* while ($row = $query->fetch()) {
|
||||
* $country = $row['location_country'];
|
||||
* $numEcommerceSales = $row[Metrics::INDEX_GOAL_NB_CONVERSIONS];
|
||||
* $numVisitsWithEcommerceSales = $row[Metrics::INDEX_GOAL_NB_VISITS_CONVERTED];
|
||||
* $avgTaxForCountry = $country['avg_tax'];
|
||||
* $maxShippingForCountry = $country['max_shipping'];
|
||||
*
|
||||
* // ... do something with aggregated data ...
|
||||
* }
|
||||
*/
|
||||
class LogAggregator
|
||||
{
|
||||
const LOG_VISIT_TABLE = 'log_visit';
|
||||
|
||||
const LOG_ACTIONS_TABLE = 'log_link_visit_action';
|
||||
|
||||
const LOG_CONVERSION_TABLE = "log_conversion";
|
||||
|
||||
const REVENUE_SUBTOTAL_FIELD = 'revenue_subtotal';
|
||||
|
||||
const REVENUE_TAX_FIELD = 'revenue_tax';
|
||||
|
||||
const REVENUE_SHIPPING_FIELD = 'revenue_shipping';
|
||||
|
||||
const REVENUE_DISCOUNT_FIELD = 'revenue_discount';
|
||||
|
||||
const TOTAL_REVENUE_FIELD = 'revenue';
|
||||
|
||||
const ITEMS_COUNT_FIELD = "items";
|
||||
|
||||
const CONVERSION_DATETIME_FIELD = "server_time";
|
||||
|
||||
const ACTION_DATETIME_FIELD = "server_time";
|
||||
|
||||
const VISIT_DATETIME_FIELD = 'visit_last_action_time';
|
||||
|
||||
const IDGOAL_FIELD = 'idgoal';
|
||||
|
||||
const FIELDS_SEPARATOR = ", \n\t\t\t";
|
||||
|
||||
/** @var \Piwik\Date */
|
||||
protected $dateStart;
|
||||
|
||||
/** @var \Piwik\Date */
|
||||
protected $dateEnd;
|
||||
|
||||
/** @var \Piwik\Site */
|
||||
protected $site;
|
||||
|
||||
/** @var \Piwik\Segment */
|
||||
protected $segment;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param \Piwik\ArchiveProcessor\Parameters $params
|
||||
*/
|
||||
public function __construct(Parameters $params)
|
||||
{
|
||||
$this->dateStart = $params->getDateStart();
|
||||
$this->dateEnd = $params->getDateEnd();
|
||||
$this->segment = $params->getSegment();
|
||||
$this->site = $params->getSite();
|
||||
}
|
||||
|
||||
public function generateQuery($select, $from, $where, $groupBy, $orderBy)
|
||||
{
|
||||
$bind = $this->getBindDatetimeSite();
|
||||
$query = $this->segment->getSelectQuery($select, $from, $where, $bind, $orderBy, $groupBy);
|
||||
return $query;
|
||||
}
|
||||
|
||||
protected function getVisitsMetricFields()
|
||||
{
|
||||
return array(
|
||||
Metrics::INDEX_NB_UNIQ_VISITORS => "count(distinct " . self::LOG_VISIT_TABLE . ".idvisitor)",
|
||||
Metrics::INDEX_NB_VISITS => "count(*)",
|
||||
Metrics::INDEX_NB_ACTIONS => "sum(" . self::LOG_VISIT_TABLE . ".visit_total_actions)",
|
||||
Metrics::INDEX_MAX_ACTIONS => "max(" . self::LOG_VISIT_TABLE . ".visit_total_actions)",
|
||||
Metrics::INDEX_SUM_VISIT_LENGTH => "sum(" . self::LOG_VISIT_TABLE . ".visit_total_time)",
|
||||
Metrics::INDEX_BOUNCE_COUNT => "sum(case " . self::LOG_VISIT_TABLE . ".visit_total_actions when 1 then 1 when 0 then 1 else 0 end)",
|
||||
Metrics::INDEX_NB_VISITS_CONVERTED => "sum(case " . self::LOG_VISIT_TABLE . ".visit_goal_converted when 1 then 1 else 0 end)",
|
||||
);
|
||||
}
|
||||
|
||||
static public function getConversionsMetricFields()
|
||||
{
|
||||
return array(
|
||||
Metrics::INDEX_GOAL_NB_CONVERSIONS => "count(*)",
|
||||
Metrics::INDEX_GOAL_NB_VISITS_CONVERTED => "count(distinct " . self::LOG_CONVERSION_TABLE . ".idvisit)",
|
||||
Metrics::INDEX_GOAL_REVENUE => self::getSqlConversionRevenueSum(self::TOTAL_REVENUE_FIELD),
|
||||
Metrics::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL => self::getSqlConversionRevenueSum(self::REVENUE_SUBTOTAL_FIELD),
|
||||
Metrics::INDEX_GOAL_ECOMMERCE_REVENUE_TAX => self::getSqlConversionRevenueSum(self::REVENUE_TAX_FIELD),
|
||||
Metrics::INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING => self::getSqlConversionRevenueSum(self::REVENUE_SHIPPING_FIELD),
|
||||
Metrics::INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT => self::getSqlConversionRevenueSum(self::REVENUE_DISCOUNT_FIELD),
|
||||
Metrics::INDEX_GOAL_ECOMMERCE_ITEMS => "SUM(" . self::LOG_CONVERSION_TABLE . "." . self::ITEMS_COUNT_FIELD . ")",
|
||||
);
|
||||
}
|
||||
|
||||
static private function getSqlConversionRevenueSum($field)
|
||||
{
|
||||
return self::getSqlRevenue('SUM(' . self::LOG_CONVERSION_TABLE . '.' . $field . ')');
|
||||
}
|
||||
|
||||
static public function getSqlRevenue($field)
|
||||
{
|
||||
return "ROUND(" . $field . "," . GoalManager::REVENUE_PRECISION . ")";
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function that returns an array with common metrics for a given log_visit field distinct values.
|
||||
*
|
||||
* The statistics returned are:
|
||||
* - number of unique visitors
|
||||
* - number of visits
|
||||
* - number of actions
|
||||
* - maximum number of action for a visit
|
||||
* - sum of the visits' length in sec
|
||||
* - count of bouncing visits (visits with one page view)
|
||||
*
|
||||
* For example if $dimension = 'config_os' it will return the statistics for every distinct Operating systems
|
||||
* The returned array will have a row per distinct operating systems,
|
||||
* and a column per stat (nb of visits, max actions, etc)
|
||||
*
|
||||
* 'label' Metrics::INDEX_NB_UNIQ_VISITORS Metrics::INDEX_NB_VISITS etc.
|
||||
* Linux 27 66 ...
|
||||
* Windows XP 12 ...
|
||||
* Mac OS 15 36 ...
|
||||
*
|
||||
* @param string $dimension Table log_visit field name to be use to compute common stats
|
||||
* @return DataArray
|
||||
*/
|
||||
public function getMetricsFromVisitByDimension($dimension)
|
||||
{
|
||||
if (!is_array($dimension)) {
|
||||
$dimension = array($dimension);
|
||||
}
|
||||
if (count($dimension) == 1) {
|
||||
$dimension = array("label" => reset($dimension));
|
||||
}
|
||||
$query = $this->queryVisitsByDimension($dimension);
|
||||
$metrics = new DataArray();
|
||||
while ($row = $query->fetch()) {
|
||||
$metrics->sumMetricsVisits($row["label"], $row);
|
||||
}
|
||||
return $metrics;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes and returns a query aggregating visit logs, optionally grouping by some dimension. Returns
|
||||
* a DB statement that can be used to iterate over the result
|
||||
*
|
||||
* **Result Set**
|
||||
*
|
||||
* The following columns are in each row of the result set:
|
||||
*
|
||||
* - **{@link Piwik\Metrics::INDEX_NB_UNIQ_VISITORS}**: The total number of unique visitors in this group
|
||||
* of aggregated visits.
|
||||
* - **{@link Piwik\Metrics::INDEX_NB_VISITS}**: The total number of visits aggregated.
|
||||
* - **{@link Piwik\Metrics::INDEX_NB_ACTIONS}**: The total number of actions performed in this group of
|
||||
* aggregated visits.
|
||||
* - **{@link Piwik\Metrics::INDEX_MAX_ACTIONS}**: The maximum actions perfomred in one visit for this group of
|
||||
* visits.
|
||||
* - **{@link Piwik\Metrics::INDEX_SUM_VISIT_LENGTH}**: The total amount of time spent on the site for this
|
||||
* group of visits.
|
||||
* - **{@link Piwik\Metrics::INDEX_BOUNCE_COUNT}**: The total number of bounced visits in this group of
|
||||
* visits.
|
||||
* - **{@link Piwik\Metrics::INDEX_NB_VISITS_CONVERTED}**: The total number of visits for which at least one
|
||||
* conversion occurred, for this group of visits.
|
||||
*
|
||||
* Additional data can be selected by setting the `$additionalSelects` parameter.
|
||||
*
|
||||
* _Note: The metrics returned by this query can be customized by the `$metrics` parameter._
|
||||
*
|
||||
* @param array|string $dimensions `SELECT` fields (or just one field) that will be grouped by,
|
||||
* eg, `'referrer_name'` or `array('referrer_name', 'referrer_keyword')`.
|
||||
* The metrics retrieved from the query will be specific to combinations
|
||||
* of these fields. So if `array('referrer_name', 'referrer_keyword')`
|
||||
* is supplied, the query will aggregate visits for each referrer/keyword
|
||||
* combination.
|
||||
* @param bool|string $where Additional condition for the `WHERE` clause. Can be used to filter
|
||||
* the set of visits that are considered for aggregation.
|
||||
* @param array $additionalSelects Additional `SELECT` fields that are not included in the group by
|
||||
* clause. These can be aggregate expressions, eg, `SUM(somecol)`.
|
||||
* @param bool|array $metrics The set of metrics to calculate and return. If false, the query will select
|
||||
* all of them. The following values can be used:
|
||||
*
|
||||
* - {@link Piwik\Metrics::INDEX_NB_UNIQ_VISITORS}
|
||||
* - {@link Piwik\Metrics::INDEX_NB_VISITS}
|
||||
* - {@link Piwik\Metrics::INDEX_NB_ACTIONS}
|
||||
* - {@link Piwik\Metrics::INDEX_MAX_ACTIONS}
|
||||
* - {@link Piwik\Metrics::INDEX_SUM_VISIT_LENGTH}
|
||||
* - {@link Piwik\Metrics::INDEX_BOUNCE_COUNT}
|
||||
* - {@link Piwik\Metrics::INDEX_NB_VISITS_CONVERTED}
|
||||
* @param bool|\Piwik\RankingQuery $rankingQuery
|
||||
* A pre-configured ranking query instance that will be used to limit the result.
|
||||
* If set, the return value is the array returned by {@link Piwik\RankingQuery::execute()}.
|
||||
* @return mixed A Zend_Db_Statement if `$rankingQuery` isn't supplied, otherwise the result of
|
||||
* {@link Piwik\RankingQuery::execute()}. Read {@link queryVisitsByDimension() this}
|
||||
* to see what aggregate data is calculated by the query.
|
||||
* @api
|
||||
*/
|
||||
public function queryVisitsByDimension(array $dimensions = array(), $where = false, array $additionalSelects = array(),
|
||||
$metrics = false, $rankingQuery = false)
|
||||
{
|
||||
$tableName = self::LOG_VISIT_TABLE;
|
||||
$availableMetrics = $this->getVisitsMetricFields();
|
||||
|
||||
$select = $this->getSelectStatement($dimensions, $tableName, $additionalSelects, $availableMetrics, $metrics);
|
||||
$from = array($tableName);
|
||||
$where = $this->getWhereStatement($tableName, self::VISIT_DATETIME_FIELD, $where);
|
||||
$groupBy = $this->getGroupByStatement($dimensions, $tableName);
|
||||
$orderBy = false;
|
||||
|
||||
if ($rankingQuery) {
|
||||
$orderBy = '`' . Metrics::INDEX_NB_VISITS . '` DESC';
|
||||
}
|
||||
$query = $this->generateQuery($select, $from, $where, $groupBy, $orderBy);
|
||||
|
||||
if ($rankingQuery) {
|
||||
unset($availableMetrics[Metrics::INDEX_MAX_ACTIONS]);
|
||||
$sumColumns = array_keys($availableMetrics);
|
||||
if ($metrics) {
|
||||
$sumColumns = array_intersect($sumColumns, $metrics);
|
||||
}
|
||||
$rankingQuery->addColumn($sumColumns, 'sum');
|
||||
if ($this->isMetricRequested(Metrics::INDEX_MAX_ACTIONS, $metrics)) {
|
||||
$rankingQuery->addColumn(Metrics::INDEX_MAX_ACTIONS, 'max');
|
||||
}
|
||||
return $rankingQuery->execute($query['sql'], $query['bind']);
|
||||
}
|
||||
return $this->getDb()->query($query['sql'], $query['bind']);
|
||||
}
|
||||
|
||||
protected function getSelectsMetrics($metricsAvailable, $metricsRequested = false)
|
||||
{
|
||||
$selects = array();
|
||||
foreach ($metricsAvailable as $metricId => $statement) {
|
||||
if ($this->isMetricRequested($metricId, $metricsRequested)) {
|
||||
$aliasAs = $this->getSelectAliasAs($metricId);
|
||||
$selects[] = $statement . $aliasAs;
|
||||
}
|
||||
}
|
||||
return $selects;
|
||||
}
|
||||
|
||||
protected function getSelectStatement($dimensions, $tableName, $additionalSelects, array $availableMetrics, $requestedMetrics = false)
|
||||
{
|
||||
$dimensionsToSelect = $this->getDimensionsToSelect($dimensions, $additionalSelects);
|
||||
$selects = array_merge(
|
||||
$this->getSelectDimensions($dimensionsToSelect, $tableName),
|
||||
$this->getSelectsMetrics($availableMetrics, $requestedMetrics),
|
||||
!empty($additionalSelects) ? $additionalSelects : array()
|
||||
);
|
||||
$select = implode(self::FIELDS_SEPARATOR, $selects);
|
||||
return $select;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will return the subset of $dimensions that are not found in $additionalSelects
|
||||
*
|
||||
* @param $dimensions
|
||||
* @param array $additionalSelects
|
||||
* @return array
|
||||
*/
|
||||
protected function getDimensionsToSelect($dimensions, $additionalSelects)
|
||||
{
|
||||
if (empty($additionalSelects)) {
|
||||
return $dimensions;
|
||||
}
|
||||
$dimensionsToSelect = array();
|
||||
foreach ($dimensions as $selectAs => $dimension) {
|
||||
$asAlias = $this->getSelectAliasAs($dimension);
|
||||
foreach ($additionalSelects as $additionalSelect) {
|
||||
if (strpos($additionalSelect, $asAlias) === false) {
|
||||
$dimensionsToSelect[$selectAs] = $dimension;
|
||||
}
|
||||
}
|
||||
}
|
||||
$dimensionsToSelect = array_unique($dimensionsToSelect);
|
||||
return $dimensionsToSelect;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the dimensions array, where
|
||||
* (1) the table name is prepended to the field
|
||||
* (2) the "AS `label` " is appended to the field
|
||||
*
|
||||
* @param $dimensions
|
||||
* @param $tableName
|
||||
* @param bool $appendSelectAs
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getSelectDimensions($dimensions, $tableName, $appendSelectAs = true)
|
||||
{
|
||||
foreach ($dimensions as $selectAs => &$field) {
|
||||
$selectAsString = $field;
|
||||
if (!is_numeric($selectAs)) {
|
||||
$selectAsString = $selectAs;
|
||||
} else {
|
||||
// if function, do not alias or prefix
|
||||
if ($this->isFieldFunctionOrComplexExpression($field)) {
|
||||
$selectAsString = $appendSelectAs = false;
|
||||
}
|
||||
}
|
||||
$isKnownField = !in_array($field, array('referrer_data'));
|
||||
if ($selectAsString == $field
|
||||
&& $isKnownField
|
||||
) {
|
||||
$field = $this->prefixColumn($field, $tableName);
|
||||
}
|
||||
if ($appendSelectAs && $selectAsString) {
|
||||
$field = $this->prefixColumn($field, $tableName) . $this->getSelectAliasAs($selectAsString);
|
||||
}
|
||||
}
|
||||
return $dimensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefixes a column name with a table name if not already done.
|
||||
*
|
||||
* @param string $column eg, 'location_provider'
|
||||
* @param string $tableName eg, 'log_visit'
|
||||
* @return string eg, 'log_visit.location_provider'
|
||||
*/
|
||||
private function prefixColumn($column, $tableName)
|
||||
{
|
||||
if (strpos($column, '.') === false) {
|
||||
return $tableName . '.' . $column;
|
||||
} else {
|
||||
return $column;
|
||||
}
|
||||
}
|
||||
|
||||
protected function isFieldFunctionOrComplexExpression($field)
|
||||
{
|
||||
return strpos($field, "(") !== false
|
||||
|| strpos($field, "CASE") !== false;
|
||||
}
|
||||
|
||||
protected function getSelectAliasAs($metricId)
|
||||
{
|
||||
return " AS `" . $metricId . "`";
|
||||
}
|
||||
|
||||
protected function isMetricRequested($metricId, $metricsRequested)
|
||||
{
|
||||
return $metricsRequested === false
|
||||
|| in_array($metricId, $metricsRequested);
|
||||
}
|
||||
|
||||
protected function getWhereStatement($tableName, $datetimeField, $extraWhere = false)
|
||||
{
|
||||
$where = "$tableName.$datetimeField >= ?
|
||||
AND $tableName.$datetimeField <= ?
|
||||
AND $tableName.idsite = ?";
|
||||
if (!empty($extraWhere)) {
|
||||
$extraWhere = sprintf($extraWhere, $tableName, $tableName);
|
||||
$where .= ' AND ' . $extraWhere;
|
||||
}
|
||||
return $where;
|
||||
}
|
||||
|
||||
protected function getGroupByStatement($dimensions, $tableName)
|
||||
{
|
||||
$dimensions = $this->getSelectDimensions($dimensions, $tableName, $appendSelectAs = false);
|
||||
$groupBy = implode(", ", $dimensions);
|
||||
return $groupBy;
|
||||
}
|
||||
|
||||
protected function getBindDatetimeSite()
|
||||
{
|
||||
return array($this->dateStart->getDateStartUTC(), $this->dateEnd->getDateEndUTC(), $this->site->getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes and returns a query aggregating ecommerce item data (everything stored in the
|
||||
* **log\_conversion\_item** table) and returns a DB statement that can be used to iterate over the result
|
||||
*
|
||||
* <a name="queryEcommerceItems-result-set"></a>
|
||||
* **Result Set**
|
||||
*
|
||||
* Each row of the result set represents an aggregated group of ecommerce items. The following
|
||||
* columns are in each row of the result set:
|
||||
*
|
||||
* - **{@link Piwik\Metrics::INDEX_ECOMMERCE_ITEM_REVENUE}**: The total revenue for the group of items.
|
||||
* - **{@link Piwik\Metrics::INDEX_ECOMMERCE_ITEM_QUANTITY}**: The total number of items in this group.
|
||||
* - **{@link Piwik\Metrics::INDEX_ECOMMERCE_ITEM_PRICE}**: The total price for the group of items.
|
||||
* - **{@link Piwik\Metrics::INDEX_ECOMMERCE_ORDERS}**: The total number of orders this group of items
|
||||
* belongs to. This will be <= to the total number
|
||||
* of items in this group.
|
||||
* - **{@link Piwik\Metrics::INDEX_NB_VISITS}**: The total number of visits that caused these items to be logged.
|
||||
* - **ecommerceType**: Either {@link Piwik\Tracker\GoalManager::IDGOAL_CART} if the items in this group were
|
||||
* abandoned by a visitor, or {@link Piwik\Tracker\GoalManager::IDGOAL_ORDER} if they
|
||||
* were ordered by a visitor.
|
||||
*
|
||||
* **Limitations**
|
||||
*
|
||||
* Segmentation is not yet supported for this aggregation method.
|
||||
*
|
||||
* @param string $dimension One or more **log\_conversion\_item** columns to group aggregated data by.
|
||||
* Eg, `'idaction_sku'` or `'idaction_sku, idaction_category'`.
|
||||
* @return Zend_Db_Statement A statement object that can be used to iterate through the query's
|
||||
* result set. See [above](#queryEcommerceItems-result-set) to learn more
|
||||
* about what this query selects.
|
||||
* @api
|
||||
*/
|
||||
public function queryEcommerceItems($dimension)
|
||||
{
|
||||
$query = $this->generateQuery(
|
||||
// SELECT ...
|
||||
implode(
|
||||
', ',
|
||||
array(
|
||||
"log_action.name AS label",
|
||||
sprintf("log_conversion_item.%s AS labelIdAction", $dimension),
|
||||
sprintf(
|
||||
'%s AS `%d`',
|
||||
self::getSqlRevenue('SUM(log_conversion_item.quantity * log_conversion_item.price)'),
|
||||
Metrics::INDEX_ECOMMERCE_ITEM_REVENUE
|
||||
),
|
||||
sprintf(
|
||||
'%s AS `%d`',
|
||||
self::getSqlRevenue('SUM(log_conversion_item.quantity)'),
|
||||
Metrics::INDEX_ECOMMERCE_ITEM_QUANTITY
|
||||
),
|
||||
sprintf(
|
||||
'%s AS `%d`',
|
||||
self::getSqlRevenue('SUM(log_conversion_item.price)'),
|
||||
Metrics::INDEX_ECOMMERCE_ITEM_PRICE
|
||||
),
|
||||
sprintf(
|
||||
'COUNT(distinct log_conversion_item.idorder) AS `%d`',
|
||||
Metrics::INDEX_ECOMMERCE_ORDERS
|
||||
),
|
||||
sprintf(
|
||||
'COUNT(distinct log_conversion_item.idvisit) AS `%d`',
|
||||
Metrics::INDEX_NB_VISITS
|
||||
),
|
||||
sprintf(
|
||||
'CASE log_conversion_item.idorder WHEN \'0\' THEN %d ELSE %d END AS ecommerceType',
|
||||
GoalManager::IDGOAL_CART,
|
||||
GoalManager::IDGOAL_ORDER
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
// FROM ...
|
||||
array(
|
||||
"log_conversion_item",
|
||||
array(
|
||||
"table" => "log_action",
|
||||
"joinOn" => sprintf("log_conversion_item.%s = log_action.idaction", $dimension)
|
||||
)
|
||||
),
|
||||
|
||||
// WHERE ... AND ...
|
||||
implode(
|
||||
' AND ',
|
||||
array(
|
||||
'log_conversion_item.server_time >= ?',
|
||||
'log_conversion_item.server_time <= ?',
|
||||
'log_conversion_item.idsite = ?',
|
||||
'log_conversion_item.deleted = 0'
|
||||
)
|
||||
),
|
||||
|
||||
// GROUP BY ...
|
||||
sprintf(
|
||||
"ecommerceType, log_conversion_item.%s",
|
||||
$dimension
|
||||
),
|
||||
|
||||
// ORDER ...
|
||||
false
|
||||
);
|
||||
|
||||
return $this->getDb()->query($query['sql'], $query['bind']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes and returns a query aggregating action data (everything in the log_action table) and returns
|
||||
* a DB statement that can be used to iterate over the result
|
||||
*
|
||||
* <a name="queryActionsByDimension-result-set"></a>
|
||||
* **Result Set**
|
||||
*
|
||||
* Each row of the result set represents an aggregated group of actions. The following columns
|
||||
* are in each aggregate row:
|
||||
*
|
||||
* - **{@link Piwik\Metrics::INDEX_NB_UNIQ_VISITORS}**: The total number of unique visitors that performed
|
||||
* the actions in this group.
|
||||
* - **{@link Piwik\Metrics::INDEX_NB_VISITS}**: The total number of visits these actions belong to.
|
||||
* - **{@link Piwik\Metrics::INDEX_NB_ACTIONS}**: The total number of actions in this aggregate group.
|
||||
*
|
||||
* Additional data can be selected through the `$additionalSelects` parameter.
|
||||
*
|
||||
* _Note: The metrics calculated by this query can be customized by the `$metrics` parameter._
|
||||
*
|
||||
* @param array|string $dimensions One or more SELECT fields that will be used to group the log_action
|
||||
* rows by. This parameter determines which log_action rows will be
|
||||
* aggregated together.
|
||||
* @param bool|string $where Additional condition for the WHERE clause. Can be used to filter
|
||||
* the set of visits that are considered for aggregation.
|
||||
* @param array $additionalSelects Additional SELECT fields that are not included in the group by
|
||||
* clause. These can be aggregate expressions, eg, `SUM(somecol)`.
|
||||
* @param bool|array $metrics The set of metrics to calculate and return. If `false`, the query will select
|
||||
* all of them. The following values can be used:
|
||||
*
|
||||
* - {@link Piwik\Metrics::INDEX_NB_UNIQ_VISITORS}
|
||||
* - {@link Piwik\Metrics::INDEX_NB_VISITS}
|
||||
* - {@link Piwik\Metrics::INDEX_NB_ACTIONS}
|
||||
* @param bool|\Piwik\RankingQuery $rankingQuery
|
||||
* A pre-configured ranking query instance that will be used to limit the result.
|
||||
* If set, the return value is the array returned by {@link Piwik\RankingQuery::execute()}.
|
||||
* @param bool|string $joinLogActionOnColumn One or more columns from the **log_link_visit_action** table that
|
||||
* log_action should be joined on. The table alias used for each join
|
||||
* is `"log_action$i"` where `$i` is the index of the column in this
|
||||
* array.
|
||||
*
|
||||
* If a string is used for this parameter, the table alias is not
|
||||
* suffixed (since there is only one column).
|
||||
* @return mixed A Zend_Db_Statement if `$rankingQuery` isn't supplied, otherwise the result of
|
||||
* {@link Piwik\RankingQuery::execute()}. Read [this](#queryEcommerceItems-result-set)
|
||||
* to see what aggregate data is calculated by the query.
|
||||
* @api
|
||||
*/
|
||||
public function queryActionsByDimension($dimensions, $where = '', $additionalSelects = array(), $metrics = false, $rankingQuery = null, $joinLogActionOnColumn = false)
|
||||
{
|
||||
$tableName = self::LOG_ACTIONS_TABLE;
|
||||
$availableMetrics = $this->getActionsMetricFields();
|
||||
|
||||
$select = $this->getSelectStatement($dimensions, $tableName, $additionalSelects, $availableMetrics, $metrics);
|
||||
$from = array($tableName);
|
||||
$where = $this->getWhereStatement($tableName, self::ACTION_DATETIME_FIELD, $where);
|
||||
$groupBy = $this->getGroupByStatement($dimensions, $tableName);
|
||||
$orderBy = false;
|
||||
|
||||
if ($joinLogActionOnColumn !== false) {
|
||||
$multiJoin = is_array($joinLogActionOnColumn);
|
||||
if (!$multiJoin) {
|
||||
$joinLogActionOnColumn = array($joinLogActionOnColumn);
|
||||
}
|
||||
|
||||
foreach ($joinLogActionOnColumn as $i => $joinColumn) {
|
||||
$tableAlias = 'log_action' . ($multiJoin ? $i + 1 : '');
|
||||
if (strpos($joinColumn, ' ') === false) {
|
||||
$joinOn = $tableAlias . '.idaction = ' . $tableName . '.' . $joinColumn;
|
||||
} else {
|
||||
// more complex join column like IF(...)
|
||||
$joinOn = $tableAlias . '.idaction = ' . $joinColumn;
|
||||
}
|
||||
$from[] = array(
|
||||
'table' => 'log_action',
|
||||
'tableAlias' => $tableAlias,
|
||||
'joinOn' => $joinOn
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ($rankingQuery) {
|
||||
$orderBy = '`' . Metrics::INDEX_NB_ACTIONS . '` DESC';
|
||||
}
|
||||
|
||||
$query = $this->generateQuery($select, $from, $where, $groupBy, $orderBy);
|
||||
|
||||
if ($rankingQuery !== null) {
|
||||
$sumColumns = array_keys($availableMetrics);
|
||||
if ($metrics) {
|
||||
$sumColumns = array_intersect($sumColumns, $metrics);
|
||||
}
|
||||
$rankingQuery->addColumn($sumColumns, 'sum');
|
||||
return $rankingQuery->execute($query['sql'], $query['bind']);
|
||||
}
|
||||
|
||||
return $this->getDb()->query($query['sql'], $query['bind']);
|
||||
}
|
||||
|
||||
protected function getActionsMetricFields()
|
||||
{
|
||||
return $availableMetrics = array(
|
||||
Metrics::INDEX_NB_VISITS => "count(distinct " . self::LOG_ACTIONS_TABLE . ".idvisit)",
|
||||
Metrics::INDEX_NB_UNIQ_VISITORS => "count(distinct " . self::LOG_ACTIONS_TABLE . ".idvisitor)",
|
||||
Metrics::INDEX_NB_ACTIONS => "count(*)",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a query aggregating conversion data (everything in the **log_conversion** table) and returns
|
||||
* a DB statement that can be used to iterate over the result.
|
||||
*
|
||||
* <a name="queryConversionsByDimension-result-set"></a>
|
||||
* **Result Set**
|
||||
*
|
||||
* Each row of the result set represents an aggregated group of conversions. The
|
||||
* following columns are in each aggregate row:
|
||||
*
|
||||
* - **{@link Piwik\Metrics::INDEX_GOAL_NB_CONVERSIONS}**: The total number of conversions in this aggregate
|
||||
* group.
|
||||
* - **{@link Piwik\Metrics::INDEX_GOAL_NB_VISITS_CONVERTED}**: The total number of visits during which these
|
||||
* conversions were converted.
|
||||
* - **{@link Piwik\Metrics::INDEX_GOAL_REVENUE}**: The total revenue generated by these conversions. This value
|
||||
* includes the revenue from individual ecommerce items.
|
||||
* - **{@link Piwik\Metrics::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL}**: The total cost of all ecommerce items sold
|
||||
* within these conversions. This value does not
|
||||
* include tax, shipping or any applied discount.
|
||||
*
|
||||
* _This metric is only applicable to the special
|
||||
* **ecommerce** goal (where `idGoal == 'ecommerceOrder'`)._
|
||||
* - **{@link Piwik\Metrics::INDEX_GOAL_ECOMMERCE_REVENUE_TAX}**: The total tax applied to every transaction in these
|
||||
* conversions.
|
||||
*
|
||||
* _This metric is only applicable to the special
|
||||
* **ecommerce** goal (where `idGoal == 'ecommerceOrder'`)._
|
||||
* - **{@link Piwik\Metrics::INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING}**: The total shipping cost for every transaction
|
||||
* in these conversions.
|
||||
*
|
||||
* _This metric is only applicable to the special
|
||||
* **ecommerce** goal (where `idGoal == 'ecommerceOrder'`)._
|
||||
* - **{@link Piwik\Metrics::INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT}**: The total discount applied to every transaction
|
||||
* in these conversions.
|
||||
*
|
||||
* _This metric is only applicable to the special
|
||||
* **ecommerce** goal (where `idGoal == 'ecommerceOrder'`)._
|
||||
* - **{@link Piwik\Metrics::INDEX_GOAL_ECOMMERCE_ITEMS}**: The total number of ecommerce items sold in each transaction
|
||||
* in these conversions.
|
||||
*
|
||||
* _This metric is only applicable to the special
|
||||
* **ecommerce** goal (where `idGoal == 'ecommerceOrder'`)._
|
||||
*
|
||||
* Additional data can be selected through the `$additionalSelects` parameter.
|
||||
*
|
||||
* _Note: This method will only query the **log_conversion** table. Other tables cannot be joined
|
||||
* using this method._
|
||||
*
|
||||
* @param array|string $dimensions One or more **SELECT** fields that will be used to group the log_conversion
|
||||
* rows by. This parameter determines which **log_conversion** rows will be
|
||||
* aggregated together.
|
||||
* @param bool|string $where An optional SQL expression used in the SQL's **WHERE** clause.
|
||||
* @param array $additionalSelects Additional SELECT fields that are not included in the group by
|
||||
* clause. These can be aggregate expressions, eg, `SUM(somecol)`.
|
||||
* @return Zend_Db_Statement
|
||||
*/
|
||||
public function queryConversionsByDimension($dimensions = array(), $where = false, $additionalSelects = array())
|
||||
{
|
||||
$dimensions = array_merge(array(self::IDGOAL_FIELD), $dimensions);
|
||||
$availableMetrics = $this->getConversionsMetricFields();
|
||||
$tableName = self::LOG_CONVERSION_TABLE;
|
||||
|
||||
$select = $this->getSelectStatement($dimensions, $tableName, $additionalSelects, $availableMetrics);
|
||||
|
||||
$from = array($tableName);
|
||||
$where = $this->getWhereStatement($tableName, self::CONVERSION_DATETIME_FIELD, $where);
|
||||
$groupBy = $this->getGroupByStatement($dimensions, $tableName);
|
||||
$orderBy = false;
|
||||
$query = $this->generateQuery($select, $from, $where, $groupBy, $orderBy);
|
||||
return $this->getDb()->query($query['sql'], $query['bind']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and returns an array of SQL `SELECT` expressions that will each count how
|
||||
* many rows have a column whose value is within a certain range.
|
||||
*
|
||||
* **Note:** The result of this function is meant for use in the `$additionalSelects` parameter
|
||||
* in one of the query... methods (for example {@link queryVisitsByDimension()}).
|
||||
*
|
||||
* **Example**
|
||||
*
|
||||
* // summarize one column
|
||||
* $visitTotalActionsRanges = array(
|
||||
* array(1, 1),
|
||||
* array(2, 10),
|
||||
* array(10)
|
||||
* );
|
||||
* $selects = LogAggregator::getSelectsFromRangedColumn('visit_total_actions', $visitTotalActionsRanges, 'log_visit', 'vta');
|
||||
*
|
||||
* // summarize another column in the same request
|
||||
* $visitCountVisitsRanges = array(
|
||||
* array(1, 1),
|
||||
* array(2, 20),
|
||||
* array(20)
|
||||
* );
|
||||
* $selects = array_merge(
|
||||
* $selects,
|
||||
* LogAggregator::getSelectsFromRangedColumn('visitor_count_visits', $visitCountVisitsRanges, 'log_visit', 'vcv')
|
||||
* );
|
||||
*
|
||||
* // perform the query
|
||||
* $logAggregator = // get the LogAggregator somehow
|
||||
* $query = $logAggregator->queryVisitsByDimension($dimensions = array(), $where = false, $selects);
|
||||
* $tableSummary = $query->fetch();
|
||||
*
|
||||
* $numberOfVisitsWithOneAction = $tableSummary['vta0'];
|
||||
* $numberOfVisitsBetweenTwoAnd10 = $tableSummary['vta1'];
|
||||
*
|
||||
* $numberOfVisitsWithVisitCountOfOne = $tableSummary['vcv0'];
|
||||
*
|
||||
* @param string $column The name of a column in `$table` that will be summarized.
|
||||
* @param array $ranges The array of ranges over which the data in the table
|
||||
* will be summarized. For example,
|
||||
* ```
|
||||
* array(
|
||||
* array(1, 1),
|
||||
* array(2, 2),
|
||||
* array(3, 8),
|
||||
* array(8) // everything over 8
|
||||
* )
|
||||
* ```
|
||||
* @param string $table The unprefixed name of the table whose rows will be summarized.
|
||||
* @param string $selectColumnPrefix The prefix to prepend to each SELECT expression. This
|
||||
* prefix is used to differentiate different sets of
|
||||
* range summarization SELECTs. You can supply different
|
||||
* values to this argument to summarize several columns
|
||||
* in one query (see above for an example).
|
||||
* @param bool $restrictToReturningVisitors Whether to only summarize rows that belong to
|
||||
* visits of returning visitors or not. If this
|
||||
* argument is true, then the SELECT expressions
|
||||
* returned can only be used with the
|
||||
* {@link queryVisitsByDimension()} method.
|
||||
* @return array An array of SQL SELECT expressions, for example,
|
||||
* ```
|
||||
* array(
|
||||
* 'sum(case when log_visit.visit_total_actions between 0 and 2 then 1 else 0 end) as vta0',
|
||||
* 'sum(case when log_visit.visit_total_actions > 2 then 1 else 0 end) as vta1'
|
||||
* )
|
||||
* ```
|
||||
* @api
|
||||
*/
|
||||
public static function getSelectsFromRangedColumn($column, $ranges, $table, $selectColumnPrefix, $restrictToReturningVisitors = false)
|
||||
{
|
||||
$selects = array();
|
||||
$extraCondition = '';
|
||||
if ($restrictToReturningVisitors) {
|
||||
// extra condition for the SQL SELECT that makes sure only returning visits are counted
|
||||
// when creating the 'days since last visit' report
|
||||
$extraCondition = 'and log_visit.visitor_returning = 1';
|
||||
$extraSelect = "sum(case when log_visit.visitor_returning = 0 then 1 else 0 end) "
|
||||
. " as `" . $selectColumnPrefix . 'General_NewVisits' . "`";
|
||||
$selects[] = $extraSelect;
|
||||
}
|
||||
foreach ($ranges as $gap) {
|
||||
if (count($gap) == 2) {
|
||||
$lowerBound = $gap[0];
|
||||
$upperBound = $gap[1];
|
||||
|
||||
$selectAs = "$selectColumnPrefix$lowerBound-$upperBound";
|
||||
|
||||
$selects[] = "sum(case when $table.$column between $lowerBound and $upperBound $extraCondition" .
|
||||
" then 1 else 0 end) as `$selectAs`";
|
||||
} else {
|
||||
$lowerBound = $gap[0];
|
||||
|
||||
$selectAs = $selectColumnPrefix . ($lowerBound + 1) . urlencode('+');
|
||||
|
||||
$selects[] = "sum(case when $table.$column > $lowerBound $extraCondition then 1 else 0 end) as `$selectAs`";
|
||||
}
|
||||
}
|
||||
|
||||
return $selects;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up the row data and return values.
|
||||
* $lookForThisPrefix can be used to make sure only SOME of the data in $row is used.
|
||||
*
|
||||
* The array will have one column $columnName
|
||||
*
|
||||
* @param $row
|
||||
* @param $columnName
|
||||
* @param bool $lookForThisPrefix A string that identifies which elements of $row to use
|
||||
* in the result. Every key of $row that starts with this
|
||||
* value is used.
|
||||
* @return array
|
||||
*/
|
||||
static public function makeArrayOneColumn($row, $columnName, $lookForThisPrefix = false)
|
||||
{
|
||||
$cleanRow = array();
|
||||
foreach ($row as $label => $count) {
|
||||
if (empty($lookForThisPrefix)
|
||||
|| strpos($label, $lookForThisPrefix) === 0
|
||||
) {
|
||||
$cleanLabel = substr($label, strlen($lookForThisPrefix));
|
||||
$cleanRow[$cleanLabel] = array($columnName => $count);
|
||||
}
|
||||
}
|
||||
return $cleanRow;
|
||||
}
|
||||
|
||||
public function getDb()
|
||||
{
|
||||
return Db::get();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue