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:
jay
2026-06-11 12:31:32 -04:00
parent 370d62270b
commit 61f575ba6d
7 changed files with 105 additions and 13 deletions
+19 -9
View File
@@ -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>