matomo / plugins /BotTracking /Dao /BotRequestsDao.php
Leon4gr45's picture
Upload source files chunk 18
78185c4 verified
Raw
History Blame Contribute Delete
11.6 kB
<?php
/**
* Matomo - free/libre analytics platform
*
* @link https://matomo.org
* @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
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);
}
/**
* Creates the log table
*/
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());
}
/**
* Insert a bot telemetry record
*
* @param array<string, scalar> $data The telemetry data to insert
* @return int The inserted record ID
*/
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();
}
/**
* Delete bot telemetry records older than a specified date
*
* @param Date $date Delete records older than this date
* @return int Number of deleted records
*/
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);
}
/**
* Delete bot telemetry records for specific sites
*
* @param array<int|string> $siteIds Delete records older than this date
* @return int Number of deleted records
*/
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]
);
}
/**
* @param int[] $idSites
*/
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()) {
/** @var array<string, int|string|null> $row */
$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;
}
/**
* @param int[] $idSites
*/
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()) {
/** @var array{label: string, requests: int|string} $row */
$table->addRow(new Row([
Row::COLUMNS => [
'label' => (string) $row['label'],
Metrics::COLUMN_REQUESTS => (int) $row[Metrics::COLUMN_REQUESTS],
],
]));
}
return $table;
}
/**
* @return int[]
*/
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;
});
}
/**
* @param int[] $idSites
* @return int[]
*/
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);
}
}