Observability + warming guardrails (Codex)
* client_error details, not just a count: new client_errors table + POST /api/client-error (reason/path/user-agent/time) + GET /api/admin/client-errors. The boot-seatbelt beacon now sends the reason + path (once per page); the admin Overview lists the recent errors so we can tell chunk vs SW vs API vs JS — the truth meter for the next day as the new SW propagates. * Deploy warming now also hits the shell, routes (/play /account /admin), SW, version.json, word lists, and icons/logo/font — not just immutable chunks. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+19
-9
@@ -38,17 +38,22 @@
|
||||
// screen. Show a calm recovery card if the app hasn't mounted, and reload
|
||||
// once on a chunk/preload failure (e.g. a just-deployed hashed chunk).
|
||||
(function () {
|
||||
function showBoot() {
|
||||
var sent = false;
|
||||
function report(reason) {
|
||||
if (sent) return; sent = true; // one beacon per page
|
||||
try {
|
||||
var b = new Blob([JSON.stringify({ reason: String(reason || 'unknown').slice(0, 300),
|
||||
path: location.pathname })], { type: 'application/json' });
|
||||
navigator.sendBeacon && navigator.sendBeacon('/api/client-error', b);
|
||||
} catch (e) { /* best-effort telemetry */ }
|
||||
}
|
||||
function showBoot(reason) {
|
||||
if (window.__ubMounted) return; // app is running; in-app handles it
|
||||
var el = document.getElementById('boot-fallback');
|
||||
if (el) el.style.display = 'flex';
|
||||
try {
|
||||
var b = new Blob([JSON.stringify({ kind: 'client_error', visitor: 'e' + Math.random().toString(36).slice(2) })],
|
||||
{ type: 'application/json' });
|
||||
navigator.sendBeacon && navigator.sendBeacon('/api/events', b);
|
||||
} catch (e) { /* best-effort telemetry */ }
|
||||
report(reason);
|
||||
}
|
||||
var timer = setTimeout(showBoot, 10000);
|
||||
var timer = setTimeout(function () { showBoot('boot-timeout'); }, 10000);
|
||||
// Svelte calls this once it has mounted (see +layout.svelte).
|
||||
window.__ubBooted = function () {
|
||||
window.__ubMounted = true;
|
||||
@@ -58,6 +63,7 @@
|
||||
try { sessionStorage.removeItem('ub_reloaded'); } catch (e) {}
|
||||
};
|
||||
addEventListener('vite:preloadError', function (e) {
|
||||
report('preloadError: ' + ((e && e.payload && e.payload.message) || ''));
|
||||
try {
|
||||
if (!sessionStorage.getItem('ub_reloaded')) {
|
||||
sessionStorage.setItem('ub_reloaded', '1');
|
||||
@@ -66,8 +72,12 @@
|
||||
}
|
||||
} catch (err) { /* ignore */ }
|
||||
});
|
||||
addEventListener('error', showBoot);
|
||||
addEventListener('unhandledrejection', showBoot);
|
||||
addEventListener('error', function (e) {
|
||||
showBoot(e && (e.message || (e.error && e.error.message)) || 'error');
|
||||
});
|
||||
addEventListener('unhandledrejection', function (e) {
|
||||
showBoot(e && e.reason && (e.reason.message || String(e.reason)) || 'rejection');
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
|
||||
@@ -32,11 +32,14 @@
|
||||
feedback = await getJSON('/api/admin/feedback');
|
||||
candidates = await getJSON('/api/admin/candidates');
|
||||
wpPool = await getJSON('/api/admin/word/pool');
|
||||
clientErrors = await getJSON('/api/admin/client-errors');
|
||||
} catch {
|
||||
error = "Couldn't load stats.";
|
||||
}
|
||||
});
|
||||
|
||||
let clientErrors = $state([]);
|
||||
|
||||
// --- Games: Daily Word pool ---
|
||||
let wpWord = $state('');
|
||||
let wpResult = $state(null); // lookup result for the current input
|
||||
@@ -369,6 +372,20 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if clientErrors.length}
|
||||
<h2>Recent load errors <span class="count">(last {clientErrors.length})</span></h2>
|
||||
<ul class="cerrs">
|
||||
{#each clientErrors as e (e.created_at + e.reason)}
|
||||
<li>
|
||||
<span class="ce-when">{fdate(e.created_at)}</span>
|
||||
<span class="ce-reason">{e.reason || '—'}</span>
|
||||
<span class="ce-path">{e.path || '/'}</span>
|
||||
<span class="ce-ua">{e.user_agent}</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
|
||||
{:else if section === 'content'}
|
||||
<h2>Corpus</h2>
|
||||
<div class="cards">
|
||||
@@ -1009,6 +1026,14 @@
|
||||
|
||||
.stat.alert { background: #f3e0e0; }
|
||||
.stat.alert .n { color: #9a3b3b; }
|
||||
.cerrs { list-style: none; padding: 0; margin: 10px 0 0; display: flex; flex-direction: column; gap: 6px; }
|
||||
.cerrs li { display: grid; grid-template-columns: auto 1fr auto; gap: 6px 12px; align-items: baseline;
|
||||
font-size: 0.82rem; padding: 8px 12px; background: var(--surface); border: 1px solid var(--line); border-radius: 8px; }
|
||||
.ce-when { color: var(--muted); white-space: nowrap; }
|
||||
.ce-reason { font-family: var(--label); color: #9a3b3b; }
|
||||
.ce-path { color: var(--accent-deep); white-space: nowrap; }
|
||||
.ce-ua { grid-column: 1 / -1; color: var(--muted); font-size: 0.72rem;
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
/* Games — Daily Word pool */
|
||||
.wp-lookup { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin: 14px 0 6px; }
|
||||
|
||||
Reference in New Issue
Block a user