idSite = Fixture::createWebsite('2026-01-01 00:00:00'); } public function tearDown(): void { Date::$now = null; parent::tearDown(); } public function testRealTimeApiRejectsLookbacksLongerThanTwelveHours(): void { $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('lastMinutes only accepts values between 1 and 720'); API::getInstance()->getAIChatbotsRealTime($this->idSite, 721); } public function testRealTimeApiRejectsNonPositiveLookbacks(): void { $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('lastMinutes only accepts values between 1 and 720'); API::getInstance()->getTopPageUrlsRealTime($this->idSite, 0); } public function testRealTimeApiReturnsDecoratedChatbotLabels(): void { $dao = new BotRequestsDao(); $dao->insert([ 'idsite' => $this->idSite, 'server_time' => '2026-01-01 11:45:00', 'idaction_url' => $this->createAction('example.com/realtime/a', Action::TYPE_PAGE_URL), 'bot_name' => 'ChatGPT-User', 'bot_type' => BotDetector::BOT_TYPE_AI_CHATBOT, 'http_status_code' => 200, ]); $table = API::getInstance()->getAIChatbotsRealTime($this->idSite, '30'); $rows = $table->getRows(); self::assertCount(1, $rows); self::assertSame('ChatGPT', $rows[0]->getColumn('label')); self::assertSame(1, $rows[0]->getColumn(Metrics::COLUMN_REQUESTS)); } public function testRealTimeReportsAreHiddenFromReportMetadata(): void { $metadata = MetadataApi::getInstance()->getReportMetadata($this->idSite); $uniqueIds = array_column($metadata, 'uniqueId'); self::assertNotContains('BotTracking_getAIChatbotsRealTime', $uniqueIds); self::assertNotContains('BotTracking_getTopPageUrlsRealTime', $uniqueIds); } public function testRealTimeReportFooterShowsConfiguredLimit(): void { $config = Config::getInstance(); $previousConfig = $config->General['live_ai_chatbots_top_page_urls_maximum_rows'] ?? null; $hadPreviousConfig = array_key_exists('live_ai_chatbots_top_page_urls_maximum_rows', $config->General); $config->General['live_ai_chatbots_top_page_urls_maximum_rows'] = 37; try { Manager::getInstance()->loadPluginTranslations(); $_GET['idSite'] = (string) $this->idSite; $_GET['date'] = 'today'; $_GET['period'] = 'day'; $view = ViewDataTableFactory::build( $defaultType = null, 'BotTracking.getTopPageUrlsRealTime', $controllerAction = 'BotTracking.getTopPageUrlsRealTime', $forceDefault = false, $loadViewDataTableParametersForUser = false ); self::assertStringContainsString('37', $view->config->show_footer_message); self::assertNotSame( 'BotTracking_AIChatbotsRealTimeReportLimitFooter', $view->config->show_footer_message ); } finally { unset($_GET['idSite'], $_GET['date'], $_GET['period']); if ($hadPreviousConfig) { $config->General['live_ai_chatbots_top_page_urls_maximum_rows'] = $previousConfig; } else { unset($config->General['live_ai_chatbots_top_page_urls_maximum_rows']); } } } public function testHiddenRealTimeReportsAreAddedToGlossaryPageItems(): void { $glossaryItems = [ 'metrics' => [ 'title' => Piwik::translate('General_Metrics'), 'entries' => [], ], 'reports' => [ 'title' => Piwik::translate('General_Reports'), 'entries' => [], ], ]; Piwik::postEvent('API.addGlossaryItems', array(&$glossaryItems)); $reportNames = array_column($glossaryItems['reports']['entries'], 'name'); $category = Piwik::translate('General_AIAssistants'); self::assertContains( sprintf('%s (%s)', Piwik::translate('BotTracking_AIChatbotsLast30MinutesTitle'), $category), $reportNames ); self::assertContains( sprintf('%s (%s)', Piwik::translate('BotTracking_AIChatbotsLast8HoursTitle'), $category), $reportNames ); self::assertContains( sprintf('%s (%s)', Piwik::translate('BotTracking_TopPageUrlsLast30MinutesTitle'), $category), $reportNames ); self::assertContains( sprintf('%s (%s)', Piwik::translate('BotTracking_TopPageUrlsLast8HoursTitle'), $category), $reportNames ); } public function testRequestsMetricAppearsInGlossaryMetrics(): void { $metrics = MetadataApi::getInstance()->getGlossaryMetrics($this->idSite); $metric = $this->findMetric($metrics, Metrics::COLUMN_REQUESTS); self::assertNotNull($metric); self::assertSame(Piwik::translate('BotTracking_ColumnRequests'), $metric['name']); self::assertSame( Piwik::translate('BotTracking_ColumnRequestsDocumentation'), $metric['documentation'] ); } private function createAction(string $name, int $type): int { $actionIds = TableLogAction::loadIdsAction([[$name, $type]]); return (int) reset($actionIds); } /** * @param array> $metrics * @return array|null */ private function findMetric(array $metrics, string $metricId): ?array { foreach ($metrics as $metric) { if (($metric['id'] ?? null) === $metricId) { return $metric; } } return null; } }