Files
VoiceStudio/frontend/src/utils/globalErrorHandlers.js
T

63 lines
2.2 KiB
JavaScript

/**
* globalErrorHandlers — last-resort surfacing for uncaught failures.
*
* consoleBuffer already records `window.onerror` / `unhandledrejection`
* into the Settings → Logs → Frontend ring; this adds the user-visible
* half: a throttled error toast with a "Report this bug" action
* (utils/errorToast.jsx) so async failures outside any ErrorBoundary or
* wired call site still have a path to a GitHub issue.
*
* Throttled per message (one toast per 30s) and filtered against known
* benign noise — a render-loop bug must not bury the user in toasts.
*/
import i18next from 'i18next';
import { toastErrorWithReport } from './errorToast';
const THROTTLE_MS = 30_000;
const lastShown = new Map();
// Browser/webview noise that is not actionable by the user and must never
// produce a report prompt.
const IGNORE_PATTERNS = [
/ResizeObserver loop/i,
/AbortError/i,
/Loading chunk \d+ failed/i, // transient on dev-server restarts
/Script error\.?$/i, // opaque cross-origin errors carry no info
];
function shouldShow(message, error) {
// Some browser streams (including WaveSurfer's BodyStreamBuffer) describe
// normal cancellation without the word "AbortError" in the message. The
// structured DOMException name is the reliable cancellation contract.
if (error?.name === 'AbortError') return false;
if (!message || IGNORE_PATTERNS.some((p) => p.test(message))) return false;
const key = String(message).slice(0, 200);
const now = Date.now();
if ((lastShown.get(key) || 0) > now - THROTTLE_MS) return false;
lastShown.set(key, now);
return true;
}
function surface(message, error) {
if (!shouldShow(message, error)) return;
const err = error instanceof Error ? error : new Error(String(error ?? message));
toastErrorWithReport(
i18next.t('errors.unexpected', { message: String(message).slice(0, 140) }),
err,
);
}
let installed = false;
export function installGlobalErrorHandlers() {
if (installed || typeof window === 'undefined') return;
installed = true;
window.addEventListener('error', (e) => {
surface(e?.error?.message || e.message, e.error);
});
window.addEventListener('unhandledrejection', (e) => {
const r = e?.reason;
surface(r?.message || String(r), r);
});
}