fix(ui): log global errors silently instead of popping a toast + overlay

The global error handler (Vue errorHandler + window error + unhandledrejection)
fired a red 'Something went wrong: <raw msg>' toast AND an auto on-device overlay
on every caught error — deliberately loud for bug-bash, but it surfaces benign,
non-actionable noise (e.g. a transient RPC rejection during a ws reconnect, or
the service worker failing to register over a self-signed cert) right in the
user's face.

Demote the catch-all to SILENT capture: keep console.error + the
window.__archyErrors ring buffer, and expose the screenshot-able overlay
on-demand via window.__archyShowErrors() — but never auto-pop. Components that
need to report a specific, actionable failure still call toast.error() directly.

Also filter known-benign environmental noise (PWA service-worker registration
failing over a self-signed cert — needs a trusted cert, #56) so it doesn't even
occupy a ring-buffer slot and push out real errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago 2026-06-24 05:55:49 -04:00
parent 0406af522c
commit 2d8ade629b

View File

@ -23,8 +23,6 @@ if (!navigator.clipboard) {
},
})
}
import { useToast } from '@/composables/useToast'
const app = createApp(App)
const pinia = createPinia()
@ -97,14 +95,20 @@ function recordError(source: string, err: unknown, info?: string) {
const entry: ArchyErrorEntry = { when: new Date().toISOString(), source, message, info, stack: e?.stack }
errorLog.push(entry)
if (errorLog.length > 25) errorLog.shift()
// Log SILENTLY: a global handler error is almost always something we should
// fix at the source, not interrupt the user for. Keep the full record on the
// console + the window.__archyErrors ring buffer, and make the screenshot-able
// overlay available ON DEMAND (window.__archyShowErrors(), or the debug view)
// — but do NOT auto-pop a red toast / overlay over the UI. Components that
// need to tell the user about a *specific, actionable* failure still call
// toast.error() directly; this catch-all stays out of the way.
console.error(`[${source}]`, err, info ?? '')
// Surface the real message (truncated) instead of a generic toast — this is a
// test/bug-bash build, and "Something went wrong" hides exactly what we need.
const short = message.length > 140 ? `${message.slice(0, 140)}` : message
try {
useToast().error(`Something went wrong: ${short}`)
} catch { /* toast itself failed — the console + ring buffer still have it */ }
// Always show the on-device overlay so the error is visible without a console.
}
// Expose the on-demand error overlay + ring buffer so a crash that only repros
// in a runtime without a console (Android companion WebView) is still
// retrievable: call `window.__archyShowErrors()` to screenshot/Copy them.
;(window as unknown as { __archyShowErrors?: () => void }).__archyShowErrors = () => {
try { showErrorOverlay() } catch { /* overlay is best-effort */ }
}
@ -133,15 +137,28 @@ function reloadOnceForStaleChunk(err: unknown): boolean {
return true
}
// Known-benign environmental noise — expected on some deployments and not
// actionable by the user or us, so it shouldn't even occupy a ring-buffer slot
// (which would push out real errors). The PWA service worker can't register
// over a self-signed cert (it needs a trusted cert or localhost); on those
// nodes the SW/offline cache simply doesn't run, which is fine. Logged at debug
// only. (A trusted cert is the real fix — tracked separately, #56.)
function isBenignEnvironmentError(err: unknown): boolean {
const msg = (err as { message?: string })?.message ?? String(err ?? '')
return /Failed to register a ServiceWorker|ServiceWorker.*(SSL|certificate|SecurityError)|An SSL certificate error occurred when fetching the script/i.test(msg)
}
// Vue's errorHandler only catches errors raised synchronously inside Vue's
// lifecycle/reactivity. Async rejections and plain runtime errors (e.g. a JS
// API missing in an older WebView) slip past it, so catch those too.
window.addEventListener('error', (ev) => {
if (reloadOnceForStaleChunk(ev.error ?? ev.message)) return
if (isBenignEnvironmentError(ev.error ?? ev.message)) { console.debug('[benign]', ev.message); return }
recordError('window.error', ev.error ?? ev.message)
})
window.addEventListener('unhandledrejection', (ev) => {
if (reloadOnceForStaleChunk(ev.reason)) return
if (isBenignEnvironmentError(ev.reason)) { console.debug('[benign]', ev.reason); return }
recordError('unhandledrejection', ev.reason)
})