diff --git a/.gitattributes b/.gitattributes
index 5ffd723ef8caa3f9d5c2be75e188d4ab0545747d..1dd80d6c4daa310b102321570b717a818d2619f4 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -86,3 +86,5 @@ tests/javascript/assets/** linguist-vendored
tests/javascript/frameworks/** linguist-vendored
tests/javascript/jash/** linguist-vendored
tests/javascript/jslint/** linguist-vendored
+plugins/AnonymousPiwikUsageMeasurement/screenshots/AdminSettings.png filter=lfs diff=lfs merge=lfs -text
+plugins/Bandwidth/screenshots/Visitor_Overview.png filter=lfs diff=lfs merge=lfs -text
diff --git a/plugins/Actions/Metrics.php b/plugins/Actions/Metrics.php
new file mode 100644
index 0000000000000000000000000000000000000000..0e8907b897a07fcc006265a0a68224ac2d7f3d40
--- /dev/null
+++ b/plugins/Actions/Metrics.php
@@ -0,0 +1,103 @@
+ PiwikMetrics::INDEX_SUM_DAILY_NB_UNIQ_VISITORS,
+ PiwikMetrics::INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS => PiwikMetrics::INDEX_PAGE_ENTRY_SUM_DAILY_NB_UNIQ_VISITORS,
+ PiwikMetrics::INDEX_PAGE_EXIT_NB_UNIQ_VISITORS => PiwikMetrics::INDEX_PAGE_EXIT_SUM_DAILY_NB_UNIQ_VISITORS,
+ );
+
+ public static $columnsToDeleteAfterAggregation = array(
+ PiwikMetrics::INDEX_NB_UNIQ_VISITORS,
+ PiwikMetrics::INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS,
+ PiwikMetrics::INDEX_PAGE_EXIT_NB_UNIQ_VISITORS,
+ );
+
+ public static function getColumnsAggregationOperation()
+ {
+ $operations = [];
+ $actionMetrics = self::getActionMetrics();
+
+ foreach ($actionMetrics as $actionMetric => $definition) {
+ if (!empty($definition['aggregation']) && $definition['aggregation'] !== 'sum') {
+ $operations[$actionMetric] = $definition['aggregation'];
+ }
+ }
+
+ return $operations;
+ }
+
+ public static function getActionMetrics()
+ {
+ $metricsConfig = array(
+ PiwikMetrics::INDEX_NB_VISITS => array(
+ 'aggregation' => 'sum',
+ 'query' => "count(distinct log_link_visit_action.idvisit)",
+ ),
+ PiwikMetrics::INDEX_NB_UNIQ_VISITORS => array(
+ 'aggregation' => false,
+ 'query' => "count(distinct log_link_visit_action.idvisitor)",
+ ),
+ PiwikMetrics::INDEX_PAGE_NB_HITS => array(
+ 'aggregation' => 'sum',
+ 'query' => "count(*)",
+ ),
+ PiwikMetrics::INDEX_PAGE_SUM_TIME_GENERATION => array(
+ 'aggregation' => 'sum',
+ 'query' => "sum(
+ case when " . Action::DB_COLUMN_CUSTOM_FLOAT . " is null
+ then 0
+ else " . Action::DB_COLUMN_CUSTOM_FLOAT . "
+ end
+ ) / 1000",
+ ),
+ PiwikMetrics::INDEX_PAGE_NB_HITS_WITH_TIME_GENERATION => array(
+ 'aggregation' => 'sum',
+ 'query' => "sum(
+ case when " . Action::DB_COLUMN_CUSTOM_FLOAT . " is null
+ then 0
+ else 1
+ end
+ )",
+ ),
+ PiwikMetrics::INDEX_PAGE_MIN_TIME_GENERATION => array(
+ 'aggregation' => 'min',
+ 'query' => "min(" . Action::DB_COLUMN_CUSTOM_FLOAT . ") / 1000",
+ ),
+ PiwikMetrics::INDEX_PAGE_MAX_TIME_GENERATION => array(
+ 'aggregation' => 'max',
+ 'query' => "max(" . Action::DB_COLUMN_CUSTOM_FLOAT . ") / 1000",
+ ),
+ );
+
+ Piwik::postEvent('Actions.Archiving.addActionMetrics', array(&$metricsConfig));
+
+ return $metricsConfig;
+ }
+}
diff --git a/plugins/Actions/RecordBuilders/ActionReports.php b/plugins/Actions/RecordBuilders/ActionReports.php
new file mode 100644
index 0000000000000000000000000000000000000000..4caeafeb6d414c83e9b5f0f3b5bd0ef31d8eb9db
--- /dev/null
+++ b/plugins/Actions/RecordBuilders/ActionReports.php
@@ -0,0 +1,982 @@
+setBlobColumnAggregationOps(Metrics::getColumnsAggregationOperation());
+ $pageTitlesRecord = Record::make(Record::TYPE_BLOB, Archiver::PAGE_TITLES_RECORD_NAME)
+ ->setBlobColumnAggregationOps(Metrics::getColumnsAggregationOperation());
+
+ if ($this->isFlatArchivingEnabled()) {
+ $this->setHierarchyBuiltFromFlatRecord(
+ $pageUrlsRecord,
+ Archiver::PAGE_URLS_FLAT_RECORD_NAME,
+ [$this, 'flatRowToUrlHierarchyPath'],
+ [$this, 'reduceLegacyUrlHierarchyIntoFlatTable']
+ );
+ $this->setHierarchyBuiltFromFlatRecord(
+ $pageTitlesRecord,
+ Archiver::PAGE_TITLES_FLAT_RECORD_NAME,
+ [$this, 'flatRowToTitleHierarchyPath'],
+ [$this, 'reduceLegacyTitleHierarchyIntoFlatTable']
+ );
+ }
+
+ $records = [
+ Record::make(Record::TYPE_BLOB, Archiver::SITE_SEARCH_RECORD_NAME)
+ ->setMaxRowsInTable(ArchivingHelper::$maximumRowsInDataTableSiteSearch),
+
+ $pageUrlsRecord,
+ $pageTitlesRecord,
+
+ Record::make(Record::TYPE_BLOB, Archiver::DOWNLOADS_RECORD_NAME),
+ Record::make(Record::TYPE_BLOB, Archiver::OUTLINKS_RECORD_NAME),
+
+ Record::make(Record::TYPE_NUMERIC, Archiver::METRIC_SEARCHES_RECORD_NAME),
+ Record::make(Record::TYPE_NUMERIC, Archiver::METRIC_KEYWORDS_RECORD_NAME)
+ ->setIsCountOfBlobRecordRows(Archiver::SITE_SEARCH_RECORD_NAME),
+
+ Record::make(Record::TYPE_NUMERIC, Archiver::METRIC_OUTLINKS_RECORD_NAME),
+ Record::make(Record::TYPE_NUMERIC, Archiver::METRIC_UNIQ_OUTLINKS_RECORD_NAME),
+
+ Record::make(Record::TYPE_NUMERIC, Archiver::METRIC_PAGEVIEWS_RECORD_NAME),
+ Record::make(Record::TYPE_NUMERIC, Archiver::METRIC_UNIQ_PAGEVIEWS_RECORD_NAME),
+ Record::make(Record::TYPE_NUMERIC, Archiver::METRIC_SUM_TIME_RECORD_NAME),
+ Record::make(Record::TYPE_NUMERIC, Archiver::METRIC_HITS_TIMED_RECORD_NAME),
+
+ Record::make(Record::TYPE_NUMERIC, Archiver::METRIC_DOWNLOADS_RECORD_NAME),
+ Record::make(Record::TYPE_NUMERIC, Archiver::METRIC_UNIQ_DOWNLOADS_RECORD_NAME),
+ ];
+
+ if ($this->isFlatArchivingEnabled()) {
+ $records[] = Record::make(Record::TYPE_BLOB, Archiver::PAGE_URLS_FLAT_RECORD_NAME)
+ ->setBlobColumnAggregationOps(Metrics::getColumnsAggregationOperation())
+ ->setMaxRowsInTable(ArchivingHelper::$maximumRowsInDataTableFlat);
+ $records[] = Record::make(Record::TYPE_BLOB, Archiver::PAGE_TITLES_FLAT_RECORD_NAME)
+ ->setBlobColumnAggregationOps(Metrics::getColumnsAggregationOperation())
+ ->setMaxRowsInTable(ArchivingHelper::$maximumRowsInDataTableFlat);
+ }
+
+ return $records;
+ }
+
+ public function flatRowToUrlHierarchyPath(Row $flatRow): ?array
+ {
+ return $this->flatRowToHierarchyPath($flatRow, Action::TYPE_PAGE_URL);
+ }
+
+ public function flatRowToTitleHierarchyPath(Row $flatRow): ?array
+ {
+ return $this->flatRowToHierarchyPath($flatRow, Action::TYPE_PAGE_TITLE);
+ }
+
+ public function reduceLegacyUrlHierarchyIntoFlatTable(DataTable $legacyHierarchy, DataTable $flatTable, ArchiveProcessor $archiveProcessor, Record $record): void
+ {
+ ArchivingHelper::mergeHierarchicalActionsTableIntoBestEffortFlatTable(
+ $legacyHierarchy,
+ $flatTable,
+ Action::TYPE_PAGE_URL
+ );
+ }
+
+ public function reduceLegacyTitleHierarchyIntoFlatTable(DataTable $legacyHierarchy, DataTable $flatTable, ArchiveProcessor $archiveProcessor, Record $record): void
+ {
+ ArchivingHelper::mergeHierarchicalActionsTableIntoBestEffortFlatTable(
+ $legacyHierarchy,
+ $flatTable,
+ Action::TYPE_PAGE_TITLE
+ );
+ }
+
+ protected function aggregate(ArchiveProcessor $archiveProcessor): array
+ {
+ if (!$this->isFlatArchivingEnabled()) {
+ return $this->aggregateLegacyHierarchical($archiveProcessor);
+ }
+
+ $rankingQueryLimit = ArchivingHelper::getRankingQueryLimit();
+ ArchivingHelper::reloadConfig();
+
+ $tablesByType = $this->makeReportTables();
+ $flatPageTablesByType = $this->makeFlatPageTables();
+ $tableModesByType = [
+ Action::TYPE_PAGE_URL => ArchivingHelper::ACTION_TABLE_MODE_FLAT,
+ Action::TYPE_PAGE_TITLE => ArchivingHelper::ACTION_TABLE_MODE_FLAT,
+ ];
+ $dayTablesByType = $tablesByType;
+ $dayTablesByType[Action::TYPE_PAGE_URL] = $flatPageTablesByType[Action::TYPE_PAGE_URL];
+ $dayTablesByType[Action::TYPE_PAGE_TITLE] = $flatPageTablesByType[Action::TYPE_PAGE_TITLE];
+
+ $this->archiveDayActions(
+ $archiveProcessor,
+ $rankingQueryLimit,
+ $dayTablesByType,
+ array_diff(array_keys($dayTablesByType), [Action::TYPE_SITE_SEARCH]),
+ true,
+ $tableModesByType
+ );
+
+ if ($archiveProcessor->getParams()->getSite()->isSiteSearchEnabled()) {
+ $rankingQueryLimitSiteSearch = max($rankingQueryLimit, ArchivingHelper::$maximumRowsInDataTableSiteSearch);
+ $this->archiveDayActions(
+ $archiveProcessor,
+ $rankingQueryLimitSiteSearch,
+ $dayTablesByType,
+ [Action::TYPE_SITE_SEARCH],
+ false,
+ $tableModesByType
+ );
+ }
+
+ $this->archiveDayEntryActions($archiveProcessor->getLogAggregator(), $dayTablesByType, $rankingQueryLimit, $tableModesByType);
+ $this->archiveDayExitActions($archiveProcessor->getLogAggregator(), $dayTablesByType, $rankingQueryLimit, $tableModesByType);
+ $this->archiveDayActionsTime($archiveProcessor->getLogAggregator(), $dayTablesByType, $rankingQueryLimit, $tableModesByType);
+ $this->archiveDayActionsGoals($archiveProcessor, $rankingQueryLimit);
+ ArchivingHelper::normalizeFlatGoalsMetricsForHierarchy($flatPageTablesByType);
+
+ ArchivingHelper::clearActionsCache();
+
+ $tablesByType[Action::TYPE_PAGE_URL] = $this->buildDayHierarchicalTableFromFlatTable(
+ $flatPageTablesByType[Action::TYPE_PAGE_URL],
+ [$this, 'flatRowToUrlHierarchyPath']
+ );
+ $tablesByType[Action::TYPE_PAGE_TITLE] = $this->buildDayHierarchicalTableFromFlatTable(
+ $flatPageTablesByType[Action::TYPE_PAGE_TITLE],
+ [$this, 'flatRowToTitleHierarchyPath']
+ );
+ $this->finalizeBuiltFromFlatHierarchyTable(
+ $archiveProcessor,
+ $tablesByType[Action::TYPE_PAGE_URL],
+ Archiver::PAGE_URLS_RECORD_NAME
+ );
+ $this->finalizeBuiltFromFlatHierarchyTable(
+ $archiveProcessor,
+ $tablesByType[Action::TYPE_PAGE_TITLE],
+ Archiver::PAGE_TITLES_RECORD_NAME
+ );
+
+ $dataTable = $tablesByType[Action::TYPE_SITE_SEARCH];
+ $this->deleteUnusedColumnsFromKeywordsDataTable($dataTable);
+
+ foreach ($tablesByType as $actionType => $dataTable) {
+ if (
+ $actionType === Action::TYPE_PAGE_URL
+ || $actionType === Action::TYPE_PAGE_TITLE
+ ) {
+ continue;
+ }
+
+ ArchivingHelper::deleteInvalidSummedColumnsFromDataTable($dataTable);
+ }
+ foreach ($flatPageTablesByType as $flatDataTable) {
+ // also removes unique visitor columns from the summary row, as those cannot
+ // be summed and would otherwise be aggregated into non-day periods
+ ArchivingHelper::deleteInvalidSummedColumnsFromDataTable($flatDataTable);
+ }
+
+ $nbSearches = array_sum($dataTable->getColumn(PiwikMetrics::INDEX_PAGE_NB_HITS));
+ $nbKeywords = $dataTable->getRowsCount();
+
+ $dataTable = $tablesByType[Action::TYPE_OUTLINK];
+ $nbOutlinks = array_sum($dataTable->getColumn(PiwikMetrics::INDEX_PAGE_NB_HITS));
+ $nbUniqOutlinks = array_sum($dataTable->getColumn(PiwikMetrics::INDEX_NB_VISITS));
+
+ $dataTable = $flatPageTablesByType[Action::TYPE_PAGE_URL];
+ $nbPageviews = array_sum($dataTable->getColumn(PiwikMetrics::INDEX_PAGE_NB_HITS));
+ $nbUniqPageviews = array_sum($dataTable->getColumn(PiwikMetrics::INDEX_NB_VISITS));
+ $nbSumTimeGeneration = array_sum($dataTable->getColumn(PiwikMetrics::INDEX_PAGE_SUM_TIME_GENERATION));
+ $nbHitsWithTimeGeneration = array_sum($dataTable->getColumn(PiwikMetrics::INDEX_PAGE_NB_HITS_WITH_TIME_GENERATION));
+
+ $dataTable = $tablesByType[Action::TYPE_DOWNLOAD];
+ $nbDownloads = array_sum($dataTable->getColumn(PiwikMetrics::INDEX_PAGE_NB_HITS));
+ $nbUniqDownloads = array_sum($dataTable->getColumn(PiwikMetrics::INDEX_NB_VISITS));
+
+ return [
+ // blob records
+ Archiver::PAGE_URLS_FLAT_RECORD_NAME => $flatPageTablesByType[Action::TYPE_PAGE_URL],
+ Archiver::PAGE_TITLES_FLAT_RECORD_NAME => $flatPageTablesByType[Action::TYPE_PAGE_TITLE],
+ Archiver::PAGE_URLS_RECORD_NAME => $tablesByType[Action::TYPE_PAGE_URL],
+ Archiver::PAGE_TITLES_RECORD_NAME => $tablesByType[Action::TYPE_PAGE_TITLE],
+ Archiver::DOWNLOADS_RECORD_NAME => $tablesByType[Action::TYPE_DOWNLOAD],
+ Archiver::OUTLINKS_RECORD_NAME => $tablesByType[Action::TYPE_OUTLINK],
+ Archiver::SITE_SEARCH_RECORD_NAME => $tablesByType[Action::TYPE_SITE_SEARCH],
+
+ // numeric records
+ Archiver::METRIC_SEARCHES_RECORD_NAME => $nbSearches,
+ Archiver::METRIC_KEYWORDS_RECORD_NAME => $nbKeywords,
+
+ Archiver::METRIC_OUTLINKS_RECORD_NAME => $nbOutlinks,
+ Archiver::METRIC_UNIQ_OUTLINKS_RECORD_NAME => $nbUniqOutlinks,
+
+ Archiver::METRIC_PAGEVIEWS_RECORD_NAME => $nbPageviews,
+ Archiver::METRIC_UNIQ_PAGEVIEWS_RECORD_NAME => $nbUniqPageviews,
+ Archiver::METRIC_SUM_TIME_RECORD_NAME => $nbSumTimeGeneration,
+ Archiver::METRIC_HITS_TIMED_RECORD_NAME => $nbHitsWithTimeGeneration,
+
+ Archiver::METRIC_DOWNLOADS_RECORD_NAME => $nbDownloads,
+ Archiver::METRIC_UNIQ_DOWNLOADS_RECORD_NAME => $nbUniqDownloads,
+ ];
+ }
+
+ protected function beforeInsertBuiltFromFlatHierarchyRecord(
+ ArchiveProcessor $archiveProcessor,
+ Record $hierarchicalRecord,
+ DataTable $hierarchicalTable,
+ DataTable $flatTable
+ ): void {
+ $recordName = $hierarchicalRecord->getName();
+ if (
+ $recordName !== Archiver::PAGE_URLS_RECORD_NAME
+ && $recordName !== Archiver::PAGE_TITLES_RECORD_NAME
+ ) {
+ return;
+ }
+
+ $this->finalizeBuiltFromFlatHierarchyTable($archiveProcessor, $hierarchicalTable, $recordName);
+ }
+
+ private function flatRowToHierarchyPath(Row $flatRow, int $actionType): ?array
+ {
+ $label = $flatRow->getColumn('label');
+ if (!is_string($label) || $label === '') {
+ return null;
+ }
+
+ if ($actionType === Action::TYPE_PAGE_URL) {
+ if ($label === ArchivingHelper::getUnknownActionName(Action::TYPE_PAGE_URL)) {
+ return [$label];
+ }
+
+ return ArchivingHelper::getActionExplodedNames(
+ ArchivingHelper::removePageUrlLeafMarkerFromFlatLabel($label),
+ $actionType
+ );
+ }
+
+ return ArchivingHelper::getActionExplodedNames($label, $actionType);
+ }
+
+ private function removeFlatPathMetadataFromDataTable(DataTable $dataTable): void
+ {
+ $summaryRow = $dataTable->getRowFromId(DataTable::ID_SUMMARY_ROW);
+ if ($summaryRow instanceof Row) {
+ $summaryRow->deleteMetadata(ArchivingHelper::ACTION_FLAT_PATH_METADATA_NAME);
+ }
+
+ foreach ($dataTable->getRows() as $row) {
+ $row->deleteMetadata(ArchivingHelper::ACTION_FLAT_PATH_METADATA_NAME);
+
+ $subtable = $row->getSubtable();
+ if ($subtable instanceof DataTable) {
+ $this->removeFlatPathMetadataFromDataTable($subtable);
+ }
+ }
+ }
+
+ private function finalizeBuiltFromFlatHierarchyTable(
+ ArchiveProcessor $archiveProcessor,
+ DataTable $hierarchicalTable,
+ string $recordName
+ ): void {
+ $hierarchicalTable->setMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME, Metrics::getColumnsAggregationOperation());
+
+ if ($recordName === Archiver::PAGE_URLS_RECORD_NAME) {
+ $prefix = $archiveProcessor->getParams()->getSite()->getMainUrl();
+ $prefix = rtrim($prefix, '/') . '/';
+ ArchivingHelper::setFolderPathMetadata($hierarchicalTable, $isUrl = true, $prefix);
+ } elseif ($recordName === Archiver::PAGE_TITLES_RECORD_NAME) {
+ ArchivingHelper::setFolderPathMetadata($hierarchicalTable, $isUrl = false);
+ }
+
+ $this->removeFlatPathMetadataFromDataTable($hierarchicalTable);
+ ArchivingHelper::deleteInvalidSummedColumnsFromDataTable($hierarchicalTable);
+ }
+
+ private function isFlatArchivingEnabled(): bool
+ {
+ ArchivingHelper::reloadConfig();
+ return ArchivingHelper::$maximumRowsInDataTableFlat > 0;
+ }
+
+ private function setHierarchyBuiltFromFlatRecord(
+ Record $record,
+ string $flatRecordName,
+ callable $flatToHierarchyPathCallback,
+ callable $legacyHierarchyToFlatReducer
+ ): void {
+ $record->setBuiltFromFlatRecord(
+ $flatRecordName,
+ $flatToHierarchyPathCallback,
+ $legacyHierarchyToFlatReducer
+ )->setMaxRowsInTable(0)->setMaxRowsInSubtable(0);
+ }
+
+ private function buildDayHierarchicalTableFromFlatTable(
+ DataTable $flatTable,
+ callable $flatToHierarchyPathCallback
+ ): DataTable {
+ return $this->buildHierarchicalTableFromFlatTable(
+ $flatTable,
+ Metrics::getColumnsAggregationOperation(),
+ $flatToHierarchyPathCallback,
+ $this->getDefaultHierarchyRowColumns()
+ );
+ }
+
+ private function getDefaultHierarchyRowColumns(): array
+ {
+ return [
+ PiwikMetrics::INDEX_NB_VISITS => 0,
+ PiwikMetrics::INDEX_NB_UNIQ_VISITORS => 0,
+ PiwikMetrics::INDEX_PAGE_NB_HITS => 0,
+ PiwikMetrics::INDEX_PAGE_SUM_TIME_SPENT => 0,
+ ];
+ }
+
+ private function aggregateLegacyHierarchical(ArchiveProcessor $archiveProcessor): array
+ {
+ $rankingQueryLimit = ArchivingHelper::getRankingQueryLimit();
+ ArchivingHelper::reloadConfig();
+
+ $tablesByType = $this->makeReportTables();
+
+ $this->archiveDayActions(
+ $archiveProcessor,
+ $rankingQueryLimit,
+ $tablesByType,
+ array_diff(array_keys($tablesByType), [Action::TYPE_SITE_SEARCH]),
+ true
+ );
+
+ if ($archiveProcessor->getParams()->getSite()->isSiteSearchEnabled()) {
+ $rankingQueryLimitSiteSearch = max($rankingQueryLimit, ArchivingHelper::$maximumRowsInDataTableSiteSearch);
+ $this->archiveDayActions($archiveProcessor, $rankingQueryLimitSiteSearch, $tablesByType, [Action::TYPE_SITE_SEARCH], false);
+ }
+
+ $this->archiveDayEntryActions($archiveProcessor->getLogAggregator(), $tablesByType, $rankingQueryLimit);
+ $this->archiveDayExitActions($archiveProcessor->getLogAggregator(), $tablesByType, $rankingQueryLimit);
+ $this->archiveDayActionsTime($archiveProcessor->getLogAggregator(), $tablesByType, $rankingQueryLimit);
+ $this->archiveDayActionsGoals($archiveProcessor, $rankingQueryLimit);
+
+ ArchivingHelper::clearActionsCache();
+
+ $prefix = $archiveProcessor->getParams()->getSite()->getMainUrl();
+ $prefix = rtrim($prefix, '/') . '/';
+ ArchivingHelper::setFolderPathMetadata($tablesByType[Action::TYPE_PAGE_URL], $isUrl = true, $prefix);
+ ArchivingHelper::setFolderPathMetadata($tablesByType[Action::TYPE_PAGE_TITLE], $isUrl = false);
+
+ $dataTable = $tablesByType[Action::TYPE_SITE_SEARCH];
+ $this->deleteUnusedColumnsFromKeywordsDataTable($dataTable);
+
+ foreach ($tablesByType as $dataTable) {
+ ArchivingHelper::deleteInvalidSummedColumnsFromDataTable($dataTable);
+ }
+
+ $nbSearches = array_sum($dataTable->getColumn(PiwikMetrics::INDEX_PAGE_NB_HITS));
+ $nbKeywords = $dataTable->getRowsCount();
+
+ $dataTable = $tablesByType[Action::TYPE_OUTLINK];
+ $nbOutlinks = array_sum($dataTable->getColumn(PiwikMetrics::INDEX_PAGE_NB_HITS));
+ $nbUniqOutlinks = array_sum($dataTable->getColumn(PiwikMetrics::INDEX_NB_VISITS));
+
+ $dataTable = $tablesByType[Action::TYPE_PAGE_URL];
+ $nbPageviews = array_sum($dataTable->getColumn(PiwikMetrics::INDEX_PAGE_NB_HITS));
+ $nbUniqPageviews = array_sum($dataTable->getColumn(PiwikMetrics::INDEX_NB_VISITS));
+ $nbSumTimeGeneration = array_sum($dataTable->getColumn(PiwikMetrics::INDEX_PAGE_SUM_TIME_GENERATION));
+ $nbHitsWithTimeGeneration = array_sum($dataTable->getColumn(PiwikMetrics::INDEX_PAGE_NB_HITS_WITH_TIME_GENERATION));
+
+ $dataTable = $tablesByType[Action::TYPE_DOWNLOAD];
+ $nbDownloads = array_sum($dataTable->getColumn(PiwikMetrics::INDEX_PAGE_NB_HITS));
+ $nbUniqDownloads = array_sum($dataTable->getColumn(PiwikMetrics::INDEX_NB_VISITS));
+
+ return [
+ Archiver::PAGE_URLS_RECORD_NAME => $tablesByType[Action::TYPE_PAGE_URL],
+ Archiver::PAGE_TITLES_RECORD_NAME => $tablesByType[Action::TYPE_PAGE_TITLE],
+ Archiver::DOWNLOADS_RECORD_NAME => $tablesByType[Action::TYPE_DOWNLOAD],
+ Archiver::OUTLINKS_RECORD_NAME => $tablesByType[Action::TYPE_OUTLINK],
+ Archiver::SITE_SEARCH_RECORD_NAME => $tablesByType[Action::TYPE_SITE_SEARCH],
+
+ Archiver::METRIC_SEARCHES_RECORD_NAME => $nbSearches,
+ Archiver::METRIC_KEYWORDS_RECORD_NAME => $nbKeywords,
+ Archiver::METRIC_OUTLINKS_RECORD_NAME => $nbOutlinks,
+ Archiver::METRIC_UNIQ_OUTLINKS_RECORD_NAME => $nbUniqOutlinks,
+ Archiver::METRIC_PAGEVIEWS_RECORD_NAME => $nbPageviews,
+ Archiver::METRIC_UNIQ_PAGEVIEWS_RECORD_NAME => $nbUniqPageviews,
+ Archiver::METRIC_SUM_TIME_RECORD_NAME => $nbSumTimeGeneration,
+ Archiver::METRIC_HITS_TIMED_RECORD_NAME => $nbHitsWithTimeGeneration,
+ Archiver::METRIC_DOWNLOADS_RECORD_NAME => $nbDownloads,
+ Archiver::METRIC_UNIQ_DOWNLOADS_RECORD_NAME => $nbUniqDownloads,
+ ];
+ }
+
+ protected function deleteUnusedColumnsFromKeywordsDataTable(DataTable $dataTable): void
+ {
+ $columnsToDelete = array(
+ PiwikMetrics::INDEX_NB_UNIQ_VISITORS,
+ PiwikMetrics::INDEX_PAGE_IS_FOLLOWING_SITE_SEARCH_NB_HITS,
+ PiwikMetrics::INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS,
+ PiwikMetrics::INDEX_PAGE_ENTRY_NB_ACTIONS,
+ PiwikMetrics::INDEX_PAGE_ENTRY_SUM_VISIT_LENGTH,
+ PiwikMetrics::INDEX_PAGE_ENTRY_NB_VISITS,
+ PiwikMetrics::INDEX_PAGE_ENTRY_BOUNCE_COUNT,
+ PiwikMetrics::INDEX_PAGE_EXIT_NB_UNIQ_VISITORS,
+ );
+ $dataTable->deleteColumns($columnsToDelete);
+ }
+
+ private function makeReportTables(): array
+ {
+ $result = [];
+ foreach (Metrics::$actionTypes as $type) {
+ $dataTable = new DataTable();
+ if ($type === Action::TYPE_SITE_SEARCH) {
+ $maxRows = ArchivingHelper::$maximumRowsInDataTableSiteSearch;
+ } else {
+ $maxRows = ArchivingHelper::$maximumRowsInDataTableLevelZero;
+ }
+ $dataTable->setMaximumAllowedRows($maxRows);
+
+ if (
+ $type == Action::TYPE_PAGE_URL
+ || $type == Action::TYPE_PAGE_TITLE
+ ) {
+ // for page urls and page titles, performance metrics exist and have to be aggregated correctly
+ $dataTable->setMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME, Metrics::getColumnsAggregationOperation());
+ }
+
+ $result[$type] = $dataTable;
+ }
+ return $result;
+ }
+
+ private function makeFlatPageTables(): array
+ {
+ $tables = [];
+ $maxRowsInFlatTable = ArchivingHelper::$maximumRowsInDataTableFlat;
+ foreach ([Action::TYPE_PAGE_URL, Action::TYPE_PAGE_TITLE] as $type) {
+ $table = new DataTable();
+ $table->setMaximumAllowedRows($maxRowsInFlatTable);
+ $table->setMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME, Metrics::getColumnsAggregationOperation());
+ $tables[$type] = $table;
+ }
+
+ return $tables;
+ }
+
+ protected function archiveDayActions(
+ ArchiveProcessor $archiveProcessor,
+ int $rankingQueryLimit,
+ array $actionsTablesByType,
+ $actionTypes,
+ bool $includePageNotDefined,
+ array $tableModesByType = []
+ ): void {
+ $logAggregator = $archiveProcessor->getLogAggregator();
+
+ $metricsConfig = Metrics::getActionMetrics();
+
+ $select = "log_action.name,
+ log_action.type,
+ log_action.idaction,
+ log_action.url_prefix";
+
+ $select = $this->addMetricsToSelect($select, $metricsConfig);
+
+ $from = array(
+ "log_link_visit_action",
+ array(
+ "table" => "log_action",
+ "joinOn" => "log_link_visit_action.%s = log_action.idaction",
+ ),
+ );
+
+ $where = $logAggregator->getWhereStatement('log_link_visit_action', 'server_time');
+ $where .= " AND log_link_visit_action.%s IS NOT NULL"
+ . $this->getWhereClauseActionIsNotEvent();
+
+ $actionTypesWhere = "log_action.type IN (" . implode(", ", $actionTypes) . ")";
+ if ($includePageNotDefined) {
+ $actionTypesWhere = "(" . $actionTypesWhere . " OR log_action.type IS NULL)";
+ }
+ $where .= " AND $actionTypesWhere";
+
+ $groupBy = "log_link_visit_action.%s";
+ $orderBy = "`" . PiwikMetrics::INDEX_PAGE_NB_HITS . "` DESC, name ASC";
+
+ $siteSearchEnabled = $archiveProcessor->getParams()->getSite()->isSiteSearchEnabled();
+
+ $rankingQuery = false;
+ if ($rankingQueryLimit > 0) {
+ $rankingQuery = new RankingQuery($rankingQueryLimit);
+ $rankingQuery->addLabelColumn(array('idaction', 'name'));
+ $rankingQuery->addColumn('url_prefix');
+
+ if ($siteSearchEnabled) {
+ $rankingQuery->addColumn(PiwikMetrics::INDEX_SITE_SEARCH_HAS_NO_RESULT, 'min');
+ $rankingQuery->addColumn(PiwikMetrics::INDEX_PAGE_IS_FOLLOWING_SITE_SEARCH_NB_HITS, 'sum');
+ }
+
+ $this->addMetricsToRankingQuery($rankingQuery, $metricsConfig);
+
+ $rankingQuery->partitionResultIntoMultipleGroups('type', $actionTypes);
+ }
+
+ // Special Magic to get
+ // 1) No result Keywords
+ // 2) For each page view, count number of times the referrer page was a Site Search
+ if ($siteSearchEnabled) {
+ $this->updateQuerySelectFromForSiteSearch($select, $from);
+ }
+
+ $this->archiveDayQueryProcess(
+ $logAggregator,
+ $actionsTablesByType,
+ $select,
+ $from,
+ $where,
+ $groupBy,
+ $orderBy,
+ "idaction_name",
+ $rankingQuery,
+ $metricsConfig,
+ $tableModesByType
+ );
+ $this->archiveDayQueryProcess(
+ $logAggregator,
+ $actionsTablesByType,
+ $select,
+ $from,
+ $where,
+ $groupBy,
+ $orderBy,
+ "idaction_url",
+ $rankingQuery,
+ $metricsConfig,
+ $tableModesByType
+ );
+ }
+
+ /**
+ * Entry actions for Page URLs and Page names
+ */
+ protected function archiveDayEntryActions(LogAggregator $logAggregator, array $actionsTablesByType, int $rankingQueryLimit, array $tableModesByType = [])
+ {
+ $rankingQuery = false;
+ if ($rankingQueryLimit > 0) {
+ $rankingQuery = new RankingQuery($rankingQueryLimit);
+ $rankingQuery->addLabelColumn('idaction');
+ $rankingQuery->addColumn(PiwikMetrics::INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS);
+ $rankingQuery->addColumn(array(PiwikMetrics::INDEX_PAGE_ENTRY_NB_VISITS,
+ PiwikMetrics::INDEX_PAGE_ENTRY_NB_ACTIONS,
+ PiwikMetrics::INDEX_PAGE_ENTRY_SUM_VISIT_LENGTH,
+ PiwikMetrics::INDEX_PAGE_ENTRY_BOUNCE_COUNT), 'sum');
+ $rankingQuery->partitionResultIntoMultipleGroups('type', array_keys($actionsTablesByType));
+
+ $extraSelects = 'log_action.type, log_action.name,';
+ $from = array(
+ "log_visit",
+ array(
+ "table" => "log_action",
+ "joinOn" => "log_visit.%s = log_action.idaction",
+ ),
+ );
+ $orderBy = "`" . PiwikMetrics::INDEX_PAGE_ENTRY_NB_ACTIONS . "` DESC, log_action.name ASC";
+ } else {
+ $extraSelects = false;
+ $from = "log_visit";
+ $orderBy = false;
+ }
+
+ $select = "log_visit.%s as idaction, $extraSelects
+ count(distinct log_visit.idvisitor) as `" . PiwikMetrics::INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS . "`,
+ count(*) as `" . PiwikMetrics::INDEX_PAGE_ENTRY_NB_VISITS . "`,
+ sum(log_visit.visit_total_actions) as `" . PiwikMetrics::INDEX_PAGE_ENTRY_NB_ACTIONS . "`,
+ sum(log_visit.visit_total_time) as `" . PiwikMetrics::INDEX_PAGE_ENTRY_SUM_VISIT_LENGTH . "`,
+ sum(case log_visit.visit_total_actions when 1 then 1 when 0 then 1 else 0 end) as `" . PiwikMetrics::INDEX_PAGE_ENTRY_BOUNCE_COUNT . "`";
+
+ $where = $logAggregator->getWhereStatement('log_visit', 'visit_last_action_time');
+ $where .= " AND log_visit.%s > 0";
+
+ $groupBy = "log_visit.%s";
+
+ $this->archiveDayQueryProcess(
+ $logAggregator,
+ $actionsTablesByType,
+ $select,
+ $from,
+ $where,
+ $groupBy,
+ $orderBy,
+ "visit_entry_idaction_url",
+ $rankingQuery,
+ [],
+ $tableModesByType
+ );
+
+ $this->archiveDayQueryProcess(
+ $logAggregator,
+ $actionsTablesByType,
+ $select,
+ $from,
+ $where,
+ $groupBy,
+ $orderBy,
+ "visit_entry_idaction_name",
+ $rankingQuery,
+ [],
+ $tableModesByType
+ );
+ }
+
+ /**
+ * Exit actions
+ */
+ protected function archiveDayExitActions(LogAggregator $logAggregator, array $actionsTablesByType, int $rankingQueryLimit, array $tableModesByType = [])
+ {
+ $rankingQuery = false;
+ if ($rankingQueryLimit > 0) {
+ $rankingQuery = new RankingQuery($rankingQueryLimit);
+ $rankingQuery->addLabelColumn('idaction');
+ $rankingQuery->addColumn(PiwikMetrics::INDEX_PAGE_EXIT_NB_UNIQ_VISITORS);
+ $rankingQuery->addColumn(PiwikMetrics::INDEX_PAGE_EXIT_NB_VISITS, 'sum');
+ $rankingQuery->partitionResultIntoMultipleGroups('type', array_keys($actionsTablesByType));
+
+ $extraSelects = 'log_action.type, log_action.name,';
+ $from = array(
+ "log_visit",
+ array(
+ "table" => "log_action",
+ "joinOn" => "log_visit.%s = log_action.idaction",
+ ),
+ );
+ $orderBy = "`" . PiwikMetrics::INDEX_PAGE_EXIT_NB_VISITS . "` DESC, log_action.name ASC";
+ } else {
+ $extraSelects = false;
+ $from = "log_visit";
+ $orderBy = false;
+ }
+
+ $select = "log_visit.%s as idaction, $extraSelects
+ count(distinct log_visit.idvisitor) as `" . PiwikMetrics::INDEX_PAGE_EXIT_NB_UNIQ_VISITORS . "`,
+ count(*) as `" . PiwikMetrics::INDEX_PAGE_EXIT_NB_VISITS . "`";
+
+ $where = $logAggregator->getWhereStatement('log_visit', 'visit_last_action_time');
+ $where .= " AND log_visit.%s > 0";
+
+ $groupBy = "log_visit.%s";
+
+ $this->archiveDayQueryProcess(
+ $logAggregator,
+ $actionsTablesByType,
+ $select,
+ $from,
+ $where,
+ $groupBy,
+ $orderBy,
+ "visit_exit_idaction_url",
+ $rankingQuery,
+ [],
+ $tableModesByType
+ );
+
+ $this->archiveDayQueryProcess(
+ $logAggregator,
+ $actionsTablesByType,
+ $select,
+ $from,
+ $where,
+ $groupBy,
+ $orderBy,
+ "visit_exit_idaction_name",
+ $rankingQuery,
+ [],
+ $tableModesByType
+ );
+
+ return array($rankingQuery, $extraSelects, $from, $orderBy, $select, $where, $groupBy);
+ }
+
+ /**
+ * Time per action
+ */
+ protected function archiveDayActionsTime(LogAggregator $logAggregator, array $actionsTablesByType, int $rankingQueryLimit, array $tableModesByType = [])
+ {
+ $rankingQuery = false;
+ if ($rankingQueryLimit > 0) {
+ $rankingQuery = new RankingQuery($rankingQueryLimit);
+ $rankingQuery->addLabelColumn('idaction');
+ $rankingQuery->addColumn(PiwikMetrics::INDEX_PAGE_SUM_TIME_SPENT, 'sum');
+ $rankingQuery->partitionResultIntoMultipleGroups('type', array_keys($actionsTablesByType));
+
+ $extraSelects = "log_action.type, log_action.name, count(*) as `" . PiwikMetrics::INDEX_PAGE_NB_HITS . "`,";
+ $from = array(
+ "log_link_visit_action",
+ array(
+ "table" => "log_action",
+ "joinOn" => "log_link_visit_action.%s = log_action.idaction",
+ ),
+ );
+ $orderBy = "`" . PiwikMetrics::INDEX_PAGE_NB_HITS . "` DESC, log_action.name ASC";
+ } else {
+ $extraSelects = false;
+ $from = "log_link_visit_action";
+ $orderBy = false;
+ }
+
+ $select = "log_link_visit_action.%s as idaction, $extraSelects
+ sum(log_link_visit_action.time_spent_ref_action) as `" . PiwikMetrics::INDEX_PAGE_SUM_TIME_SPENT . "`";
+
+ $where = $logAggregator->getWhereStatement('log_link_visit_action', 'server_time');
+ $where .= " AND log_link_visit_action.time_spent_ref_action > 0
+ AND log_link_visit_action.%s > 0"
+ . $this->getWhereClauseActionIsNotEvent();
+
+ $groupBy = "log_link_visit_action.%s";
+
+ $this->archiveDayQueryProcess(
+ $logAggregator,
+ $actionsTablesByType,
+ $select,
+ $from,
+ $where,
+ $groupBy,
+ $orderBy,
+ "idaction_url_ref",
+ $rankingQuery,
+ [],
+ $tableModesByType
+ );
+
+ $this->archiveDayQueryProcess(
+ $logAggregator,
+ $actionsTablesByType,
+ $select,
+ $from,
+ $where,
+ $groupBy,
+ $orderBy,
+ "idaction_name_ref",
+ $rankingQuery,
+ [],
+ $tableModesByType
+ );
+ }
+
+ protected function archiveDayQueryProcess(
+ LogAggregator $logAggregator,
+ array $actionsTablesByType,
+ string $select,
+ $from,
+ string $where,
+ string $groupBy,
+ $orderBy,
+ string $sprintfField,
+ ?RankingQuery $rankingQuery = null,
+ array $metricsConfig = array(),
+ array $tableModesByType = []
+ ): void {
+ $select = sprintf($select, $sprintfField);
+
+ // get query with segmentation
+ $query = $logAggregator->generateQuery($select, $from, $where, $groupBy, $orderBy);
+
+ // replace the rest of the %s
+ $querySql = str_replace("%s", $sprintfField, $query['sql']);
+
+ // apply ranking query
+ if ($rankingQuery) {
+ $querySql = $rankingQuery->generateRankingQuery($querySql);
+ }
+
+ // get result
+ $resultSet = $logAggregator->getDb()->query($querySql, $query['bind']);
+ ArchivingHelper::updateActionsTableWithRowQuery(
+ $resultSet,
+ $sprintfField,
+ $actionsTablesByType,
+ $metricsConfig,
+ $tableModesByType
+ );
+ }
+
+ protected function updateQuerySelectFromForSiteSearch(string &$select, array &$from): void
+ {
+ $selectFlagNoResultKeywords = ",
+ MAX(log_link_visit_action.search_count) = 0 AS `" . PiwikMetrics::INDEX_SITE_SEARCH_HAS_NO_RESULT . "`";
+
+ // we need an extra JOIN to know whether the referrer "idaction_name_ref" was a Site Search request
+ $from[] = array(
+ "table" => "log_action",
+ "tableAlias" => "log_action_name_ref",
+ "joinOn" => "log_link_visit_action.idaction_name_ref = log_action_name_ref.idaction",
+ );
+
+ $selectPageIsFollowingSiteSearch = ",
+ SUM( CASE WHEN log_action_name_ref.type = " . Action::TYPE_SITE_SEARCH . "
+ THEN 1 ELSE 0 END)
+ AS `" . PiwikMetrics::INDEX_PAGE_IS_FOLLOWING_SITE_SEARCH_NB_HITS . "`";
+
+ $select .= $selectFlagNoResultKeywords
+ . $selectPageIsFollowingSiteSearch;
+ }
+
+ private function addMetricsToRankingQuery(RankingQuery $rankingQuery, array $metricsConfig): void
+ {
+ foreach ($metricsConfig as $metric => $config) {
+ if (!empty($config['aggregation'])) {
+ $rankingQuery->addColumn($metric, $config['aggregation']);
+ } else {
+ $rankingQuery->addColumn($metric);
+ }
+ }
+ }
+
+ private function addMetricsToSelect(string $select, array $metricsConfig): string
+ {
+ if (!empty($metricsConfig)) {
+ foreach ($metricsConfig as $metric => $config) {
+ $select .= ', ' . $config['query'] . " as `" . $metric . "`";
+ }
+ }
+
+ return $select;
+ }
+
+ protected function getWhereClauseActionIsNotEvent(): string
+ {
+ return " AND log_link_visit_action.idaction_event_category IS NULL";
+ }
+
+ /**
+ * Add goals data for each combination of url / title and pageviews / entries
+ */
+ protected function archiveDayActionsGoals(ArchiveProcessor $archiveProcessor, int $rankingQueryLimit): void
+ {
+ $site = $archiveProcessor->getParams()->getSite();
+
+ if (
+ !\Piwik\Common::isGoalPluginEnabled() ||
+ GeneralConfig::getConfigValue('disable_archive_actions_goals', $site->getId())
+ ) {
+ return;
+ }
+
+ $goals = $this->getGoalsForSite($site->getId());
+
+ // Add orders and abandoned cart codes if the site is enabled for ecommerce
+ if ($site->isEcommerceEnabled()) {
+ $goals[] = GoalManager::IDGOAL_CART;
+ $goals[] = GoalManager::IDGOAL_ORDER;
+ }
+
+ foreach ($goals as $idGoal) {
+ $this->archiveDayActionsGoalsPages($archiveProcessor->getLogAggregator(), true, $idGoal);
+ $this->archiveDayActionsGoalsPages($archiveProcessor->getLogAggregator(), false, $idGoal);
+ }
+
+ $this->archiveDayActionsGoalsPagesEntry($archiveProcessor, $rankingQueryLimit, true);
+ $this->archiveDayActionsGoalsPagesEntry($archiveProcessor, $rankingQueryLimit, false);
+ }
+
+ /**
+ * Query goal page view data and update actions data table
+ *
+ * @param bool $isUrl If true then query goal data by url, else by name
+ * @param int $idGoal Goal to archive
+ *
+ * @return int|null Count of records processed
+ * @throws \Exception
+ */
+ protected function archiveDayActionsGoalsPages(LogAggregator $logAggregator, bool $isUrl, int $idGoal): ?int
+ {
+ $linkField = ($isUrl ? 'idaction_url' : 'idaction_name');
+ $resultSet = $logAggregator->queryConversionsByPageView($linkField, $idGoal);
+ if (!$resultSet) {
+ return null;
+ }
+ return ArchivingHelper::updateActionsTableWithGoals($resultSet, true);
+ }
+
+ /**
+ * Get a list of goal ids for a site
+ *
+ * @return array
+ */
+ private function getGoalsForSite(string $idSite): array
+ {
+ $cache = Cache::getTransientCache();
+ $key = 'ActionArchives_allGoalIds_' . $idSite;
+
+ if ($cache->contains($key)) {
+ return $cache->fetch($key);
+ }
+
+ $siteGoals = Request::processRequest('Goals.getGoals', ['idSite' => $idSite, 'filter_limit' => '-1'], $default = []);
+ $goalIds = array_column($siteGoals, 'idgoal');
+
+ $cache->save($key, $goalIds);
+ return $goalIds;
+ }
+
+ /**
+ * Query goal entry page data and update actions data table
+ *
+ * @param bool $isUrl If true then query goal data by url, else by name
+ *
+ * @return int|null Count of records processed
+ * @throws \Exception
+ */
+ protected function archiveDayActionsGoalsPagesEntry(ArchiveProcessor $archiveProcessor, int $rankingQueryLimit, bool $isUrl): ?int
+ {
+ if (GeneralConfig::getConfigValue('disable_archive_actions_goals', $archiveProcessor->getParams()->getSite()->getId())) {
+ return null;
+ }
+ $linkField = ($isUrl ? 'visit_entry_idaction_url' : 'visit_entry_idaction_name');
+ $resultSet = $archiveProcessor->getLogAggregator()->queryConversionsByEntryPageView($linkField, $rankingQueryLimit);
+ if (!$resultSet) {
+ return null;
+ }
+
+ return ArchivingHelper::updateActionsTableWithGoals($resultSet, false);
+ }
+}
diff --git a/plugins/Actions/RecordBuilders/Hits.php b/plugins/Actions/RecordBuilders/Hits.php
new file mode 100644
index 0000000000000000000000000000000000000000..80b0a38c2b0766753b34091cff469837945b5d9e
--- /dev/null
+++ b/plugins/Actions/RecordBuilders/Hits.php
@@ -0,0 +1,42 @@
+getLogAggregator()->queryActionsByDimension(
+ [],
+ '',
+ ['count(distinct log_link_visit_action.idlink_va) as `' . PiwikMetrics::INDEX_NB_HITS . '`'],
+ [],
+ null
+ );
+ $data = $query->fetch();
+ $nbHits = $data[PiwikMetrics::INDEX_NB_HITS];
+
+ return [
+ Archiver::METRIC_HITS_RECORD_NAME => $nbHits,
+ ];
+ }
+}
diff --git a/plugins/Actions/RecordBuilders/SearchCategories.php b/plugins/Actions/RecordBuilders/SearchCategories.php
new file mode 100644
index 0000000000000000000000000000000000000000..240074d6b70fc35c637e124efc4765c04a0627fa
--- /dev/null
+++ b/plugins/Actions/RecordBuilders/SearchCategories.php
@@ -0,0 +1,52 @@
+getLogAggregator();
+ $query = $logAggregator->queryActionsByDimension(['search_cat'], "%s.search_cat != '' AND %s.search_cat IS NOT NULL");
+
+ $table = new DataTable();
+
+ while ($row = $query->fetch()) {
+ $label = $row['search_cat'];
+ unset($row['search_cat']);
+
+ $row = new DataTable\Row([DataTable\Row::COLUMNS => ['label' => $label] + $row]);
+ $table->addRow($row);
+ }
+
+ return [
+ Archiver::SITE_SEARCH_CATEGORY_RECORD_NAME => $table,
+ ];
+ }
+
+ public function getRecordMetadata(ArchiveProcessor $archiveProcessor): array
+ {
+ return [
+ Record::make(Record::TYPE_BLOB, Archiver::SITE_SEARCH_CATEGORY_RECORD_NAME)
+ ->setMaxRowsInTable(ArchivingHelper::$maximumRowsInDataTableSiteSearch),
+ ];
+ }
+
+ public function isEnabled(ArchiveProcessor $archiveProcessor): bool
+ {
+ return $archiveProcessor->getParams()->getSite()->isSiteSearchEnabled();
+ }
+}
diff --git a/plugins/Actions/Reports/Base.php b/plugins/Actions/Reports/Base.php
new file mode 100644
index 0000000000000000000000000000000000000000..79c13a0cc13887d8e53159ae0d1b47e2a4bc84cd
--- /dev/null
+++ b/plugins/Actions/Reports/Base.php
@@ -0,0 +1,159 @@
+categoryId = 'General_Actions';
+ $this->processedMetrics = [];
+ $this->recursiveLabelSeparator = '/';
+ }
+
+ protected function addBaseDisplayProperties(ViewDataTable $view)
+ {
+ $view->config->datatable_js_type = 'ActionsDataTable';
+ $view->config->search_recursive = true;
+ $view->config->show_table_all_columns = false;
+ $view->requestConfig->filter_limit = Actions::ACTIONS_REPORT_ROWS_DISPLAY;
+ $view->config->show_all_views_icons = false;
+
+ if ($view->requestConfig->getRequestParam('performance') === '1') {
+ $view->requestConfig->filter_limit = 25;
+ // hide visualization selector
+ $view->config->footer_icons = [[
+ 'class' => 'tableAllColumnsSwitch',
+ 'buttons' => [],
+ ]];
+ }
+
+ if ($view->isViewDataTableId(HtmlTable::ID)) {
+ $view->config->show_embedded_subtable = true;
+ }
+
+ if (Request::shouldLoadExpanded()) {
+ if ($view->isViewDataTableId(HtmlTable::ID)) {
+ $view->config->show_expanded = true;
+ }
+
+ $view->config->filters[] = function ($dataTable) {
+ Actions::setDataTableRowLevels($dataTable);
+ };
+ }
+
+ $view->config->filters[] = function ($dataTable) use ($view) {
+ if ($view->isViewDataTableId(HtmlTable::ID)) {
+ $view->config->datatable_css_class = 'dataTableActions';
+ }
+ };
+ }
+
+ protected function addPageDisplayProperties(ViewDataTable $view)
+ {
+ $view->config->addTranslations(array(
+ 'nb_hits' => Piwik::translate('General_ColumnPageviews'),
+ 'nb_visits' => Piwik::translate('General_ColumnUniquePageviews'),
+ ));
+
+ $formatter = new Formatter();
+
+ // add avg_generation_time tooltip
+ $tooltipCallback = function ($hits, $min, $max) use ($formatter) {
+ if (!$hits) {
+ return false;
+ }
+
+ return Piwik::translate("Actions_AvgGenerationTimeTooltip", array(
+ $hits,
+ "
",
+ $formatter->getPrettyTimeFromSeconds($min, true),
+ $formatter->getPrettyTimeFromSeconds($max, true),
+ ));
+ };
+ $view->config->filters[] = array('ColumnCallbackAddMetadata',
+ array(
+ array('nb_hits_with_time_generation', 'min_time_generation', 'max_time_generation'),
+ 'avg_time_generation_tooltip',
+ $tooltipCallback,
+ ),
+ );
+
+ $this->addExcludeLowPopDisplayProperties($view);
+
+ // hide the performance columns viz in page reports when not displayed as widget
+ if ($view->requestConfig->getRequestParam('widget') != '1') {
+ $view->config->show_table_performance = false;
+ }
+ }
+
+ protected function addExcludeLowPopDisplayProperties(ViewDataTable $view)
+ {
+ if (Common::getRequestVar('enable_filter_excludelowpop', '0', 'string') != '0') {
+ if (Common::getRequestVar('flat', 0, 'int') === 1) {
+ $view->requestConfig->filter_excludelowpop = 'nb_hits';
+ } else {
+ $view->requestConfig->filter_excludelowpop = Metrics::INDEX_PAGE_NB_HITS;
+ }
+ $view->requestConfig->filter_excludelowpop_value = function () {
+ // computing minimum value to exclude (2 percent of the total number of actions)
+ $visitsInfo = \Piwik\Plugins\VisitsSummary\Controller::getVisitsSummary()->getFirstRow();
+ $nbActions = $visitsInfo->getColumn('nb_actions');
+ $nbActionsLowPopulationThreshold = floor(0.02 * $nbActions);
+
+ // we remove 1 to make sure some actions/downloads are displayed in the case we have a very few of them
+ // and each of them has 1 or 2 hits...
+ return min($visitsInfo->getColumn('max_actions') - 1, $nbActionsLowPopulationThreshold - 1);
+ };
+ }
+ }
+
+ public function getRecursiveLabelSeparator()
+ {
+ $legacyDelimiter = GeneralConfig::getConfigValue('action_category_delimiter');
+ if (!empty($legacyDelimiter)) {
+ return $legacyDelimiter;
+ }
+
+ if (
+ $this->dimension instanceof PageTitle
+ || $this->dimension instanceof EntryPageTitle
+ || $this->dimension instanceof ExitPageTitle
+ ) {
+ return GeneralConfig::getConfigValue('action_title_category_delimiter') ?: $this->recursiveLabelSeparator;
+ }
+
+ if (
+ $this->dimension instanceof PageUrl
+ || $this->dimension instanceof EntryPageUrl
+ || $this->dimension instanceof ExitPageUrl
+ ) {
+ return GeneralConfig::getConfigValue('action_url_category_delimiter') ?: $this->recursiveLabelSeparator;
+ }
+
+ return parent::getRecursiveLabelSeparator();
+ }
+}
diff --git a/plugins/Actions/Reports/Get.php b/plugins/Actions/Reports/Get.php
new file mode 100644
index 0000000000000000000000000000000000000000..8f5e7479e631a70588f9f922d59d5234f151b7c4
--- /dev/null
+++ b/plugins/Actions/Reports/Get.php
@@ -0,0 +1,39 @@
+name = Piwik::translate('General_Actions') . ' - ' . Piwik::translate('General_MainMetrics');
+ $this->documentation = Piwik::translate('Actions_MainMetricsReportDocumentation');
+ $this->order = 1;
+ $this->processedMetrics = array(
+ new AveragePageGenerationTime(),
+ );
+ $this->metrics = array(
+ 'nb_pageviews',
+ 'nb_uniq_pageviews',
+ 'nb_downloads',
+ 'nb_uniq_downloads',
+ 'nb_outlinks',
+ 'nb_uniq_outlinks',
+ 'nb_searches',
+ 'nb_keywords',
+ 'hits',
+ );
+ }
+}
diff --git a/plugins/Actions/Reports/GetDownloads.php b/plugins/Actions/Reports/GetDownloads.php
new file mode 100644
index 0000000000000000000000000000000000000000..ca5c2143da5a8e343e79069834e78b2cc5d18e4a
--- /dev/null
+++ b/plugins/Actions/Reports/GetDownloads.php
@@ -0,0 +1,56 @@
+dimension = new DownloadUrl();
+ $this->name = Piwik::translate('General_Downloads');
+ $this->documentation = Piwik::translate('Actions_DownloadsReportDocumentation', '
');
+ $this->metrics = array('nb_visits', 'nb_hits');
+
+ $this->actionToLoadSubTables = $this->action;
+ $this->order = 9;
+
+ $this->subcategoryId = 'General_Downloads';
+ }
+
+ public function getMetrics()
+ {
+ return array(
+ 'nb_visits' => Piwik::translate('Actions_ColumnUniqueDownloads'),
+ 'nb_hits' => Piwik::translate('General_Downloads'),
+ );
+ }
+
+ protected function getMetricsDocumentation()
+ {
+ return array(
+ 'nb_visits' => Piwik::translate('Actions_ColumnUniqueClicksDocumentation'),
+ 'nb_hits' => Piwik::translate('Actions_ColumnClicksDocumentation'),
+ );
+ }
+
+ public function configureView(ViewDataTable $view)
+ {
+ $view->config->columns_to_display = array('label', 'nb_visits', 'nb_hits');
+ $view->config->show_exclude_low_population = false;
+
+ $this->addBaseDisplayProperties($view);
+ }
+}
diff --git a/plugins/Actions/Reports/GetEntryPageTitles.php b/plugins/Actions/Reports/GetEntryPageTitles.php
new file mode 100644
index 0000000000000000000000000000000000000000..6dcb5593d970cffa67f74c2b90906bc8cc60cf72
--- /dev/null
+++ b/plugins/Actions/Reports/GetEntryPageTitles.php
@@ -0,0 +1,95 @@
+dimension = new EntryPageTitle();
+ $this->name = Piwik::translate('Actions_EntryPageTitles');
+ $this->documentation = Piwik::translate('Actions_EntryPageTitlesReportDocumentation', '
')
+ . ' ' . Piwik::translate('General_UsePlusMinusIconsDocumentation');
+ $this->metrics = array('entry_nb_visits', 'entry_bounce_count');
+ $this->processedMetrics = array(
+ new AverageTimeOnPage(),
+ new BounceRate(),
+ new ExitRate(),
+ new AveragePageGenerationTime(),
+ );
+ $this->order = 6;
+ $this->actionToLoadSubTables = $this->action;
+ $this->subcategoryId = 'Actions_SubmenuPagesEntry';
+ $this->hasGoalMetrics = true;
+ }
+
+ public function configureWidgets(WidgetsList $widgetsList, ReportWidgetFactory $factory)
+ {
+ $widgetsList->addWidgetConfig($factory->createWidget()->setName('Actions_WidgetEntryPageTitles'));
+ }
+
+ public function getProcessedMetrics()
+ {
+ $result = parent::getProcessedMetrics();
+
+ // these metrics are not displayed in the API.getProcessedReport version of this report,
+ // so they are removed here.
+ unset($result['avg_time_on_page']);
+ unset($result['exit_rate']);
+
+ return $result;
+ }
+
+ protected function getMetricsDocumentation()
+ {
+ $metrics = parent::getMetricsDocumentation();
+ $metrics['bounce_rate'] = Piwik::translate('General_ColumnPageBounceRateDocumentation');
+
+ // remove these metrics from API.getProcessedReport version of this report
+ unset($metrics['avg_time_on_page']);
+ unset($metrics['exit_rate']);
+
+ return $metrics;
+ }
+
+ public function configureView(ViewDataTable $view)
+ {
+ $view->config->columns_to_display = array('label', 'entry_nb_visits', 'entry_bounce_count', 'bounce_rate');
+ $view->config->title = $this->name;
+
+ $view->requestConfig->filter_sort_column = 'entry_nb_visits';
+
+ $this->addPageDisplayProperties($view);
+ $this->addBaseDisplayProperties($view);
+
+ $view->config->show_goals = true;
+ }
+
+ public function getRelatedReports()
+ {
+ return array(
+ ReportsProvider::factory('Actions', 'getPageTitles'),
+ ReportsProvider::factory('Actions', 'getEntryPageUrls'),
+ );
+ }
+}
diff --git a/plugins/Actions/Reports/GetEntryPageUrls.php b/plugins/Actions/Reports/GetEntryPageUrls.php
new file mode 100644
index 0000000000000000000000000000000000000000..aa1e222e75ce01077fb5a2c6bcce8b6659339833
--- /dev/null
+++ b/plugins/Actions/Reports/GetEntryPageUrls.php
@@ -0,0 +1,88 @@
+dimension = new EntryPageUrl();
+ $this->name = Piwik::translate('Actions_SubmenuPagesEntry');
+ $this->documentation = Piwik::translate('Actions_EntryPagesReportDocumentation', '
')
+ . '
' . Piwik::translate('General_UsePlusMinusIconsDocumentation');
+
+ $this->metrics = array('entry_nb_visits', 'entry_bounce_count');
+ $this->processedMetrics = array(
+ new AverageTimeOnPage(),
+ new BounceRate(),
+ new ExitRate(),
+ new AveragePageGenerationTime(),
+ );
+ $this->order = 3;
+
+ $this->actionToLoadSubTables = $this->action;
+ $this->subcategoryId = 'Actions_SubmenuPagesEntry';
+ $this->hasGoalMetrics = true;
+ }
+
+ public function getProcessedMetrics()
+ {
+ $result = parent::getProcessedMetrics();
+
+ // these metrics are not displayed in the API.getProcessedReport version of this report,
+ // so they are removed here.
+ unset($result['avg_time_on_page']);
+ unset($result['exit_rate']);
+
+ return $result;
+ }
+
+ protected function getMetricsDocumentation()
+ {
+ $metrics = parent::getMetricsDocumentation();
+ $metrics['bounce_rate'] = Piwik::translate('General_ColumnPageBounceRateDocumentation');
+
+ unset($metrics['avg_time_on_page']);
+ unset($metrics['exit_rate']);
+
+ return $metrics;
+ }
+
+ public function configureView(ViewDataTable $view)
+ {
+ $view->config->title = $this->name;
+ $view->config->columns_to_display = array('label', 'entry_nb_visits', 'entry_bounce_count', 'bounce_rate');
+ $view->requestConfig->filter_sort_column = 'entry_nb_visits';
+ $view->requestConfig->filter_sort_order = 'desc';
+
+ $this->addPageDisplayProperties($view);
+ $this->addBaseDisplayProperties($view);
+
+ $view->config->show_goals = true;
+ }
+
+ public function getRelatedReports()
+ {
+ return array(
+ ReportsProvider::factory('Actions', 'getEntryPageTitles'),
+ );
+ }
+}
diff --git a/plugins/Actions/Reports/GetExitPageTitles.php b/plugins/Actions/Reports/GetExitPageTitles.php
new file mode 100644
index 0000000000000000000000000000000000000000..addfd426146a3a6557528f7f5f6fa39717fe5dfe
--- /dev/null
+++ b/plugins/Actions/Reports/GetExitPageTitles.php
@@ -0,0 +1,101 @@
+dimension = new ExitPageTitle();
+ $this->name = Piwik::translate('Actions_ExitPageTitles');
+ $this->documentation = Piwik::translate('Actions_ExitPageTitlesReportDocumentation', '
')
+ . ' ' . Piwik::translate('General_UsePlusMinusIconsDocumentation');
+ $this->subcategoryId = 'Actions_SubmenuPagesExit';
+
+ $this->metrics = array('exit_nb_visits', 'nb_visits');
+ $this->processedMetrics = array(
+ new AverageTimeOnPage(),
+ new BounceRate(),
+ new ExitRate(),
+ new AveragePageGenerationTime(),
+ );
+ $this->order = 7;
+
+ $this->actionToLoadSubTables = $this->action;
+ }
+
+ public function configureWidgets(WidgetsList $widgetsList, ReportWidgetFactory $factory)
+ {
+ // we have to do it manually since it's only done automatically if a subcategoryId is specified,
+ // we do not set a subcategoryId since this report is not supposed to be shown in the UI
+ $widgetsList->addWidgetConfig($factory->createWidget());
+ }
+
+ public function getProcessedMetrics()
+ {
+ $result = parent::getProcessedMetrics();
+
+ // these metrics are not displayed in the API.getProcessedReport version of this report,
+ // so they are removed here.
+ unset($result['bounce_rate']);
+ unset($result['avg_time_on_page']);
+
+ return $result;
+ }
+
+ public function getMetrics()
+ {
+ $metrics = parent::getMetrics();
+ $metrics['nb_visits'] = Piwik::translate('General_ColumnUniquePageviews');
+
+ return $metrics;
+ }
+
+ protected function getMetricsDocumentation()
+ {
+ $metrics = parent::getMetricsDocumentation();
+ $metrics['nb_visits'] = Piwik::translate('General_ColumnUniquePageviewsDocumentation');
+
+ unset($metrics['bounce_rate']);
+ unset($metrics['avg_time_on_page']);
+
+ return $metrics;
+ }
+
+ public function configureView(ViewDataTable $view)
+ {
+ $view->config->title = $this->name;
+ $view->config->columns_to_display = array('label', 'exit_nb_visits', 'nb_visits', 'exit_rate');
+
+ $this->addPageDisplayProperties($view);
+ $this->addBaseDisplayProperties($view);
+ }
+
+ public function getRelatedReports()
+ {
+ return array(
+ ReportsProvider::factory('Actions', 'getPageTitles'),
+ ReportsProvider::factory('Actions', 'getExitPageUrls'),
+ );
+ }
+}
diff --git a/plugins/Actions/Reports/GetExitPageUrls.php b/plugins/Actions/Reports/GetExitPageUrls.php
new file mode 100644
index 0000000000000000000000000000000000000000..095395a6237a926b3b41b94e85572557fb64d7ba
--- /dev/null
+++ b/plugins/Actions/Reports/GetExitPageUrls.php
@@ -0,0 +1,101 @@
+dimension = new ExitPageUrl();
+ $this->name = Piwik::translate('Actions_SubmenuPagesExit');
+ $this->documentation = Piwik::translate('Actions_ExitPagesReportDocumentation', '
')
+ . '
' . Piwik::translate('General_UsePlusMinusIconsDocumentation');
+
+ $this->metrics = array('exit_nb_visits', 'nb_visits');
+ $this->processedMetrics = array(
+ new AverageTimeOnPage(),
+ new BounceRate(),
+ new ExitRate(),
+ new AveragePageGenerationTime(),
+ );
+ $this->actionToLoadSubTables = $this->action;
+
+ $this->order = 4;
+
+ $this->subcategoryId = 'Actions_SubmenuPagesExit';
+ }
+
+ public function getProcessedMetrics()
+ {
+ $result = parent::getProcessedMetrics();
+
+ // these metrics are not displayed in the API.getProcessedReport version of this report,
+ // so they are removed here.
+ unset($result['bounce_rate']);
+ unset($result['avg_time_on_page']);
+
+ return $result;
+ }
+
+ public function getMetrics()
+ {
+ $metrics = parent::getMetrics();
+ $metrics['nb_visits'] = Piwik::translate('General_ColumnUniquePageviews');
+
+ unset($metrics['bounce_rate']);
+ unset($metrics['avg_time_on_page']);
+
+ return $metrics;
+ }
+
+ protected function getMetricsDocumentation()
+ {
+ $metrics = parent::getMetricsDocumentation();
+ $metrics['nb_visits'] = Piwik::translate('General_ColumnUniquePageviewsDocumentation');
+
+ return $metrics;
+ }
+
+ public function configureView(ViewDataTable $view)
+ {
+ $view->config->self_url = Request::getCurrentUrlWithoutGenericFilters(array(
+ 'module' => 'Actions',
+ 'action' => 'getExitPageUrls',
+ ));
+
+ $view->config->title = $this->name;
+
+ $view->config->columns_to_display = array('label', 'exit_nb_visits', 'nb_visits', 'exit_rate');
+ $view->requestConfig->filter_sort_column = 'exit_nb_visits';
+ $view->requestConfig->filter_sort_order = 'desc';
+
+ $this->addPageDisplayProperties($view);
+ $this->addBaseDisplayProperties($view);
+ }
+
+ public function getRelatedReports()
+ {
+ return array(
+ ReportsProvider::factory('Actions', 'getExitPageTitles'),
+ );
+ }
+}
diff --git a/plugins/Actions/Reports/GetOutlinks.php b/plugins/Actions/Reports/GetOutlinks.php
new file mode 100644
index 0000000000000000000000000000000000000000..9b2cda81d3d1ba80f02b5d40e4d1c2bfb3a46435
--- /dev/null
+++ b/plugins/Actions/Reports/GetOutlinks.php
@@ -0,0 +1,59 @@
+dimension = new ClickedUrl();
+ $this->name = Piwik::translate('General_Outlinks');
+ $this->documentation = Piwik::translate('Actions_OutlinksReportDocumentation') . ' '
+ . Piwik::translate('Actions_OutlinkDocumentation') . '
'
+ . Piwik::translate('General_UsePlusMinusIconsDocumentation');
+
+ $this->metrics = array('nb_visits', 'nb_hits');
+ $this->order = 8;
+
+ $this->actionToLoadSubTables = $this->action;
+
+ $this->subcategoryId = 'General_Outlinks';
+ }
+
+ public function getMetrics()
+ {
+ return array(
+ 'nb_visits' => Piwik::translate('Actions_ColumnUniqueClicks'),
+ 'nb_hits' => Piwik::translate('Actions_ColumnClicks'),
+ );
+ }
+
+ protected function getMetricsDocumentation()
+ {
+ return array(
+ 'nb_visits' => Piwik::translate('Actions_ColumnUniqueClicksDocumentation'),
+ 'nb_hits' => Piwik::translate('Actions_ColumnClicksDocumentation'),
+ );
+ }
+
+ public function configureView(ViewDataTable $view)
+ {
+ $view->config->columns_to_display = array('label', 'nb_visits', 'nb_hits');
+ $view->config->show_exclude_low_population = false;
+
+ $this->addBaseDisplayProperties($view);
+ }
+}
diff --git a/plugins/Actions/Reports/GetPageTitles.php b/plugins/Actions/Reports/GetPageTitles.php
new file mode 100644
index 0000000000000000000000000000000000000000..d08505d598ad6888fca9fd2499918d04d24e6fcf
--- /dev/null
+++ b/plugins/Actions/Reports/GetPageTitles.php
@@ -0,0 +1,96 @@
+dimension = new PageTitle();
+ $this->name = Piwik::translate('Actions_SubmenuPageTitles');
+ $this->documentation = Piwik::translate(
+ 'Actions_PageTitlesReportDocumentation',
+ ['
', htmlentities('