| <?php |
|
|
| |
| |
| |
| |
| |
| |
|
|
| declare(strict_types=1); |
|
|
| namespace Piwik\Plugins\BotTracking\Dao; |
|
|
| use Piwik\Common; |
| use Piwik\Config\GeneralConfig; |
| use Piwik\DataTable; |
| use Piwik\DataTable\Row; |
| use Piwik\Date; |
| use Piwik\Db; |
| use Piwik\DbHelper; |
| use Piwik\Plugins\BotTracking\BotDetector; |
| use Piwik\Plugins\BotTracking\Metrics; |
| use Piwik\Tracker\Action; |
|
|
| class BotRequestsDao |
| { |
| private const CONFIG_LIVE_AI_CHATBOTS_MAXIMUM_ROWS = 'live_ai_chatbots_maximum_rows'; |
| private const CONFIG_LIVE_AI_CHATBOTS_TOP_PAGE_URLS_MAXIMUM_ROWS = 'live_ai_chatbots_top_page_urls_maximum_rows'; |
|
|
| public static function getTableName(): string |
| { |
| return 'log_bot_request'; |
| } |
|
|
| public static function getPrefixedTableName(): string |
| { |
| return Common::prefixTable(self::getTableName()); |
| } |
|
|
| public static function getAIChatbotActivityForDateRangeLimit(): int |
| { |
| return self::getRealTimeReportLimit(self::CONFIG_LIVE_AI_CHATBOTS_MAXIMUM_ROWS); |
| } |
|
|
| public static function getAIChatbotTopPageUrlsForDateRangeLimit(): int |
| { |
| return self::getRealTimeReportLimit(self::CONFIG_LIVE_AI_CHATBOTS_TOP_PAGE_URLS_MAXIMUM_ROWS); |
| } |
|
|
| |
| |
| |
| public function createTable(): void |
| { |
| $tableName = self::getTableName(); |
| $definition = ' |
| `idrequest` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, |
| `idsite` INT UNSIGNED NOT NULL, |
| `server_time` DATETIME NOT NULL, |
| `created_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, |
| `idaction_url` INT UNSIGNED NULL, |
| `bot_name` VARCHAR(100) NOT NULL, |
| `bot_type` VARCHAR(50) NOT NULL, |
| `http_status_code` SMALLINT UNSIGNED NULL, |
| `response_size_bytes` INT UNSIGNED NULL, |
| `response_time_ms` INT UNSIGNED NULL, |
| `source` VARCHAR(50) NULL, |
| PRIMARY KEY (`idrequest`), |
| INDEX `index_idsite_server_time` (`idsite`, `server_time`)'; |
|
|
| DbHelper::createTable($tableName, $definition); |
| } |
|
|
| public function dropTable(): void |
| { |
| Db::query('DROP TABLE IF EXISTS ' . self::getPrefixedTableName()); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public function insert(array $data): int |
| { |
| $tableName = self::getPrefixedTableName(); |
|
|
| $fields = [ |
| 'idsite', |
| 'server_time', |
| 'idaction_url', |
| 'bot_name', |
| 'bot_type', |
| 'http_status_code', |
| 'response_size_bytes', |
| 'response_time_ms', |
| 'source', |
| ]; |
|
|
| $values = []; |
| $bind = []; |
|
|
| foreach ($fields as $field) { |
| if (isset($data[$field])) { |
| $values[] = '?'; |
| $bind[] = $data[$field]; |
| } else { |
| $values[] = 'NULL'; |
| } |
| } |
|
|
| $sql = sprintf( |
| 'INSERT INTO `%s` (%s) VALUES (%s)', |
| $tableName, |
| implode(', ', $fields), |
| implode(', ', $values) |
| ); |
|
|
| Db::query($sql, $bind); |
|
|
| return (int)Db::get()->lastInsertId(); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public function deleteOldRecords(Date $date): int |
| { |
| $tableName = self::getPrefixedTableName(); |
|
|
| $sql = sprintf( |
| 'DELETE FROM `%s` WHERE server_time < ?', |
| $tableName |
| ); |
|
|
| $result = Db::query($sql, [$date->getDatetime()]); |
|
|
| return (int)Db::get()->rowCount($result); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public function deleteRecordsForIdSites(array $siteIds): int |
| { |
| $tableName = self::getPrefixedTableName(); |
| $siteIds = array_map('intval', $siteIds); |
|
|
| $sql = sprintf( |
| 'DELETE FROM `%s` WHERE idsite IN (' . implode(', ', $siteIds) . ') LIMIT 25000', |
| $tableName |
| ); |
|
|
| $result = Db::query($sql); |
|
|
| return (int)Db::get()->rowCount($result); |
| } |
|
|
| public function getLastServerTimeForSiteAndBotType(int $idSite, string $botType): ?string |
| { |
| $tableName = self::getPrefixedTableName(); |
|
|
| return Db::fetchOne( |
| sprintf( |
| 'SELECT MAX(server_time) FROM `%s` WHERE idsite = ? AND bot_type = ?', |
| $tableName |
| ), |
| [$idSite, $botType] |
| ); |
| } |
|
|
| |
| |
| |
| public function getAIChatbotActivityForDateRange(array $idSites, string $startDate, string $endDate): DataTable |
| { |
| $idSites = $this->normalizeIdSites($idSites); |
|
|
| if (empty($idSites)) { |
| return new DataTable(); |
| } |
|
|
| $limit = self::getAIChatbotActivityForDateRangeLimit(); |
|
|
| $idSitePlaceholders = Common::getSqlStringFieldsArray($idSites); |
| $tableName = self::getPrefixedTableName(); |
| $actionTable = Common::prefixTable('log_action'); |
|
|
| $sql = sprintf( |
| "SELECT bot.bot_name AS label, |
| COUNT(*) AS %s, |
| COUNT(DISTINCT CASE WHEN log_action.type = %d THEN log_action.name END) AS %s, |
| SUM(CASE WHEN bot.http_status_code IN (404, 410) THEN 1 ELSE 0 END) AS %s, |
| SUM(CASE WHEN bot.http_status_code BETWEEN 500 AND 599 THEN 1 ELSE 0 END) AS %s |
| FROM `%s` AS bot |
| LEFT JOIN `%s` AS log_action ON log_action.idaction = bot.idaction_url |
| WHERE bot.idsite IN (%s) |
| AND bot.bot_type = ? |
| AND bot.server_time >= ? |
| AND bot.server_time <= ? |
| GROUP BY bot.bot_name |
| ORDER BY %s DESC, bot.bot_name |
| LIMIT %d", |
| Metrics::COLUMN_REQUESTS, |
| Action::TYPE_PAGE_URL, |
| Metrics::METRIC_AI_CHATBOTS_UNIQUE_PAGE_URLS, |
| Metrics::METRIC_AI_CHATBOTS_NOT_FOUND_REQUESTS, |
| Metrics::METRIC_AI_CHATBOTS_SERVER_ERROR_REQUESTS, |
| $tableName, |
| $actionTable, |
| $idSitePlaceholders, |
| Metrics::COLUMN_REQUESTS, |
| $limit |
| ); |
|
|
| $bind = array_merge($idSites, [BotDetector::BOT_TYPE_AI_CHATBOT, $startDate, $endDate]); |
|
|
| $sql = $this->addRealTimeQueryMaxExecutionTimeHint($sql); |
|
|
| $stmt = Db::query($sql, $bind); |
| $table = new DataTable(); |
| $table->setMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME, [ |
| Metrics::METRIC_AI_CHATBOTS_UNIQUE_PAGE_URLS => 'skip', |
| ]); |
|
|
| while ($row = $stmt->fetch()) { |
| |
| $table->addRow(new Row([ |
| Row::COLUMNS => [ |
| 'label' => (string) $row['label'], |
| Metrics::COLUMN_REQUESTS => (int) $row[Metrics::COLUMN_REQUESTS], |
| Metrics::METRIC_AI_CHATBOTS_UNIQUE_PAGE_URLS => (int) $row[Metrics::METRIC_AI_CHATBOTS_UNIQUE_PAGE_URLS], |
| Metrics::METRIC_AI_CHATBOTS_NOT_FOUND_REQUESTS => (int) $row[Metrics::METRIC_AI_CHATBOTS_NOT_FOUND_REQUESTS], |
| Metrics::METRIC_AI_CHATBOTS_SERVER_ERROR_REQUESTS => (int) $row[Metrics::METRIC_AI_CHATBOTS_SERVER_ERROR_REQUESTS], |
| ], |
| ])); |
| } |
|
|
| return $table; |
| } |
|
|
| |
| |
| |
| public function getAIChatbotTopPageUrlsForDateRange(array $idSites, string $startDate, string $endDate): DataTable |
| { |
| $idSites = $this->normalizeIdSites($idSites); |
|
|
| if (empty($idSites)) { |
| return new DataTable(); |
| } |
|
|
| $limit = self::getAIChatbotTopPageUrlsForDateRangeLimit(); |
|
|
| $idSitePlaceholders = Common::getSqlStringFieldsArray($idSites); |
| $tableName = self::getPrefixedTableName(); |
| $actionTable = Common::prefixTable('log_action'); |
|
|
| $sql = sprintf( |
| "SELECT log_action.name AS label, |
| COUNT(*) AS %s |
| FROM `%s` AS bot |
| INNER JOIN `%s` AS log_action ON log_action.idaction = bot.idaction_url |
| WHERE bot.idsite IN (%s) |
| AND bot.bot_type = ? |
| AND bot.server_time >= ? |
| AND bot.server_time <= ? |
| AND log_action.name IS NOT NULL |
| AND log_action.name <> '' |
| AND log_action.type = %d |
| GROUP BY log_action.name |
| ORDER BY %s DESC, log_action.name |
| LIMIT %d", |
| Metrics::COLUMN_REQUESTS, |
| $tableName, |
| $actionTable, |
| $idSitePlaceholders, |
| Action::TYPE_PAGE_URL, |
| Metrics::COLUMN_REQUESTS, |
| $limit |
| ); |
|
|
| $bind = array_merge($idSites, [BotDetector::BOT_TYPE_AI_CHATBOT, $startDate, $endDate]); |
|
|
| $sql = $this->addRealTimeQueryMaxExecutionTimeHint($sql); |
|
|
| $stmt = Db::query($sql, $bind); |
| $table = new DataTable(); |
|
|
| while ($row = $stmt->fetch()) { |
| |
| $table->addRow(new Row([ |
| Row::COLUMNS => [ |
| 'label' => (string) $row['label'], |
| Metrics::COLUMN_REQUESTS => (int) $row[Metrics::COLUMN_REQUESTS], |
| ], |
| ])); |
| } |
|
|
| return $table; |
| } |
|
|
| |
| |
| |
| public function getDistinctIdSitesInTable(int $maxIdSite): array |
| { |
| $tableName = self::getPrefixedTableName(); |
| $idSitesLogTable = Db::fetchAll('SELECT DISTINCT idsite FROM ' . $tableName); |
| $idSitesLogTable = array_column($idSitesLogTable, 'idsite'); |
| $idSitesLogTable = array_map('intval', $idSitesLogTable); |
| return array_filter($idSitesLogTable, function ($idSite) use ($maxIdSite) { |
| return !empty($idSite) && $idSite <= $maxIdSite; |
| }); |
| } |
|
|
| |
| |
| |
| |
| private function normalizeIdSites(array $idSites): array |
| { |
| $idSites = array_map('intval', $idSites); |
| $idSites = array_filter($idSites, function (int $idSite): bool { |
| return $idSite > 0; |
| }); |
|
|
| return array_values(array_unique($idSites)); |
| } |
|
|
| private function addRealTimeQueryMaxExecutionTimeHint(string $sql): string |
| { |
| return DbHelper::addMaxExecutionTimeHintToQuery($sql, $this->getRealTimeQueryMaxExecutionTime()); |
| } |
|
|
| private static function getRealTimeReportLimit(string $configKey): int |
| { |
| $limit = GeneralConfig::getIntegerConfigValue($configKey); |
| if ($limit === null || $limit <= 0) { |
| throw new \UnexpectedValueException(sprintf('Config option "%s" must be greater than 0.', $configKey)); |
| } |
|
|
| return $limit; |
| } |
|
|
| private function getRealTimeQueryMaxExecutionTime(): float |
| { |
| return GeneralConfig::getFloatConfigValue('live_query_max_execution_time', 0.0); |
| } |
| } |
|
|