File size: 5,469 Bytes
78185c4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
<?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\RecordBuilders;

use Piwik\ArchiveProcessor;
use Piwik\ArchiveProcessor\Record;
use Piwik\ArchiveProcessor\RecordBuilder;
use Piwik\Common;
use Piwik\Config\GeneralConfig;
use Piwik\DataTable;
use Piwik\Plugins\BotTracking\Archiver;
use Piwik\Plugins\BotTracking\Dao\BotRequestsDao;
use Piwik\Plugins\BotTracking\Metrics;
use Piwik\Tracker\Action;

/**
 * Builds three flat blob records for the "AI Chatbots Content Requests" subcategory:
 *   - Pages record: page URLs requested by AI chatbots, with request counts and avg server-time/response-size.
 *   - Documents record: download URLs requested by AI chatbots, with the same metrics.
 *   - Broken Content record: page + document URLs that returned HTTP 4xx/5xx errors, with per-status counts.
 * All three records are keyed by full URL (single label dimension); no URL-path subtables.
 *
 * The per-URL page/document request query lives in {@see AIChatbotPageMetricsTrait} so the Favoured Pages
 * builder can reuse the exact same "AI chatbot request" counting logic.
 */
class AIChatbotContentReports extends RecordBuilder
{
    use AIChatbotPageMetricsTrait;

    /**
     * @var int
     */
    protected $maxRowsInTable;

    /**
     * @var int
     */
    private $rankingQueryLimit;

    public function __construct()
    {
        parent::__construct();

        $this->maxRowsInTable    = GeneralConfig::getIntegerConfigValue('datatable_archiving_maximum_rows_ai_chatbot_content', 50000);
        $this->rankingQueryLimit = $this->getRankingQueryLimit($this->maxRowsInTable);
    }

    public function getRecordMetadata(ArchiveProcessor $archiveProcessor): array
    {
        return [
            Record::make(Record::TYPE_BLOB, Archiver::AI_CHATBOTS_REQUESTED_PAGES_RECORD)
                ->setColumnToSortByBeforeTruncation(Metrics::COLUMN_REQUESTS),
            Record::make(Record::TYPE_BLOB, Archiver::AI_CHATBOTS_REQUESTED_DOCUMENTS_RECORD)
                ->setColumnToSortByBeforeTruncation(Metrics::COLUMN_REQUESTS),
            Record::make(Record::TYPE_BLOB, Archiver::AI_CHATBOTS_BROKEN_CONTENT_RECORD)
                ->setColumnToSortByBeforeTruncation(Metrics::COLUMN_TOTAL_BROKEN_REQUESTS),
        ];
    }

    public function isEnabled(ArchiveProcessor $archiveProcessor): bool
    {
        // don't process reports for any segment
        return $archiveProcessor->getParams()->getSegment()->isEmpty();
    }

    protected function aggregate(ArchiveProcessor $archiveProcessor): array
    {
        // The content reports expose the full metric set (requests + avg server-time/response-size + errors).
        $columns = array_keys($this->pageUrlMetricExpressions());

        $tables = [
            Archiver::AI_CHATBOTS_REQUESTED_PAGES_RECORD     => $this->queryPageOrDocumentUrls($archiveProcessor, Action::TYPE_PAGE_URL, $this->rankingQueryLimit, $columns),
            Archiver::AI_CHATBOTS_REQUESTED_DOCUMENTS_RECORD => $this->queryPageOrDocumentUrls($archiveProcessor, Action::TYPE_DOWNLOAD, $this->rankingQueryLimit, $columns),
            Archiver::AI_CHATBOTS_BROKEN_CONTENT_RECORD      => $this->queryBrokenContent($archiveProcessor),
        ];

        return $tables;
    }

    /**
     * Queries page and document URLs that returned HTTP errors (404/410/5xx) to AI chatbots.
     * The HAVING clause in the inner query ensures only URLs with at least one error row appear.
     */
    private function queryBrokenContent(ArchiveProcessor $archiveProcessor): DataTable
    {
        $logAggregator = $archiveProcessor->getLogAggregator();
        $where         = $logAggregator->getWhereStatement('bot', 'server_time');
        $botTable      = BotRequestsDao::getPrefixedTableName();
        $actionTable   = Common::prefixTable('log_action');

        $innerSql = sprintf(
            "SELECT log_action.name AS url,
                    SUM(bot.http_status_code IN (404, 410)) + SUM(bot.http_status_code BETWEEN 500 AND 599) AS %s,
                    SUM(bot.http_status_code IN (404, 410)) AS %s,
                    SUM(bot.http_status_code BETWEEN 500 AND 599) AS %s
             FROM `%s` AS bot
             INNER JOIN `%s` AS log_action ON log_action.idaction = bot.idaction_url
             WHERE log_action.name IS NOT NULL
               AND log_action.name <> ''
               AND log_action.type IN (%d, %d)
               AND bot.bot_type = ?
               AND %s
             GROUP BY log_action.name
             HAVING %s >= 1
             ORDER BY %s DESC",
            Metrics::COLUMN_TOTAL_BROKEN_REQUESTS,
            Metrics::COLUMN_PAGE_NOT_FOUND_404_REQUESTS,
            Metrics::COLUMN_SERVER_ERROR_5XX_REQUESTS,
            $botTable,
            $actionTable,
            Action::TYPE_PAGE_URL,
            Action::TYPE_DOWNLOAD,
            $where,
            Metrics::COLUMN_TOTAL_BROKEN_REQUESTS,
            Metrics::COLUMN_TOTAL_BROKEN_REQUESTS
        );

        $columns = [
            Metrics::COLUMN_TOTAL_BROKEN_REQUESTS       => 'sum',
            Metrics::COLUMN_PAGE_NOT_FOUND_404_REQUESTS => 'sum',
            Metrics::COLUMN_SERVER_ERROR_5XX_REQUESTS   => 'sum',
        ];

        return $this->executeUrlQuery($archiveProcessor, $innerSql, $columns, $this->rankingQueryLimit);
    }
}