SSR cockpit: label the response time, leave test reports out of the client error counts (0114-FE)

The big response time number is labelled last check. ClientErrorClassifier tells smoke and curl test reports and bot reports apart; tests are counted separately and left out of the tile, the hourly bars, the list and the dashboard tile, bots keep a label.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
orascheandClaude Opus 5.5 committed 2026-11-12 11:42:57 +01:00
1 parent 4bc38b1760
commit 06b9bc8293
5 files changed
+125 -41

No files matched your search

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Evomedien\Vitec\Controller\Backend;
use Evomedien\Vitec\Service\ClientErrorClassifier;
use Evomedien\Vitec\Service\SsrPurgeService;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
@@ -68,7 +69,8 @@ final class SsrController
}
$healthLog = $this->rows('tx_vitec_ssrhealthlog', 10);
$lastSample = $healthLog[0] ?? null;
$clientErrors = $this->clientErrors(50);
$errorReport = $this->clientErrors(50);
$clientErrors = $errorReport['list'];
$cockpit = $this->cockpitData($health, $lastSample);
$alert = $this->purgeService->getAlertRecipients();
@@ -97,8 +99,10 @@ final class SsrController
'alertList' => implode(', ', $alert['recipients']),
'alertCount' => count($alert['recipients']),
'alertInvalid' => implode(', ', $alert['invalid']),
'chunkCount24h' => count(array_filter($clientErrors, static fn(array $r): bool => $r['is_chunk_reload'])),
'jsErrorCount24h' => count(array_filter($clientErrors, static fn(array $r): bool => !$r['is_chunk_reload'])),
'chunkCount24h' => $errorReport['chunk'],
'jsErrorCount24h' => $errorReport['js'],
'testCount24h' => $errorReport['tests'],
'botCount24h' => $errorReport['bots'],
'flagOn' => file_exists(Environment::getVarPath() . '/vitec-ssr-on.flag'),
'configured' => $this->purgeService->isConfigured(),
'health' => $health,
@@ -111,37 +115,51 @@ final class SsrController
'moduleUri' => (string)$this->uriBuilder->buildUriFromRoute('web_vitecssr'),
'healthLog' => $healthLog,
'clientErrors' => $clientErrors,
'clientErrorCount24h' => $this->clientErrorCount24h(),
'clientErrorCount24h' => $errorReport['real'],
]);
return $view->renderResponse('Ssr/Index');
}
/**
* Client error reports of the last 24 hours (POST /api/vitec/client-error,
* ClientErrorMiddleware), newest first. Rows carry a "host" (URL without
* scheme/query) and a shortened user agent for the table; the full
* values stay in the row for the details toggle.
* ClientErrorMiddleware), newest first. Test reports (smoke tests, curl)
* are counted apart and left out of the list (0114-FE); bot reports stay
* in with a label. Rows carry a "path" and a shortened user agent for the
* list; the full values stay in the row for the details toggle.
*
* @return array{list: list<array>, real: int, chunk: int, js: int, tests: int, bots: int}
*/
private function clientErrors(int $limit): array
{
$report = ['list' => [], 'real' => 0, 'chunk' => 0, 'js' => 0, 'tests' => 0, 'bots' => 0];
try {
$qb = $this->connectionPool->getQueryBuilderForTable('tx_vitec_clienterrorlog');
$rows = $qb->select('*')->from('tx_vitec_clienterrorlog')
->where($qb->expr()->gt('crdate', $qb->createNamedParameter(time() - 86400, Connection::PARAM_INT)))
->orderBy('uid', 'DESC')->setMaxResults($limit)
->orderBy('uid', 'DESC')->setMaxResults(2000)
->executeQuery()->fetchAllAssociative();
} catch (\Throwable) {
return [];
return $report;
}
foreach ($rows as &$row) {
$path = (string)(parse_url((string)$row['url'], PHP_URL_PATH) ?? '');
$row['path'] = $path !== '' ? $path : (string)$row['url'];
foreach ($rows as $row) {
$ua = (string)$row['user_agent'];
$row['ua_short'] = mb_strlen($ua) > 60 ? mb_substr($ua, 0, 57) . '...' : $ua;
$row['is_chunk_reload'] = str_contains(strtolower((string)$row['message']), 'chunk reload');
if (ClientErrorClassifier::isTest((string)$row['message'], $ua, (string)$row['release'])) {
$report['tests']++;
continue;
}
$row['is_chunk_reload'] = ClientErrorClassifier::isChunkReload((string)$row['message']);
$row['is_bot'] = ClientErrorClassifier::isBot($ua);
$report['real']++;
$report[$row['is_chunk_reload'] ? 'chunk' : 'js']++;
$report['bots'] += $row['is_bot'] ? 1 : 0;
if (count($report['list']) < $limit) {
$path = (string)(parse_url((string)$row['url'], PHP_URL_PATH) ?? '');
$row['path'] = $path !== '' ? $path : (string)$row['url'];
$row['ua_short'] = mb_strlen($ua) > 60 ? mb_substr($ua, 0, 57) . '...' : $ua;
$report['list'][] = $row;
}
}
unset($row);
return $rows;
return $report;
}
/**
@@ -225,7 +243,7 @@ final class SsrController
[$from]
)->fetchAllAssociative();
$errors = $this->connectionPool->getConnectionForTable('tx_vitec_clienterrorlog')->executeQuery(
'SELECT crdate, message FROM tx_vitec_clienterrorlog WHERE crdate > ?',
'SELECT crdate, message, user_agent, `release` FROM tx_vitec_clienterrorlog WHERE crdate > ?',
[$now - 86400]
)->fetchAllAssociative();
} catch (\Throwable) {
@@ -362,6 +380,9 @@ final class SsrController
$errHours = array_fill(0, 24, 0);
$hourEnd = intdiv($now, 3600) * 3600 + 3600;
foreach ($errors as $e) {
if (ClientErrorClassifier::isTest((string)$e['message'], (string)$e['user_agent'], (string)$e['release'])) {
continue;
}
$i = 23 - intdiv($hourEnd - 1 - (int)$e['crdate'], 3600);
if ($i >= 0 && $i < 24) {
$errHours[$i]++;
@@ -395,17 +416,6 @@ final class SsrController
];
}
private function clientErrorCount24h(): int
{
try {
return (int)$this->connectionPool->getConnectionForTable('tx_vitec_clienterrorlog')
->executeQuery('SELECT COUNT(*) FROM tx_vitec_clienterrorlog WHERE crdate > ?', [time() - 86400])
->fetchOne();
} catch (\Throwable) {
return 0;
}
}
public function purgeAction(ServerRequestInterface $request): ResponseInterface
{
$body = (array)$request->getParsedBody();