| <?php |
|
|
| |
| |
| |
| |
| |
| |
|
|
| namespace Piwik\DataAccess; |
|
|
| use Piwik\ArchiveProcessor\Parameters; |
| use Piwik\Common; |
| use Piwik\Config; |
| use Piwik\Config\DatabaseConfig; |
| use Piwik\Container\StaticContainer; |
| use Piwik\DataArray; |
| use Piwik\Date; |
| use Piwik\Db; |
| use Piwik\DbHelper; |
| use Piwik\Metrics; |
| use Piwik\Plugin\LogTablesProvider; |
| use Piwik\RankingQuery; |
| use Piwik\Segment; |
| use Piwik\Tracker\Action; |
| use Piwik\Tracker\GoalManager; |
| use Piwik\Log\LoggerInterface; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| class LogAggregator |
| { |
| public const LOG_VISIT_TABLE = 'log_visit'; |
|
|
| public const LOG_ACTIONS_TABLE = 'log_link_visit_action'; |
|
|
| public const LOG_CONVERSION_TABLE = "log_conversion"; |
|
|
| public const REVENUE_SUBTOTAL_FIELD = 'revenue_subtotal'; |
|
|
| public const REVENUE_TAX_FIELD = 'revenue_tax'; |
|
|
| public const REVENUE_SHIPPING_FIELD = 'revenue_shipping'; |
|
|
| public const REVENUE_DISCOUNT_FIELD = 'revenue_discount'; |
|
|
| public const TOTAL_REVENUE_FIELD = 'revenue'; |
|
|
| public const ITEMS_COUNT_FIELD = "items"; |
|
|
| public const CONVERSION_DATETIME_FIELD = "server_time"; |
|
|
| public const ACTION_DATETIME_FIELD = "server_time"; |
|
|
| public const VISIT_DATETIME_FIELD = 'visit_last_action_time'; |
|
|
| public const IDGOAL_FIELD = 'idgoal'; |
|
|
| public const FIELDS_SEPARATOR = ", \n\t\t\t"; |
|
|
| public const LOG_TABLE_SEGMENT_TEMPORARY_PREFIX = 'logtmpsegment'; |
|
|
| |
| protected $dateStart; |
|
|
| |
| protected $dateEnd; |
|
|
| |
| protected $sites; |
|
|
| |
| protected $segment; |
|
|
| |
| |
| |
| private $queryOriginHint = ''; |
|
|
| |
| |
| |
| private $logger; |
|
|
| |
| |
| |
| private $allowUsageSegmentCache = false; |
|
|
| |
| |
| |
| private $params; |
|
|
| public function __construct(Parameters $params, ?LoggerInterface $logger = null) |
| { |
| $this->dateStart = $params->getDateTimeStart(); |
| $this->dateEnd = $params->getDateTimeEnd(); |
| $this->segment = $params->getSegment(); |
| $this->sites = $params->getIdSites(); |
| $this->logger = $logger ?: StaticContainer::get(LoggerInterface::class); |
| $this->params = $params; |
| } |
|
|
| |
| |
| |
| |
| public function setSites($sites) |
| { |
| $this->sites = array_map('intval', $sites); |
| } |
|
|
| |
| |
| |
| public function getSites() |
| { |
| return $this->sites; |
| } |
|
|
| |
| |
| |
| public function getSegment() |
| { |
| return $this->segment; |
| } |
|
|
| |
| |
| |
| |
| public function setQueryOriginHint($nameOfOrigin) |
| { |
| $this->queryOriginHint = $nameOfOrigin; |
| } |
|
|
| |
| |
| |
| public function getQueryOriginHint() |
| { |
| return $this->queryOriginHint; |
| } |
|
|
| |
| |
| |
| public function getSegmentTmpTableName() |
| { |
| $bind = $this->getGeneralQueryBindParams(); |
| $tableName = self::LOG_TABLE_SEGMENT_TEMPORARY_PREFIX . md5(json_encode($bind) . $this->segment->getString()); |
|
|
| $lengthPrefix = mb_strlen(Common::prefixTable('')); |
| $maxLength = Db\Schema\Mysql::MAX_TABLE_NAME_LENGTH - $lengthPrefix; |
|
|
| return mb_substr($tableName, 0, $maxLength); |
| } |
|
|
| |
| |
| |
| public function cleanup() |
| { |
| if (!$this->segment->isEmpty() && $this->isSegmentCacheEnabled()) { |
| $segmentTable = $this->getSegmentTmpTableName(); |
| $segmentTable = Common::prefixTable($segmentTable); |
|
|
| if ($this->doesSegmentTableExist($segmentTable)) { |
| |
| |
| $this->getDb()->query('DROP TEMPORARY TABLE IF EXISTS ' . $segmentTable); |
| } |
|
|
| $logTablesProvider = $this->getLogTableProvider(); |
| if ($logTablesProvider->getLogTable($segmentTable)) { |
| $logTablesProvider->setTempTable(null); |
| } |
| } |
| } |
|
|
| private function doesSegmentTableExist(string $segmentTablePrefixed): bool |
| { |
| try { |
| |
| $this->getDb()->fetchOne('SELECT /* WP IGNORE ERROR */ 1 FROM `' . $segmentTablePrefixed . '` LIMIT 1'); |
| $tableExists = true; |
| } catch (\Exception $e) { |
| $tableExists = false; |
| } |
|
|
| return $tableExists; |
| } |
|
|
| private function isSegmentCacheEnabled(): bool |
| { |
| if (!$this->allowUsageSegmentCache) { |
| return false; |
| } |
|
|
| $config = Config::getInstance(); |
| $general = $config->General; |
| return !empty($general['enable_segments_cache']); |
| } |
|
|
| |
| |
| |
| public function allowUsageSegmentCache() |
| { |
| $this->allowUsageSegmentCache = true; |
| } |
|
|
| private function getLogTableProvider(): LogTablesProvider |
| { |
| return StaticContainer::get(LogTablesProvider::class); |
| } |
|
|
| |
| |
| |
| private function createTemporaryTable(string $unprefixedSegmentTableName, string $segmentSelectSql, array $segmentSelectBind): void |
| { |
| $table = Common::prefixTable($unprefixedSegmentTableName); |
|
|
| if ($this->doesSegmentTableExist($table)) { |
| return; |
| } |
|
|
| $engine = ''; |
| if (defined('PIWIK_TEST_MODE') && PIWIK_TEST_MODE) { |
| $engine = 'ENGINE=MEMORY'; |
| } |
| $createTableSql = 'CREATE TEMPORARY TABLE ' . $table . ' (idvisit BIGINT(10) UNSIGNED NOT NULL, PRIMARY KEY (`idvisit`)) ' . $engine; |
| |
| |
|
|
| $readerDb = Db::getReader(); |
| try { |
| $readerDb->query($createTableSql); |
| } catch (\Exception $e) { |
| if ($readerDb->isErrNo($e, \Piwik\Updater\Migration\Db::ERROR_CODE_TABLE_EXISTS)) { |
| return; |
| } else { |
| throw $e; |
| } |
| } |
|
|
| $transactionLevel = new Db\TransactionLevel($readerDb); |
| $canSetTransactionLevel = $transactionLevel->canLikelySetTransactionLevel(); |
|
|
| if ($canSetTransactionLevel) { |
| |
| |
| |
| |
| if (!$transactionLevel->setTransactionLevelForNonLockingReads()) { |
| $canSetTransactionLevel = false; |
| } |
| } |
|
|
| if (!$canSetTransactionLevel) { |
| |
| |
| $all = $readerDb->fetchAll($segmentSelectSql, $segmentSelectBind); |
| if (!empty($all)) { |
| |
| $readerDb->query('INSERT IGNORE INTO ' . $table . ' VALUES (' . implode('),(', array_column($all, 'idvisit')) . ')'); |
| } |
| return; |
| } |
|
|
| $insertIntoStatement = 'INSERT IGNORE INTO ' . $table . ' (idvisit) ' . $segmentSelectSql; |
| $readerDb->query($insertIntoStatement, $segmentSelectBind); |
|
|
| $transactionLevel->restorePreviousStatus(); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| public function generateQuery($select, $from, $where, $groupBy, $orderBy, $limit = 0, $offset = 0, bool $withRollup = false) |
| { |
| $segment = $this->segment; |
| $bind = $this->getGeneralQueryBindParams(); |
|
|
| if (!$this->segment->isEmpty() && $this->isSegmentCacheEnabled()) { |
| $segment = new Segment('', $this->sites, $this->params->getPeriod()->getDateTimeStart(), $this->params->getPeriod()->getDateTimeEnd()); |
|
|
| $logTablesProvider = $this->getLogTableProvider(); |
| $segmentTable = $this->createSegmentTable(); |
| $logTablesProvider->setTempTable(new LogTableTemporary($segmentTable)); |
|
|
| |
| |
| if (!is_array($from)) { |
| $from = array($segmentTable, $from); |
| } else { |
| array_unshift($from, $segmentTable); |
| } |
|
|
| foreach ($logTablesProvider->getAllLogTables() as $logTable) { |
| |
| |
| |
| foreach ($from as $fromJoin) { |
| if ( |
| !empty($fromJoin['table']) && $fromJoin['table'] === $logTable->getName() && |
| !empty($fromJoin['join']) && strtoupper($fromJoin['join']) === 'RIGHT JOIN' |
| ) { |
| continue 2; |
| } |
| } |
|
|
| if ($logTable->getDateTimeColumn()) { |
| $whereTest = $this->getWhereStatement($logTable->getName(), $logTable->getDateTimeColumn()); |
| if (strpos($where, $whereTest) === 0) { |
| |
| |
| $where = ltrim(str_replace($whereTest, '', $where)); |
| if (stripos($where, 'and ') === 0) { |
| $where = substr($where, strlen('and ')); |
| } |
| $bind = array(); |
| break; |
| } |
| } |
| } |
| } |
|
|
| $query = $segment->getSelectQuery($select, $from, $where, $bind, $orderBy, $groupBy, $limit, $offset, $forceGroupBy = false, $withRollup); |
|
|
| $query['sql'] = DbHelper::addOriginHintToQuery($query['sql'], $this->queryOriginHint, $this->dateStart, $this->dateEnd, $this->sites, $this->segment); |
| if (DatabaseConfig::getConfigValue('enable_first_table_join_prefix')) { |
| $fromTable = is_array($from) ? reset($from) : $from; |
| $fromTable = is_array($fromTable) ? $fromTable['table'] : $fromTable; |
| $query['sql'] = DbHelper::addJoinPrefixHintToQuery($query['sql'], $fromTable); |
| } |
|
|
| return $query; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| private function createSegmentTable(): string |
| { |
| $segmentTable = $this->getSegmentTmpTableName(); |
| $segmentSql = $this->getSegmentTableSql(); |
|
|
| $this->createTemporaryTable($segmentTable, $segmentSql['sql'], $segmentSql['bind']); |
|
|
| return $segmentTable; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| public function getSegmentTableSql(): array |
| { |
| $segmentWhere = $this->getWhereStatement('log_visit', 'visit_last_action_time'); |
| $segmentBind = $this->getGeneralQueryBindParams(); |
|
|
| $logQueryBuilder = StaticContainer::get('Piwik\DataAccess\LogQueryBuilder'); |
| $forceGroupByBackup = $logQueryBuilder->getForcedInnerGroupBySubselect(); |
| $logQueryBuilder->forceInnerGroupBySubselect(LogQueryBuilder::FORCE_INNER_GROUP_BY_NO_SUBSELECT); |
| $segmentSql = $this->segment->getSelectQuery('distinct log_visit.idvisit as idvisit', 'log_visit', $segmentWhere, $segmentBind, 'log_visit.idvisit ASC'); |
|
|
| if (DatabaseConfig::getConfigValue('enable_segment_first_table_join_prefix')) { |
| $segmentSql['sql'] = DbHelper::addJoinPrefixHintToQuery($segmentSql['sql'], 'log_visit'); |
| } |
|
|
| $logQueryBuilder->forceInnerGroupBySubselect($forceGroupByBackup); |
|
|
| return $segmentSql; |
| } |
|
|
| |
| |
| |
| public function getVisitsMetricFields(): array |
| { |
| return [ |
| Metrics::INDEX_NB_UNIQ_VISITORS => [ |
| 'aggregation' => 'sum', |
| 'query' => "count(distinct " . self::LOG_VISIT_TABLE . ".idvisitor)", |
| ], |
| Metrics::INDEX_NB_UNIQ_FINGERPRINTS => [ |
| 'aggregation' => 'sum', |
| 'query' => "count(distinct " . self::LOG_VISIT_TABLE . ".config_id)", |
| ], |
| Metrics::INDEX_NB_VISITS => [ |
| 'aggregation' => 'sum', |
| 'query' => "count(*)", |
| ], |
| Metrics::INDEX_NB_ACTIONS => [ |
| 'aggregation' => 'sum', |
| 'query' => "sum(" . self::LOG_VISIT_TABLE . ".visit_total_actions)", |
| ], |
| Metrics::INDEX_MAX_ACTIONS => [ |
| 'aggregation' => 'max', |
| 'query' => "max(" . self::LOG_VISIT_TABLE . ".visit_total_actions)", |
| ], |
| Metrics::INDEX_SUM_VISIT_LENGTH => [ |
| 'aggregation' => 'sum', |
| 'query' => "sum(" . self::LOG_VISIT_TABLE . ".visit_total_time)", |
| ], |
| Metrics::INDEX_BOUNCE_COUNT => [ |
| 'aggregation' => 'sum', |
| 'query' => "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 => [ |
| 'aggregation' => 'sum', |
| 'query' => "sum(case " . self::LOG_VISIT_TABLE . ".visit_goal_converted when 1 then 1 else 0 end)", |
| ], |
| Metrics::INDEX_NB_USERS => [ |
| 'aggregation' => 'sum', |
| 'query' => "count(distinct " . self::LOG_VISIT_TABLE . ".user_id)", |
| ], |
| ]; |
| } |
|
|
| |
| |
| |
| public static function getConversionsMetricFields() |
| { |
| return [ |
| 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 . ")", |
| ]; |
| } |
|
|
| private static function getSqlConversionRevenueSum(string $field): string |
| { |
| $column = self::LOG_CONVERSION_TABLE . '.' . $field; |
|
|
| return self::getSqlRevenue(self::getSqlSumExcludingOutOfRange($column, $column)); |
| } |
|
|
| |
| |
| |
| |
| public static function getSqlRevenue($field) |
| { |
| return "ROUND(" . $field . "," . GoalManager::REVENUE_PRECISION . ")"; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| private static function getSqlSumExcludingOutOfRange(string $guardColumn, string $valueExpression): string |
| { |
| return sprintf( |
| 'SUM(CASE WHEN ABS(%s) > %d THEN 0 ELSE %s END)', |
| $guardColumn, |
| GoalManager::MAX_ALLOWED_REVENUE, |
| $valueExpression |
| ); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| public function getMetricsFromVisitByDimension($dimension) |
| { |
| if (!is_array($dimension)) { |
| $dimension = [$dimension]; |
| } |
| if (count($dimension) === 1) { |
| $dimension = ["label" => reset($dimension)]; |
| } |
| $query = $this->queryVisitsByDimension($dimension); |
| $metrics = new DataArray(); |
| while ($row = $query->fetch()) { |
| $metrics->sumMetricsVisits($row["label"], $row); |
| } |
| return $metrics; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| public function queryVisitsByDimension( |
| array $dimensions = [], |
| $where = false, |
| array $additionalSelects = [], |
| $metrics = false, |
| $rankingQuery = false, |
| $orderBy = false, |
| $timeLimit = -1, |
| $rankingQueryGenerate = false |
| ) { |
| $query = $this->getQueryByDimensionSql( |
| $dimensions, |
| $where, |
| $additionalSelects, |
| $metrics, |
| $rankingQuery, |
| $orderBy, |
| $timeLimit, |
| $rankingQueryGenerate |
| ); |
|
|
| |
| if ($rankingQuery && !$rankingQueryGenerate) { |
| return $query; |
| } |
|
|
| return $this->getDb()->query($query['sql'], $query['bind']); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| public function getQueryByDimensionSql( |
| array $dimensions, |
| $where, |
| array $additionalSelects, |
| $metrics, |
| $rankingQuery, |
| $orderBy, |
| $timeLimit, |
| $rankingQueryGenerate |
| ): array { |
| $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); |
| $orderBys = $orderBy ? [$orderBy] : []; |
|
|
| if ($rankingQuery) { |
| $orderBys[] = '`' . Metrics::INDEX_NB_VISITS . '` DESC'; |
| } |
|
|
| $query = $this->generateQuery($select, $from, $where, $groupBy, implode(', ', $orderBys)); |
|
|
| if ($rankingQuery) { |
| |
| if (!$this->isMetricRequested(Metrics::INDEX_NB_UNIQ_FINGERPRINTS, $metrics)) { |
| unset($availableMetrics[Metrics::INDEX_NB_UNIQ_FINGERPRINTS]); |
| } |
|
|
| foreach ($availableMetrics as $metricId => $config) { |
| if ($this->isMetricRequested($metricId, $metrics)) { |
| $rankingQuery->addColumn($metricId, $config['aggregation']); |
| } |
| } |
|
|
| if ($rankingQueryGenerate) { |
| $query['sql'] = $rankingQuery->generateRankingQuery($query['sql']); |
| } else { |
| return $rankingQuery->execute($query['sql'], $query['bind'], $timeLimit); |
| } |
| } |
|
|
| $query['sql'] = DbHelper::addMaxExecutionTimeHintToQuery($query['sql'], $timeLimit); |
|
|
| return $query; |
| } |
|
|
| protected function getSelectsMetrics($metricsAvailable, $metricsRequested = false): array |
| { |
| $selects = []; |
|
|
| foreach ($metricsAvailable as $metricId => $config) { |
| if ($this->isMetricRequested($metricId, $metricsRequested)) { |
| $aliasAs = $this->getSelectAliasAs($metricId); |
| $selects[] = (is_array($config) ? $config['query'] : $config) . $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; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| 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; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| protected function getSelectDimensions(array $dimensions, string $tableName): array |
| { |
| $selectDimensions = []; |
|
|
| foreach ($dimensions as $selectAs => $field) { |
| if ($this->isFieldFunctionOrComplexExpression($field) && is_numeric($selectAs)) { |
| |
| $selectDimensions[] = $field; |
| continue; |
| } |
|
|
| $selectAlias = !is_numeric($selectAs) ? $selectAs : $field; |
|
|
| if (!$this->isFieldFunctionOrComplexExpression($field)) { |
| |
| $field = $this->prefixColumn($field, $tableName); |
| } |
|
|
| |
| $field .= $this->getSelectAliasAs($selectAlias); |
| $selectDimensions[] = $field; |
| } |
|
|
| return $selectDimensions; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| protected function getGroupByDimensions(array $dimensions, string $tableName): array |
| { |
| $orderByDimensions = []; |
|
|
| foreach ($dimensions as $selectAs => $field) { |
| if (!is_numeric($selectAs)) { |
| $orderByDimensions[] = $selectAs; |
| continue; |
| } |
|
|
| if ($this->isFieldFunctionOrComplexExpression($field)) { |
| |
| if (preg_match('/\s+AS\s+(.*?)\s*$/', $field, $matches)) { |
| $orderByDimensions[] = $matches[1]; |
| continue; |
| } |
|
|
| $orderByDimensions[] = $field; |
| continue; |
| } |
|
|
| $orderByDimensions[] = $this->prefixColumn($field, $tableName); |
| } |
|
|
| return $orderByDimensions; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| private function prefixColumn(string $column, string $tableName): string |
| { |
| 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) |
| { |
| |
| if ($metricsRequested === false) { |
| if ($metricId == Metrics::INDEX_NB_UNIQ_FINGERPRINTS) { |
| return false; |
| } |
| return true; |
| } |
| return in_array($metricId, $metricsRequested); |
| } |
|
|
| public function getWhereStatement($tableName, $datetimeField, $extraWhere = false) |
| { |
| $where = "$tableName.$datetimeField >= ? |
| AND $tableName.$datetimeField <= ? |
| AND $tableName.idsite IN (" . Common::getSqlStringFieldsArray($this->sites) . ")"; |
|
|
| if (!empty($extraWhere)) { |
| $extraWhere = sprintf($extraWhere, $tableName, $tableName); |
| $where .= ' AND ' . $extraWhere; |
| } |
|
|
| return $where; |
| } |
|
|
| protected function getGroupByStatement($dimensions, $tableName) |
| { |
| $dimensions = $this->getGroupByDimensions($dimensions, $tableName); |
| $groupBy = implode(", ", $dimensions); |
|
|
| return $groupBy; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public function getGeneralQueryBindParams() |
| { |
| $bind = [ |
| $this->dateStart->toString(Date::DATE_TIME_FORMAT), |
| $this->dateEnd->toString(Date::DATE_TIME_FORMAT), |
| ]; |
| return array_merge($bind, $this->sites); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| 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(self::getSqlSumExcludingOutOfRange('log_conversion_item.price', '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(self::getSqlSumExcludingOutOfRange('log_conversion_item.price', '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 |
| ), |
| ) |
| ), |
| |
| array( |
| "log_conversion_item", |
| array( |
| "table" => "log_action", |
| "joinOn" => sprintf("log_conversion_item.%s = log_action.idaction", $dimension), |
| ), |
| ), |
| |
| implode( |
| ' AND ', |
| array( |
| 'log_conversion_item.server_time >= ?', |
| 'log_conversion_item.server_time <= ?', |
| 'log_conversion_item.idsite IN (' . Common::getSqlStringFieldsArray($this->sites) . ')', |
| 'log_conversion_item.deleted = 0', |
| ) |
| ), |
| |
| sprintf( |
| "ecommerceType, log_conversion_item.%s", |
| $dimension |
| ), |
| |
| false |
| ); |
|
|
| return $this->getDb()->query($query['sql'], $query['bind']); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| public function queryActionsByDimension( |
| $dimensions, |
| $where = '', |
| $additionalSelects = array(), |
| $metrics = false, |
| $rankingQuery = null, |
| $joinLogActionOnColumn = false, |
| $secondaryOrderBy = null, |
| $timeLimit = -1 |
| ) { |
| $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); |
|
|
| 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 { |
| |
| $joinOn = $tableAlias . '.idaction = ' . $joinColumn; |
| } |
|
|
| $from[] = array( |
| 'table' => 'log_action', |
| 'tableAlias' => $tableAlias, |
| 'joinOn' => $joinOn, |
| ); |
| } |
| } |
|
|
| $orderBy = false; |
| if ($rankingQuery) { |
| $orderBy = '`' . Metrics::INDEX_NB_ACTIONS . '` DESC'; |
| if ($secondaryOrderBy) { |
| $orderBy .= ', ' . $secondaryOrderBy; |
| } |
| } |
|
|
| $query = $this->generateQuery($select, $from, $where, $groupBy, $orderBy); |
|
|
| if ($rankingQuery) { |
| $sumColumns = array_keys($availableMetrics); |
| if ($metrics) { |
| $sumColumns = array_intersect($sumColumns, $metrics); |
| } |
|
|
| $rankingQuery->addColumn($sumColumns, 'sum'); |
|
|
| return $rankingQuery->execute($query['sql'], $query['bind'], $timeLimit); |
| } |
|
|
| $query['sql'] = DbHelper::addMaxExecutionTimeHintToQuery($query['sql'], $timeLimit); |
|
|
| return $this->getDb()->query($query['sql'], $query['bind']); |
| } |
|
|
| |
| |
| |
| protected function getActionsMetricFields() |
| { |
| return 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(*)", |
| ); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| public function queryConversionsByDimension( |
| $dimensions = [], |
| $where = false, |
| $additionalSelects = [], |
| $extraFrom = [], |
| $rankingQuery = false, |
| $rankingQueryGenerate = false, |
| $forceSiteDateIndex = false |
| ) { |
| $dimensions = array_merge([self::IDGOAL_FIELD], $dimensions); |
| $tableName = self::LOG_CONVERSION_TABLE; |
| $availableMetrics = $this->getConversionsMetricFields(); |
|
|
| $select = $this->getSelectStatement($dimensions, $tableName, $additionalSelects, $availableMetrics); |
|
|
| $primaryFrom = !$forceSiteDateIndex ? [$tableName] : [['table' => $tableName, 'useIndex' => 'index_idsite_datetime']]; |
| $from = array_merge($primaryFrom, $extraFrom); |
| $where = $this->getWhereStatement($tableName, self::CONVERSION_DATETIME_FIELD, $where); |
| $groupBy = $this->getGroupByStatement($dimensions, $tableName); |
| $orderBy = false; |
| $query = $this->generateQuery($select, $from, $where, $groupBy, $orderBy); |
|
|
| if (!empty($rankingQuery)) { |
| $sumColumns = array_keys($availableMetrics); |
| $rankingQuery->addColumn($sumColumns, 'sum'); |
|
|
| if ($rankingQueryGenerate) { |
| $query['sql'] = $rankingQuery->generateRankingQuery($query['sql']); |
| } else { |
| return $rankingQuery->execute($query['sql'], $query['bind']); |
| } |
| } |
|
|
| return $this->getDb()->query($query['sql'], $query['bind']); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public function queryConversionsByPageView(string $linkField, int $idGoal) |
| { |
|
|
| $select = " |
| log_conversion.idvisit AS idvisit, |
| " . $idGoal . " AS idgoal, |
| " . ($linkField == 'idaction_url' ? Action::TYPE_PAGE_URL : Action::TYPE_PAGE_TITLE) . " AS `type`, |
| lac.idaction AS idaction, |
| COUNT(*) AS `1`, |
| " . sprintf("ROUND(%s,2) AS `%d`,", self::getSqlSumExcludingOutOfRange('log_conversion.revenue', 'log_conversion.revenue'), Metrics::INDEX_GOAL_REVENUE) . " |
| " . sprintf("COUNT(log_conversion.idvisit) AS `%d`,", Metrics::INDEX_GOAL_NB_VISITS_CONVERTED) . " |
| " . sprintf("ROUND(%s,2) AS `%d`,", self::getSqlSumExcludingOutOfRange('log_conversion.revenue_subtotal', '1 / log_conversion.pageviews_before * log_conversion.revenue_subtotal'), Metrics::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL) . " |
| " . sprintf("ROUND(%s,2) AS `%d`,", self::getSqlSumExcludingOutOfRange('log_conversion.revenue_tax', '1 / log_conversion.pageviews_before * log_conversion.revenue_tax'), Metrics::INDEX_GOAL_ECOMMERCE_REVENUE_TAX) . " |
| " . sprintf("ROUND(%s,2) AS `%d`,", self::getSqlSumExcludingOutOfRange('log_conversion.revenue_shipping', '1 / log_conversion.pageviews_before * log_conversion.revenue_shipping'), Metrics::INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING) . " |
| " . sprintf("ROUND(%s,2) AS `%d`,", self::getSqlSumExcludingOutOfRange('log_conversion.revenue_discount', '1 / log_conversion.pageviews_before * log_conversion.revenue_discount'), Metrics::INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT) . " |
| " . sprintf("SUM(ROUND(1 / log_conversion.pageviews_before * log_conversion.items, 4)) AS `%d`,", Metrics::INDEX_GOAL_ECOMMERCE_ITEMS) . " |
| " . sprintf("log_conversion.pageviews_before AS `%d`,", Metrics::INDEX_GOAL_NB_PAGES_UNIQ_BEFORE) . " |
| " . sprintf("SUM(ROUND(1 / log_conversion.pageviews_before, 4)) AS `%d`,", Metrics::INDEX_GOAL_NB_CONVERSIONS_ATTRIB) . " |
| " . sprintf("COUNT(*) AS `%d`,", Metrics::INDEX_GOAL_NB_CONVERSIONS_PAGE_UNIQ) . " |
| " . sprintf("ROUND(%s,2) AS `%d`", self::getSqlSumExcludingOutOfRange('log_conversion.revenue', '1 / log_conversion.pageviews_before * log_conversion.revenue'), Metrics::INDEX_GOAL_REVENUE_ATTRIB); |
|
|
| $from = [ |
| 'log_conversion', |
| ['table' => 'log_link_visit_action', 'tableAlias' => 'logva', 'join' => 'RIGHT JOIN', |
| 'joinOn' => 'log_conversion.idvisit = logva.idvisit'], |
| ['table' => 'log_action', 'tableAlias' => 'lac', |
| 'joinOn' => 'logva.' . $linkField . ' = lac.idaction'], |
| ]; |
|
|
| $where = $this->getWhereStatement('log_conversion', 'server_time'); |
| $where .= sprintf( |
| 'AND log_conversion.idgoal = %d |
| AND logva.server_time <= log_conversion.server_time |
| AND lac.type = %s', |
| (int) $idGoal, |
| ($linkField == 'idaction_url' ? Action::TYPE_PAGE_URL : Action::TYPE_PAGE_TITLE) |
| ); |
|
|
| $groupBy = 'log_conversion.idvisit, lac.idaction'; |
|
|
| $query = $this->generateQuery($select, $from, $where, $groupBy, false); |
| return $this->getDb()->query($query['sql'], $query['bind']); |
| } |
|
|
| |
| |
| |
| |
| |
| public function queryConversionsByEntryPageView(string $linkField, int $rankingQueryLimit = 0) |
| { |
| $tableName = self::LOG_CONVERSION_TABLE; |
|
|
| $select = implode( |
| ', ', |
| [ |
| 'log_conversion.idgoal AS idgoal', |
| sprintf('log_visit.%s AS idaction', $linkField), |
| 'log_action.type', |
| sprintf('COUNT(*) AS `%d`', Metrics::INDEX_GOAL_NB_CONVERSIONS), |
| sprintf('COUNT(distinct log_conversion.idvisit) AS `%d`', Metrics::INDEX_GOAL_NB_VISITS_CONVERTED), |
| sprintf('%s AS `%d`', self::getSqlRevenue(self::getSqlSumExcludingOutOfRange('log_conversion.revenue', 'log_conversion.revenue')), Metrics::INDEX_GOAL_REVENUE_ENTRY), |
| sprintf('%s AS `%d`', self::getSqlRevenue(self::getSqlSumExcludingOutOfRange('log_conversion.revenue_subtotal', 'log_conversion.revenue_subtotal')), Metrics::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL), |
| sprintf('%s AS `%d`', self::getSqlRevenue(self::getSqlSumExcludingOutOfRange('log_conversion.revenue_tax', 'log_conversion.revenue_tax')), Metrics::INDEX_GOAL_ECOMMERCE_REVENUE_TAX), |
| sprintf('%s AS `%d`', self::getSqlRevenue(self::getSqlSumExcludingOutOfRange('log_conversion.revenue_shipping', 'log_conversion.revenue_shipping')), Metrics::INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING), |
| sprintf('%s AS `%d`', self::getSqlRevenue(self::getSqlSumExcludingOutOfRange('log_conversion.revenue_discount', 'log_conversion.revenue_discount')), Metrics::INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT), |
| sprintf('SUM(log_conversion.items) AS `%d`', Metrics::INDEX_GOAL_ECOMMERCE_ITEMS), |
| sprintf('COUNT(*) AS `%d`', Metrics::INDEX_GOAL_NB_CONVERSIONS_ENTRY), |
| ] |
| ); |
|
|
| $from = [ |
| $tableName, |
| [ |
| "table" => "log_visit", |
| "joinOn" => "log_visit.idvisit = log_conversion.idvisit", |
| ], |
| [ |
| "table" => "log_action", |
| "joinOn" => "log_action.idaction = log_visit." . $linkField, |
| ], |
| ]; |
|
|
| $where = $linkField . ' IS NOT NULL AND log_conversion.idgoal >= 0'; |
| $where = $this->getWhereStatement($tableName, self::CONVERSION_DATETIME_FIELD, $where); |
| $groupBy = 'log_visit.' . $linkField . ', log_conversion.idgoal'; |
| $orderBy = false; |
|
|
| $query = $this->generateQuery($select, $from, $where, $groupBy, $orderBy); |
|
|
| return $this->getDb()->query($query['sql'], $query['bind']); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| public static function getSelectsFromRangedColumn($column, $ranges, $table, $selectColumnPrefix, $restrictToReturningVisitors = false) |
| { |
| $selects = array(); |
| $extraCondition = ''; |
|
|
| $tableColumn = $column; |
| if (strpos($tableColumn, $table) === false) { |
| $tableColumn = "$table.$column"; |
| } |
|
|
| if ($restrictToReturningVisitors) { |
| |
| |
| $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 $tableColumn 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 $tableColumn > $lowerBound $extraCondition then 1 else 0 end) as `$selectAs`"; |
| } |
| } |
|
|
| return $selects; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| public static function makeArrayOneColumn($row, $columnName, $lookForThisPrefix = false) |
| { |
| $cleanRow = []; |
|
|
| 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 new ArchivingDbAdapter(Db::getReader(), $this->logger); |
| } |
| } |
|
|