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:
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();
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\Service;
|
||||
|
||||
/**
|
||||
* Tells test reports and bot reports in tx_vitec_clienterrorlog apart
|
||||
* from real visitor errors (ticket 0114-FE: the cockpit tile must not
|
||||
* count smoke tests). One rule set for the module and the dashboard.
|
||||
*
|
||||
* Test: sent with curl, a user agent that says "smoke" or "client-error
|
||||
* test", a message starting with "test", or the dummy release "t" of the
|
||||
* endpoint smoke test. Bot: a crawler user agent (Googlebot & Co.); bot
|
||||
* reports stay real errors, they only get a label.
|
||||
*/
|
||||
final class ClientErrorClassifier
|
||||
{
|
||||
public static function isTest(string $message, string $userAgent, string $release = ''): bool
|
||||
{
|
||||
if ($release === 't') {
|
||||
return true;
|
||||
}
|
||||
if (preg_match('#curl/|smoke|client-error test#i', $userAgent) === 1) {
|
||||
return true;
|
||||
}
|
||||
return preg_match('#^\s*test\b#i', $message) === 1;
|
||||
}
|
||||
|
||||
public static function isBot(string $userAgent): bool
|
||||
{
|
||||
return preg_match('#bot\b|crawler|spider|slurp|lighthouse|headlesschrome#i', $userAgent) === 1;
|
||||
}
|
||||
|
||||
public static function isChunkReload(string $message): bool
|
||||
{
|
||||
return str_contains(strtolower($message), 'chunk reload');
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\Widgets;
|
||||
|
||||
use Evomedien\Vitec\Service\ClientErrorClassifier;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Dashboard\Widgets\AdditionalCssInterface;
|
||||
use TYPO3\CMS\Dashboard\Widgets\WidgetConfigurationInterface;
|
||||
@@ -35,9 +36,9 @@ final class SsrStatsWidget implements WidgetInterface, AdditionalCssInterface
|
||||
$full7d = $this->count('tx_vitec_ssrpurgelog', "crdate > " . $d7 . " AND paths = '*'");
|
||||
$avgMs = $this->scalar('SELECT ROUND(AVG(response_ms)) FROM tx_vitec_ssrhealthlog WHERE crdate > ' . $d1 . ' AND ok = 1');
|
||||
$maxCache = $this->scalar('SELECT MAX(cache_mb) FROM tx_vitec_ssrhealthlog WHERE crdate > ' . $d7);
|
||||
// Browser-side errors reported via POST /api/vitec/client-error.
|
||||
$clientErrors24h = $this->count('tx_vitec_clienterrorlog', 'crdate > ' . $d1);
|
||||
$clientErrors7d = $this->count('tx_vitec_clienterrorlog', 'crdate > ' . $d7);
|
||||
// Browser-side errors reported via POST /api/vitec/client-error,
|
||||
// without smoke/curl test reports (0114-FE).
|
||||
[$clientErrors24h, $clientErrors7d] = $this->realClientErrors($d1, $d7);
|
||||
|
||||
$strip = $h::strip($this->connectionPool);
|
||||
$uptime = $strip['uptime'];
|
||||
@@ -59,7 +60,7 @@ final class SsrStatsWidget implements WidgetInterface, AdditionalCssInterface
|
||||
. $tile('Failed purges', (string)$failed7d, 'last 7 days', $failed7d > 0 ? 'vcw-bad' : 'vcw-ok')
|
||||
. $tile('Avg. response', $avgMs !== null ? $avgMs . '<small>ms</small>' : '–', 'last 24 h')
|
||||
. $tile('Max. cache', $maxCache !== null ? $maxCache . '<small>MB</small>' : '–', 'last 7 days')
|
||||
. $tile('Client errors', $clientErrors24h . '<small>/ ' . $clientErrors7d . '</small>', '24 h / 7 days', $clientErrors24h > 0 ? 'vcw-warn' : 'vcw-ok')
|
||||
. $tile('Client errors', $clientErrors24h . '<small>/ ' . $clientErrors7d . '</small>', '24 h / 7 days, no tests', $clientErrors24h > 0 ? 'vcw-warn' : 'vcw-ok')
|
||||
. '</div>';
|
||||
|
||||
if ($strip['samples'] === 0) {
|
||||
@@ -82,6 +83,31 @@ final class SsrStatsWidget implements WidgetInterface, AdditionalCssInterface
|
||||
return [CockpitHtml::CSS];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: int, 1: int} real client error reports in 24 h / 7 days
|
||||
*/
|
||||
private function realClientErrors(int $d1, int $d7): array
|
||||
{
|
||||
try {
|
||||
$rows = $this->connectionPool->getConnectionForTable('tx_vitec_clienterrorlog')->executeQuery(
|
||||
'SELECT crdate, message, user_agent, `release` FROM tx_vitec_clienterrorlog WHERE crdate > ?',
|
||||
[$d7]
|
||||
)->fetchAllAssociative();
|
||||
} catch (\Throwable) {
|
||||
return [0, 0];
|
||||
}
|
||||
$day = 0;
|
||||
$week = 0;
|
||||
foreach ($rows as $r) {
|
||||
if (ClientErrorClassifier::isTest((string)$r['message'], (string)$r['user_agent'], (string)$r['release'])) {
|
||||
continue;
|
||||
}
|
||||
$week++;
|
||||
$day += (int)$r['crdate'] > $d1 ? 1 : 0;
|
||||
}
|
||||
return [$day, $week];
|
||||
}
|
||||
|
||||
private function count(string $table, string $where): int
|
||||
{
|
||||
try {
|
||||
|
||||
Reference in new issue
Block a user