Files
multica/apps/web/app/global-error.tsx
Naiyuan Qing c222088262 feat: client failure telemetry (JS errors + freeze/crash) to PostHog (#4187)
* feat(analytics): capture JS exceptions to PostHog

Turn on posthog-js exception autocapture (window.onerror + unhandled
rejections, with stack) and add a buffered captureException() wrapper for
boundary-caught React errors those handlers can't see. Wire the web
route-level global-error boundary to report through it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(diagnostics): add shared freeze watchdog

Long-task observer (>=2s) emits client_unresponsive via captureEvent;
client_type super-property tags desktop vs web for free. Installed once in
CoreProvider so web and desktop share one in-thread, SSR-safe detector for
recoverable freezes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(desktop): report true hangs and crashes via breadcrumb

A real hang or crashed renderer can't report itself. The main process now
persists a breadcrumb on unresponsive / render-process-gone, and the next
renderer boot flushes it to PostHog (client_unresponsive / client_crash).
A recovered hang clears its breadcrumb so it isn't double-counted by the
in-thread watchdog.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(analytics): scrub PII from $exception before send

Error messages can interpolate user input (typed values, URLs with tokens).
Add a before_send hook that redacts emails, URL query strings, and long
opaque tokens from the exception message and $exception_list values, keeping
type + stack frames (code locations, not user data). Addresses the privacy
gap from leaving capture_exceptions on with no sanitizer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: cover breadcrumb state machine and freeze watchdog

The breadcrumb persist/clear orchestration is the correctness-critical part
and was untested. Cover: hang->write, recover->clear (no double-count),
recover-before-delay->no-op, force-quit->retained, crash->write-and-never-
clear, clean-exit->no-write. Add watchdog tests (threshold, idempotent,
SSR/PerformanceObserver no-op) via a fake observer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(desktop): breadcrumb field precedence + document limits

Spread the persisted context FIRST so explicit event fields (source,
recovered) always win over a future colliding context key. Document why
preload-error skips the breadcrumb and the single-slot last-write-wins
undercount limitation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 16:31:38 +08:00

59 lines
1.6 KiB
TypeScript

"use client";
import { useEffect } from "react";
import { captureException } from "@multica/core/analytics";
/**
* Route-level error boundary for the web app. Next.js renders this (replacing
* the root layout) when an error escapes everything below it — the full-page
* white-screen case. React catches these before they reach window.onerror, so
* posthog-js's automatic exception capture never sees them; we report them
* explicitly here. Section-level failures are handled in place by
* `@multica/ui` ErrorBoundary and don't reach this far.
*/
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
captureException(error, { source: "global-error", digest: error.digest });
}, [error]);
return (
<html>
<body
style={{
display: "flex",
minHeight: "100vh",
alignItems: "center",
justifyContent: "center",
fontFamily: "system-ui, sans-serif",
}}
>
<div style={{ maxWidth: 420, textAlign: "center" }}>
<h1 style={{ fontSize: 18, fontWeight: 600 }}>Something went wrong</h1>
<p style={{ marginTop: 8, color: "#666" }}>
The page hit an unexpected error. Try reloading.
</p>
<button
type="button"
onClick={reset}
style={{
marginTop: 16,
padding: "8px 16px",
borderRadius: 6,
border: "1px solid #ccc",
cursor: "pointer",
}}
>
Reload
</button>
</div>
</body>
</html>
);
}