|
| 1 | +<?php |
| 2 | + |
| 3 | +/** |
| 4 | + * Matomo - free/libre analytics platform |
| 5 | + * |
| 6 | + * @link https://matomo.org |
| 7 | + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later |
| 8 | + */ |
| 9 | + |
| 10 | +namespace Piwik\Plugins\Referrers\RecordBuilders; |
| 11 | + |
| 12 | +use Piwik\ArchiveProcessor; |
| 13 | +use Piwik\ArchiveProcessor\Record; |
| 14 | +use Piwik\ArchiveProcessor\RecordBuilder; |
| 15 | +use Piwik\Common; |
| 16 | +use Piwik\Config; |
| 17 | +use Piwik\DataAccess\LogAggregator; |
| 18 | +use Piwik\DataTable; |
| 19 | +use Piwik\Metrics; |
| 20 | +use Piwik\Plugins\Referrers\Archiver; |
| 21 | +use Piwik\RankingQuery; |
| 22 | +use Piwik\Tracker\PageUrl; |
| 23 | + |
| 24 | +class AIReferrers extends RecordBuilder |
| 25 | +{ |
| 26 | + private $rankingQueryLimit; |
| 27 | + |
| 28 | + public function __construct() |
| 29 | + { |
| 30 | + parent::__construct(); |
| 31 | + |
| 32 | + $this->columnToSortByBeforeTruncation = Metrics::INDEX_NB_VISITS; |
| 33 | + |
| 34 | + // Reading pre 2.0 config file settings |
| 35 | + $this->maxRowsInTable = @Config::getInstance()->General['datatable_archiving_maximum_rows_referers']; |
| 36 | + $this->maxRowsInSubtable = @Config::getInstance()->General['datatable_archiving_maximum_rows_subtable_referers']; |
| 37 | + if (empty($this->maxRowsInTable)) { |
| 38 | + $this->maxRowsInTable = Config::getInstance()->General['datatable_archiving_maximum_rows_referrers']; |
| 39 | + $this->maxRowsInSubtable = Config::getInstance()->General['datatable_archiving_maximum_rows_subtable_referrers']; |
| 40 | + } |
| 41 | + $this->rankingQueryLimit = $this->getRankingQueryLimit(); |
| 42 | + } |
| 43 | + |
| 44 | + public function getRecordMetadata(ArchiveProcessor $archiveProcessor): array |
| 45 | + { |
| 46 | + return [ |
| 47 | + Record::make(Record::TYPE_BLOB, Archiver::AI_ASSISTANTS_ENTRY_URL_RECORD_NAME), |
| 48 | + Record::make(Record::TYPE_BLOB, Archiver::AI_ASSISTANTS_ENTRY_TITLE_RECORD_NAME), |
| 49 | + |
| 50 | + Record::make(Record::TYPE_NUMERIC, Archiver::METRIC_DISTINCT_AI_ASSISTANT_RECORD_NAME) |
| 51 | + ->setIsCountOfBlobRecordRows(Archiver::AI_ASSISTANTS_ENTRY_URL_RECORD_NAME), |
| 52 | + ]; |
| 53 | + } |
| 54 | + |
| 55 | + protected function aggregate(ArchiveProcessor $archiveProcessor): array |
| 56 | + { |
| 57 | + $records = []; |
| 58 | + foreach ($this->getRecordNames() as $record) { |
| 59 | + $records[$record] = new DataTable(); |
| 60 | + } |
| 61 | + |
| 62 | + $logAggregator = $archiveProcessor->getLogAggregator(); |
| 63 | + |
| 64 | + $this->aggregateFromVisits($logAggregator, $records[Archiver::AI_ASSISTANTS_ENTRY_URL_RECORD_NAME], 'visit_entry_idaction_url'); |
| 65 | + $this->aggregateFromVisits($logAggregator, $records[Archiver::AI_ASSISTANTS_ENTRY_TITLE_RECORD_NAME], 'visit_entry_idaction_name'); |
| 66 | + |
| 67 | + $this->aggregateFromConversions($logAggregator, $records, ["referer_name"]); |
| 68 | + |
| 69 | + $numericRecords = [ |
| 70 | + Archiver::METRIC_DISTINCT_AI_ASSISTANT_RECORD_NAME => count($records[Archiver::AI_ASSISTANTS_ENTRY_URL_RECORD_NAME]->getRows()), |
| 71 | + ]; |
| 72 | + |
| 73 | + return array_merge($records, $numericRecords); |
| 74 | + } |
| 75 | + |
| 76 | + protected function getRecordNames() |
| 77 | + { |
| 78 | + return [ |
| 79 | + Archiver::AI_ASSISTANTS_ENTRY_URL_RECORD_NAME, |
| 80 | + Archiver::AI_ASSISTANTS_ENTRY_TITLE_RECORD_NAME, |
| 81 | + ]; |
| 82 | + } |
| 83 | + |
| 84 | + private function aggregateFromVisits(LogAggregator $logAggregator, DataTable $report, string $field): void |
| 85 | + { |
| 86 | + $resultSet = $this->queryAIReferrerEntryPages($logAggregator, $field); |
| 87 | + |
| 88 | + $actionRows = []; |
| 89 | + |
| 90 | + while ($row = $resultSet->fetch()) { |
| 91 | + if (!isset($row[Metrics::INDEX_NB_VISITS])) { |
| 92 | + return; |
| 93 | + } |
| 94 | + |
| 95 | + $label = $row['referer_name']; |
| 96 | + |
| 97 | + $pageUrlOrTitle = $row['action_name']; |
| 98 | + |
| 99 | + if (is_null($label)) { |
| 100 | + continue; |
| 101 | + } |
| 102 | + |
| 103 | + if (!is_null($pageUrlOrTitle)) { |
| 104 | + $actionRows[] = $row; |
| 105 | + continue; |
| 106 | + } |
| 107 | + |
| 108 | + $report->sumRowWithLabel($label, $this->makeVisitRow($row)); |
| 109 | + } |
| 110 | + |
| 111 | + foreach ($actionRows as $row) { |
| 112 | + if (!isset($row[Metrics::INDEX_NB_VISITS])) { |
| 113 | + return; |
| 114 | + } |
| 115 | + |
| 116 | + $label = $row['referer_name']; |
| 117 | + $pageUrlOrTitle = $row['action_name']; |
| 118 | + |
| 119 | + $tableRow = $report->getRowFromLabel($label); |
| 120 | + |
| 121 | + if (empty($tableRow)) { |
| 122 | + continue; |
| 123 | + } |
| 124 | + |
| 125 | + if ($field === 'visit_entry_idaction_url') { |
| 126 | + // make sure we always work with normalized URL no matter how the individual action stores it |
| 127 | + $normalized = PageUrl::normalizeUrl($pageUrlOrTitle); |
| 128 | + $pageUrlOrTitle = $normalized['url']; |
| 129 | + } |
| 130 | + |
| 131 | + $tableRow->sumRowWithLabelToSubtable($pageUrlOrTitle, $this->makeVisitRow($row)); |
| 132 | + } |
| 133 | + } |
| 134 | + |
| 135 | + public function makeVisitRow(array $row) |
| 136 | + { |
| 137 | + $metricIds = [ |
| 138 | + Metrics::INDEX_NB_UNIQ_VISITORS, |
| 139 | + Metrics::INDEX_NB_VISITS, |
| 140 | + Metrics::INDEX_NB_ACTIONS, |
| 141 | + Metrics::INDEX_NB_USERS, |
| 142 | + Metrics::INDEX_MAX_ACTIONS, |
| 143 | + Metrics::INDEX_SUM_VISIT_LENGTH, |
| 144 | + Metrics::INDEX_BOUNCE_COUNT, |
| 145 | + Metrics::INDEX_NB_VISITS_CONVERTED, |
| 146 | + ]; |
| 147 | + |
| 148 | + $columns = []; |
| 149 | + foreach ($metricIds as $id) { |
| 150 | + $columns[$id] = (float)($row[$id] ?? 0); |
| 151 | + } |
| 152 | + |
| 153 | + return $columns; |
| 154 | + } |
| 155 | + |
| 156 | + /** |
| 157 | + * @param DataTable[] $reports |
| 158 | + */ |
| 159 | + protected function aggregateFromConversions(LogAggregator $logAggregator, array $reports, array $dimensions): void |
| 160 | + { |
| 161 | + $where = 'referer_type = ' . Common::REFERRER_TYPE_AI_ASSISTANT; |
| 162 | + $query = $logAggregator->queryConversionsByDimension($dimensions, $where); |
| 163 | + while ($row = $query->fetch()) { |
| 164 | + $idGoal = (int) $row['idgoal']; |
| 165 | + $columns = [ |
| 166 | + Metrics::INDEX_GOALS => [ |
| 167 | + $idGoal => Metrics::makeGoalColumnsRow($idGoal, $row), |
| 168 | + ], |
| 169 | + ]; |
| 170 | + |
| 171 | + $this->aggregateConversionRow($row, $reports, $columns); |
| 172 | + } |
| 173 | + |
| 174 | + foreach ($reports as $dataTable) { |
| 175 | + $dataTable->filter(DataTable\Filter\EnrichRecordWithGoalMetricSums::class); |
| 176 | + } |
| 177 | + } |
| 178 | + |
| 179 | + /** |
| 180 | + * @param DataTable[] $reports |
| 181 | + */ |
| 182 | + protected function aggregateConversionRow(array $row, array $reports, array $columns): void |
| 183 | + { |
| 184 | + $reports[Archiver::AI_ASSISTANTS_ENTRY_URL_RECORD_NAME]->sumRowWithLabel($row['referer_name'], $columns); |
| 185 | + $reports[Archiver::AI_ASSISTANTS_ENTRY_TITLE_RECORD_NAME]->sumRowWithLabel($row['referer_name'], $columns); |
| 186 | + } |
| 187 | + |
| 188 | + protected function queryAIReferrerEntryPages(LogAggregator $logAggregator, string $actionIdField) |
| 189 | + { |
| 190 | + $metricsConfig = $logAggregator->getVisitsMetricFields(); |
| 191 | + $select = "log_visit.referer_name, COALESCE(log_action.name, '') as action_name"; |
| 192 | + |
| 193 | + $select = $this->addMetricsToSelect($select, $metricsConfig); |
| 194 | + |
| 195 | + $from = [ |
| 196 | + "log_visit", |
| 197 | + [ |
| 198 | + "table" => "log_action", |
| 199 | + "joinOn" => "log_visit.$actionIdField = log_action.idaction", |
| 200 | + ], |
| 201 | + ]; |
| 202 | + |
| 203 | + $where = $logAggregator->getWhereStatement('log_visit', 'visit_last_action_time'); |
| 204 | + $where .= ' AND log_visit.referer_type = ' . Common::REFERRER_TYPE_AI_ASSISTANT; |
| 205 | + |
| 206 | + $groupBy = "log_visit.referer_name, action_name"; |
| 207 | + $orderBy = "`" . Metrics::INDEX_NB_VISITS . "` DESC"; |
| 208 | + |
| 209 | + // get query with segmentation |
| 210 | + $query = $logAggregator->generateQuery( |
| 211 | + $select, |
| 212 | + $from, |
| 213 | + $where, |
| 214 | + $groupBy, |
| 215 | + $orderBy, |
| 216 | + $limit = 0, |
| 217 | + $offset = 0, |
| 218 | + true |
| 219 | + ); |
| 220 | + |
| 221 | + if ($this->rankingQueryLimit > 0) { |
| 222 | + $rankingQuery = new RankingQuery($this->rankingQueryLimit); |
| 223 | + $rankingQuery->addLabelColumn(['referer_name', 'action_name']); |
| 224 | + |
| 225 | + $rankingQuery->addColumn(array_keys($metricsConfig), 'sum'); |
| 226 | + |
| 227 | + foreach ($metricsConfig as $column => $config) { |
| 228 | + if (empty($config['aggregation'])) { |
| 229 | + continue; |
| 230 | + } |
| 231 | + $rankingQuery->addColumn($column, $config['aggregation']); |
| 232 | + } |
| 233 | + |
| 234 | + |
| 235 | + $query['sql'] = $rankingQuery->generateRankingQuery($query['sql'], true); |
| 236 | + } |
| 237 | + |
| 238 | + $db = $logAggregator->getDb(); |
| 239 | + return $db->query($query['sql'], $query['bind']); |
| 240 | + } |
| 241 | + |
| 242 | + private function getRankingQueryLimit(): int |
| 243 | + { |
| 244 | + $configGeneral = Config::getInstance()->General; |
| 245 | + $configLimit = max($configGeneral['archiving_ranking_query_row_limit'], 10 * $this->maxRowsInTable); |
| 246 | + return $configLimit == 0 ? 0 : max($configLimit, $this->maxRowsInTable); |
| 247 | + } |
| 248 | + |
| 249 | + private function addMetricsToSelect(string $select, array $metricsConfig): string |
| 250 | + { |
| 251 | + if (!empty($metricsConfig)) { |
| 252 | + foreach ($metricsConfig as $metric => $query) { |
| 253 | + $select .= ', ' . $query . " as `" . $metric . "`"; |
| 254 | + } |
| 255 | + } |
| 256 | + |
| 257 | + return $select; |
| 258 | + } |
| 259 | +} |
0 commit comments