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
+119
-35
No files matched your search
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Evomedien\Vitec\Controller\Backend;
|
namespace Evomedien\Vitec\Controller\Backend;
|
||||||
|
|
||||||
|
use Evomedien\Vitec\Service\ClientErrorClassifier;
|
||||||
use Evomedien\Vitec\Service\SsrPurgeService;
|
use Evomedien\Vitec\Service\SsrPurgeService;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
@@ -68,7 +69,8 @@ final class SsrController
|
|||||||
}
|
}
|
||||||
$healthLog = $this->rows('tx_vitec_ssrhealthlog', 10);
|
$healthLog = $this->rows('tx_vitec_ssrhealthlog', 10);
|
||||||
$lastSample = $healthLog[0] ?? null;
|
$lastSample = $healthLog[0] ?? null;
|
||||||
$clientErrors = $this->clientErrors(50);
|
$errorReport = $this->clientErrors(50);
|
||||||
|
$clientErrors = $errorReport['list'];
|
||||||
$cockpit = $this->cockpitData($health, $lastSample);
|
$cockpit = $this->cockpitData($health, $lastSample);
|
||||||
$alert = $this->purgeService->getAlertRecipients();
|
$alert = $this->purgeService->getAlertRecipients();
|
||||||
|
|
||||||
@@ -97,8 +99,10 @@ final class SsrController
|
|||||||
'alertList' => implode(', ', $alert['recipients']),
|
'alertList' => implode(', ', $alert['recipients']),
|
||||||
'alertCount' => count($alert['recipients']),
|
'alertCount' => count($alert['recipients']),
|
||||||
'alertInvalid' => implode(', ', $alert['invalid']),
|
'alertInvalid' => implode(', ', $alert['invalid']),
|
||||||
'chunkCount24h' => count(array_filter($clientErrors, static fn(array $r): bool => $r['is_chunk_reload'])),
|
'chunkCount24h' => $errorReport['chunk'],
|
||||||
'jsErrorCount24h' => count(array_filter($clientErrors, static fn(array $r): bool => !$r['is_chunk_reload'])),
|
'jsErrorCount24h' => $errorReport['js'],
|
||||||
|
'testCount24h' => $errorReport['tests'],
|
||||||
|
'botCount24h' => $errorReport['bots'],
|
||||||
'flagOn' => file_exists(Environment::getVarPath() . '/vitec-ssr-on.flag'),
|
'flagOn' => file_exists(Environment::getVarPath() . '/vitec-ssr-on.flag'),
|
||||||
'configured' => $this->purgeService->isConfigured(),
|
'configured' => $this->purgeService->isConfigured(),
|
||||||
'health' => $health,
|
'health' => $health,
|
||||||
@@ -111,37 +115,51 @@ final class SsrController
|
|||||||
'moduleUri' => (string)$this->uriBuilder->buildUriFromRoute('web_vitecssr'),
|
'moduleUri' => (string)$this->uriBuilder->buildUriFromRoute('web_vitecssr'),
|
||||||
'healthLog' => $healthLog,
|
'healthLog' => $healthLog,
|
||||||
'clientErrors' => $clientErrors,
|
'clientErrors' => $clientErrors,
|
||||||
'clientErrorCount24h' => $this->clientErrorCount24h(),
|
'clientErrorCount24h' => $errorReport['real'],
|
||||||
]);
|
]);
|
||||||
return $view->renderResponse('Ssr/Index');
|
return $view->renderResponse('Ssr/Index');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Client error reports of the last 24 hours (POST /api/vitec/client-error,
|
* Client error reports of the last 24 hours (POST /api/vitec/client-error,
|
||||||
* ClientErrorMiddleware), newest first. Rows carry a "host" (URL without
|
* ClientErrorMiddleware), newest first. Test reports (smoke tests, curl)
|
||||||
* scheme/query) and a shortened user agent for the table; the full
|
* are counted apart and left out of the list (0114-FE); bot reports stay
|
||||||
* values stay in the row for the details toggle.
|
* 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
|
private function clientErrors(int $limit): array
|
||||||
{
|
{
|
||||||
|
$report = ['list' => [], 'real' => 0, 'chunk' => 0, 'js' => 0, 'tests' => 0, 'bots' => 0];
|
||||||
try {
|
try {
|
||||||
$qb = $this->connectionPool->getQueryBuilderForTable('tx_vitec_clienterrorlog');
|
$qb = $this->connectionPool->getQueryBuilderForTable('tx_vitec_clienterrorlog');
|
||||||
$rows = $qb->select('*')->from('tx_vitec_clienterrorlog')
|
$rows = $qb->select('*')->from('tx_vitec_clienterrorlog')
|
||||||
->where($qb->expr()->gt('crdate', $qb->createNamedParameter(time() - 86400, Connection::PARAM_INT)))
|
->where($qb->expr()->gt('crdate', $qb->createNamedParameter(time() - 86400, Connection::PARAM_INT)))
|
||||||
->orderBy('uid', 'DESC')->setMaxResults($limit)
|
->orderBy('uid', 'DESC')->setMaxResults(2000)
|
||||||
->executeQuery()->fetchAllAssociative();
|
->executeQuery()->fetchAllAssociative();
|
||||||
} catch (\Throwable) {
|
} catch (\Throwable) {
|
||||||
return [];
|
return $report;
|
||||||
}
|
}
|
||||||
foreach ($rows as &$row) {
|
foreach ($rows as $row) {
|
||||||
|
$ua = (string)$row['user_agent'];
|
||||||
|
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) ?? '');
|
$path = (string)(parse_url((string)$row['url'], PHP_URL_PATH) ?? '');
|
||||||
$row['path'] = $path !== '' ? $path : (string)$row['url'];
|
$row['path'] = $path !== '' ? $path : (string)$row['url'];
|
||||||
$ua = (string)$row['user_agent'];
|
|
||||||
$row['ua_short'] = mb_strlen($ua) > 60 ? mb_substr($ua, 0, 57) . '...' : $ua;
|
$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');
|
$report['list'][] = $row;
|
||||||
}
|
}
|
||||||
unset($row);
|
}
|
||||||
return $rows;
|
return $report;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -225,7 +243,7 @@ final class SsrController
|
|||||||
[$from]
|
[$from]
|
||||||
)->fetchAllAssociative();
|
)->fetchAllAssociative();
|
||||||
$errors = $this->connectionPool->getConnectionForTable('tx_vitec_clienterrorlog')->executeQuery(
|
$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]
|
[$now - 86400]
|
||||||
)->fetchAllAssociative();
|
)->fetchAllAssociative();
|
||||||
} catch (\Throwable) {
|
} catch (\Throwable) {
|
||||||
@@ -362,6 +380,9 @@ final class SsrController
|
|||||||
$errHours = array_fill(0, 24, 0);
|
$errHours = array_fill(0, 24, 0);
|
||||||
$hourEnd = intdiv($now, 3600) * 3600 + 3600;
|
$hourEnd = intdiv($now, 3600) * 3600 + 3600;
|
||||||
foreach ($errors as $e) {
|
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);
|
$i = 23 - intdiv($hourEnd - 1 - (int)$e['crdate'], 3600);
|
||||||
if ($i >= 0 && $i < 24) {
|
if ($i >= 0 && $i < 24) {
|
||||||
$errHours[$i]++;
|
$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
|
public function purgeAction(ServerRequestInterface $request): ResponseInterface
|
||||||
{
|
{
|
||||||
$body = (array)$request->getParsedBody();
|
$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;
|
namespace Evomedien\Vitec\Widgets;
|
||||||
|
|
||||||
|
use Evomedien\Vitec\Service\ClientErrorClassifier;
|
||||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
use TYPO3\CMS\Dashboard\Widgets\AdditionalCssInterface;
|
use TYPO3\CMS\Dashboard\Widgets\AdditionalCssInterface;
|
||||||
use TYPO3\CMS\Dashboard\Widgets\WidgetConfigurationInterface;
|
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 = '*'");
|
$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');
|
$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);
|
$maxCache = $this->scalar('SELECT MAX(cache_mb) FROM tx_vitec_ssrhealthlog WHERE crdate > ' . $d7);
|
||||||
// Browser-side errors reported via POST /api/vitec/client-error.
|
// Browser-side errors reported via POST /api/vitec/client-error,
|
||||||
$clientErrors24h = $this->count('tx_vitec_clienterrorlog', 'crdate > ' . $d1);
|
// without smoke/curl test reports (0114-FE).
|
||||||
$clientErrors7d = $this->count('tx_vitec_clienterrorlog', 'crdate > ' . $d7);
|
[$clientErrors24h, $clientErrors7d] = $this->realClientErrors($d1, $d7);
|
||||||
|
|
||||||
$strip = $h::strip($this->connectionPool);
|
$strip = $h::strip($this->connectionPool);
|
||||||
$uptime = $strip['uptime'];
|
$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('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('Avg. response', $avgMs !== null ? $avgMs . '<small>ms</small>' : '–', 'last 24 h')
|
||||||
. $tile('Max. cache', $maxCache !== null ? $maxCache . '<small>MB</small>' : '–', 'last 7 days')
|
. $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>';
|
. '</div>';
|
||||||
|
|
||||||
if ($strip['samples'] === 0) {
|
if ($strip['samples'] === 0) {
|
||||||
@@ -82,6 +83,31 @@ final class SsrStatsWidget implements WidgetInterface, AdditionalCssInterface
|
|||||||
return [CockpitHtml::CSS];
|
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
|
private function count(string $table, string $where): int
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -106,9 +106,10 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="vc-card vc-s3" aria-label="Response time">
|
<section class="vc-card vc-s3" aria-label="Response time">
|
||||||
<span class="vc-info"><button type="button" class="vc-info-btn" aria-label="About this tile" aria-describedby="vc-tip-ms">?</button><span class="vc-tip" role="tooltip" id="vc-tip-ms">How long TYPO3 needs to reach the node, measured when this page loads and every 15 seconds after. Median and p95 come from the cron samples of the last 24 hours, taken every 5 minutes. The curve shows the last 4 hours. Values above 400 ms turn the availability strip yellow.</span></span>
|
<span class="vc-info"><button type="button" class="vc-info-btn" aria-label="About this tile" aria-describedby="vc-tip-ms">?</button><span class="vc-tip" role="tooltip" id="vc-tip-ms">The big number is the last check: how long TYPO3 needed to reach the node, measured when this page loads and every 15 seconds after. Median and p95 come from the cron samples of the last 24 hours, taken every 5 minutes. The curve shows the last 4 hours. Values above 400 ms turn the availability strip yellow.</span></span>
|
||||||
<div class="vc-label">Response time</div>
|
<div class="vc-label">Response time</div>
|
||||||
<div class="vc-big"><span id="vc-ms"><f:if condition="{health}"><f:then>{health.responseMs}</f:then><f:else>–</f:else></f:if></span><small>ms</small></div>
|
<div class="vc-big"><span id="vc-ms"><f:if condition="{health}"><f:then>{health.responseMs}</f:then><f:else>–</f:else></f:if></span><small>ms</small></div>
|
||||||
|
<div class="vc-big-note">last check · updates every 15 s</div>
|
||||||
<div class="vc-sub">median 24 h <b id="vc-ms-median">–</b> · p95 <b id="vc-ms-p95">–</b></div>
|
<div class="vc-sub">median 24 h <b id="vc-ms-median">–</b> · p95 <b id="vc-ms-p95">–</b></div>
|
||||||
<svg class="vc-spark" id="vc-spark" viewBox="0 0 300 46" preserveAspectRatio="none" aria-hidden="true"></svg>
|
<svg class="vc-spark" id="vc-spark" viewBox="0 0 300 46" preserveAspectRatio="none" aria-hidden="true"></svg>
|
||||||
</section>
|
</section>
|
||||||
@@ -126,10 +127,10 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="vc-card vc-s3" aria-label="Client errors">
|
<section class="vc-card vc-s3" aria-label="Client errors">
|
||||||
<span class="vc-info"><button type="button" class="vc-info-btn" aria-label="About this tile" aria-describedby="vc-tip-errtile">?</button><span class="vc-tip" role="tooltip" id="vc-tip-errtile">Errors that visitor browsers reported from the React frontend in the last 24 hours. A chunk reload happens when an open tab still runs an older bundle after a deploy; that is usually harmless. JS errors point to a real bug. The bars show reports per hour, details are in the Client errors card below.</span></span>
|
<span class="vc-info"><button type="button" class="vc-info-btn" aria-label="About this tile" aria-describedby="vc-tip-errtile">?</button><span class="vc-tip" role="tooltip" id="vc-tip-errtile">Errors that visitor browsers reported from the React frontend in the last 24 hours. A chunk reload happens when an open tab still runs an older bundle after a deploy; that is usually harmless. JS errors point to a real bug. Test reports (smoke tests, curl) are left out; reports from crawlers like Googlebot count and carry a bot label. The bars show reports per hour, details are in the Client errors card below.</span></span>
|
||||||
<div class="vc-label">Client errors · 24 h</div>
|
<div class="vc-label">Client errors · 24 h</div>
|
||||||
<div class="vc-big">{clientErrorCount24h}<small>report<f:if condition="{clientErrorCount24h} != 1">s</f:if></small></div>
|
<div class="vc-big">{clientErrorCount24h}<small>report<f:if condition="{clientErrorCount24h} != 1">s</f:if></small></div>
|
||||||
<div class="vc-sub">{chunkCount24h} chunk reload<f:if condition="{chunkCount24h} != 1">s</f:if> · {jsErrorCount24h} JS error<f:if condition="{jsErrorCount24h} != 1">s</f:if></div>
|
<div class="vc-sub">{chunkCount24h} chunk reload<f:if condition="{chunkCount24h} != 1">s</f:if> · {jsErrorCount24h} JS error<f:if condition="{jsErrorCount24h} != 1">s</f:if><f:if condition="{testCount24h}"> · <span class="vc-dim" title="Smoke tests and curl reports are left out">{testCount24h} test<f:if condition="{testCount24h} != 1">s</f:if> left out</span></f:if></div>
|
||||||
<div class="vc-mini" id="vc-mini" role="img" aria-label="Client errors per hour, last 24 hours"></div>
|
<div class="vc-mini" id="vc-mini" role="img" aria-label="Client errors per hour, last 24 hours"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -270,7 +271,7 @@
|
|||||||
|
|
||||||
<!-- Client errors -->
|
<!-- Client errors -->
|
||||||
<section class="vc-card vc-s5" aria-label="Client errors">
|
<section class="vc-card vc-s5" aria-label="Client errors">
|
||||||
<span class="vc-info"><button type="button" class="vc-info-btn" aria-label="About this tile" aria-describedby="vc-tip-errors">?</button><span class="vc-tip" role="tooltip" id="vc-tip-errors">Every report from visitor browsers in the last 24 hours, newest first. Open an entry for release, URL, browser, anonymized IP and stack trace. “old bundle” means the tab ran an older release than the node. Reports are limited to 30 per minute and IP and are kept for 30 days.</span></span>
|
<span class="vc-info"><button type="button" class="vc-info-btn" aria-label="About this tile" aria-describedby="vc-tip-errors">?</button><span class="vc-tip" role="tooltip" id="vc-tip-errors">Every report from visitor browsers in the last 24 hours, newest first. Open an entry for release, URL, browser, anonymized IP and stack trace. “old bundle” means the tab ran an older release than the node. Test reports (smoke tests, curl) are counted under Tests but not listed. Reports are limited to 30 per minute and IP and are kept for 30 days.</span></span>
|
||||||
<div class="vc-card-h">
|
<div class="vc-card-h">
|
||||||
<h2 class="vc-card-t"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M12 3 2 21h20zM12 10v4M12 17.5v.5"/></svg>Client errors</h2>
|
<h2 class="vc-card-t"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M12 3 2 21h20zM12 10v4M12 17.5v.5"/></svg>Client errors</h2>
|
||||||
<span class="vc-pill is-sky" title="Reported by the React frontend via POST /api/vitec/client-error; max. 30 per minute and IP, IPs anonymized, cleaned up after 30 days by cron vitec:client-error-cleanup">kept 30 days</span>
|
<span class="vc-pill is-sky" title="Reported by the React frontend via POST /api/vitec/client-error; max. 30 per minute and IP, IPs anonymized, cleaned up after 30 days by cron vitec:client-error-cleanup">kept 30 days</span>
|
||||||
@@ -279,6 +280,7 @@
|
|||||||
<div><dt>24 h</dt><dd>{clientErrorCount24h}</dd></div>
|
<div><dt>24 h</dt><dd>{clientErrorCount24h}</dd></div>
|
||||||
<div><dt>Chunk reloads</dt><dd class="vc-warn-text">{chunkCount24h}</dd></div>
|
<div><dt>Chunk reloads</dt><dd class="vc-warn-text">{chunkCount24h}</dd></div>
|
||||||
<div><dt>JS errors</dt><dd class="vc-bad-text">{jsErrorCount24h}</dd></div>
|
<div><dt>JS errors</dt><dd class="vc-bad-text">{jsErrorCount24h}</dd></div>
|
||||||
|
<div title="Smoke tests and curl reports are not counted and not listed"><dt>Tests</dt><dd class="vc-muted-text">{testCount24h}</dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
<f:if condition="{clientErrors}">
|
<f:if condition="{clientErrors}">
|
||||||
<f:then>
|
<f:then>
|
||||||
@@ -286,10 +288,13 @@
|
|||||||
<f:for each="{clientErrors}" as="row">
|
<f:for each="{clientErrors}" as="row">
|
||||||
<details class="vc-err">
|
<details class="vc-err">
|
||||||
<summary>
|
<summary>
|
||||||
|
<span class="vc-err-pills">
|
||||||
<f:if condition="{row.is_chunk_reload}">
|
<f:if condition="{row.is_chunk_reload}">
|
||||||
<f:then><span class="vc-pill is-warn">chunk reload</span></f:then>
|
<f:then><span class="vc-pill is-warn">chunk reload</span></f:then>
|
||||||
<f:else><span class="vc-pill is-bad">error</span></f:else>
|
<f:else><span class="vc-pill is-bad">error</span></f:else>
|
||||||
</f:if>
|
</f:if>
|
||||||
|
<f:if condition="{row.is_bot}"><span class="vc-pill is-muted" title="Reported by a crawler, e.g. Googlebot. Still a real error: the page broke for this bot.">bot</span></f:if>
|
||||||
|
</span>
|
||||||
<span class="vc-err-msg">{row.message}<small>{row.path}</small></span>
|
<span class="vc-err-msg">{row.message}<small>{row.path}</small></span>
|
||||||
<span class="vc-err-t"><f:format.date format="H:i">@{row.crdate}</f:format.date></span>
|
<span class="vc-err-t"><f:format.date format="H:i">@{row.crdate}</f:format.date></span>
|
||||||
</summary>
|
</summary>
|
||||||
|
|||||||
@@ -112,6 +112,7 @@
|
|||||||
.vc-big { font-size: 32px; font-weight: 800; line-height: 1.05; margin: 6px 0 4px; font-variant-numeric: tabular-nums; letter-spacing: -.02em; white-space: nowrap; }
|
.vc-big { font-size: 32px; font-weight: 800; line-height: 1.05; margin: 6px 0 4px; font-variant-numeric: tabular-nums; letter-spacing: -.02em; white-space: nowrap; }
|
||||||
.vc-big small { font-size: 15px; font-weight: 600; color: var(--vc-muted); margin-left: 4px; letter-spacing: 0; }
|
.vc-big small { font-size: 15px; font-weight: 600; color: var(--vc-muted); margin-left: 4px; letter-spacing: 0; }
|
||||||
.vc-sub { font-size: 12px; color: var(--vc-muted); }
|
.vc-sub { font-size: 12px; color: var(--vc-muted); }
|
||||||
|
.vc-big-note { font-size: 10.5px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--vc-faint); margin: -2px 0 6px; }
|
||||||
.vc-sub b { color: var(--vc-text); }
|
.vc-sub b { color: var(--vc-text); }
|
||||||
.vc-sub .is-up { color: var(--vc-ok); } .vc-sub .is-down { color: var(--vc-bad); }
|
.vc-sub .is-up { color: var(--vc-ok); } .vc-sub .is-down { color: var(--vc-bad); }
|
||||||
.vc-ring { flex: none; }
|
.vc-ring { flex: none; }
|
||||||
@@ -206,7 +207,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Client errors */
|
/* Client errors */
|
||||||
.vc-err-sum { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; margin: 0 0 16px; }
|
.vc-err-sum { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; margin: 0 0 16px; }
|
||||||
|
@media (max-width: 619.98px) { .vc-err-sum { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||||
|
.vc-muted-text { color: var(--vc-faint); }
|
||||||
|
.vc-err-pills { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; }
|
||||||
.vc-err-sum div { background: var(--vc-panel-2); border-radius: 10px; padding: 10px 12px; min-width: 0; }
|
.vc-err-sum div { background: var(--vc-panel-2); border-radius: 10px; padding: 10px 12px; min-width: 0; }
|
||||||
.vc-err-sum dt { font-size: 11px; font-weight: 400; color: var(--vc-faint); text-transform: uppercase; letter-spacing: .07em; }
|
.vc-err-sum dt { font-size: 11px; font-weight: 400; color: var(--vc-faint); text-transform: uppercase; letter-spacing: .07em; }
|
||||||
.vc-err-sum dd { margin: 2px 0 0; font-size: 22px; font-weight: 800; font-variant-numeric: tabular-nums; }
|
.vc-err-sum dd { margin: 2px 0 0; font-size: 22px; font-weight: 800; font-variant-numeric: tabular-nums; }
|
||||||
|
|||||||
Reference in new issue
Block a user