Rollup in vite.main.config.ts doesn't know about the `@/` alias (only the
renderer configs do). After PR 4 made `lib/errors.ts` reachable from main,
its `@/config/env` import broke the main build with:
[vite]: Rollup failed to resolve import "@/config/env" from "lib/errors.ts"
Swap to a relative import — the module is now genuinely isomorphic.
logError / reportError now route through a pair of sinks installed at
bootstrap by each process (main + every renderer entry). No call site
knows about Sentry — if sentryDsn is empty, the sinks simply aren't
installed and logError/reportError stay console-only.
- appConfig.sentryDsn: per-env string (empty for now — populate when
ops creates the DSNs). Empty is the no-op mode for dev.
- lib/errors.ts: installErrorSinks({ capture, breadcrumb }) gates
Sentry.captureException / Sentry.addBreadcrumb. Everything flows
through toUserMessage and the two existing call types.
- lib/sentry.ts: initSentryRenderer() for the main window + autoplay,
huddle, and screen-record renderers.
- main/sentry.ts: initSentryMain() runs before anything else in
main.ts to catch bootstrap failures. Captures uncaught exceptions
and the crash reporter automatically.
- main/ipc-utils.ts::safeHandle now routes through reportError.
- main.ts::fetchLinkMetadata logs via logError.
Every catch now either surfaces, re-throws, or calls logError with a scope
tag. No more empty catches or bare console.error:
- auth-store: signInToFirebase / restoreSession / signOut paths gain
logError context. Behavior is unchanged (best-effort local sign-out,
fall back to login on restore failure).
- use-stream-autoplay: Audio.play() and download-URL fetches log their
failures instead of dropping silently (both are nice-to-haves so UX
stays silent — but we can now trace "why didn't autoplay trigger?").
- pusher-client: ws errors / parse failures / server errors / listener
crashes all routed through logError, and listener bugs (which silently
break user flows) now go through reportError so they're actually
surfaced in observability.
- settings-page: email-notifications toggle now toasts on failure
instead of silently reverting with no explanation.
- huddle-app: screen-share failures use logError.
Adds the infrastructure for a coherent client-side error story:
- `lib/errors.ts`: canonical ApiError + QuotaExceededError, `toUserMessage`
(friendly strings for ApiError/ZodError/network errors, strips Electron
IPC message prefixes), `logError` (expected), `reportError` (unexpected).
- `lib/query-client.ts`: QueryClient factory with sane retry defaults (no
retry on 4xx except 408/429, 2 retries otherwise; 0 mutation retries),
`QueryCache` onError logs + opts in via `meta.toastOnError`, and
`MutationCache` onError toasts `toUserMessage(err)` by default with
`meta.suppressToast` as the opt-out.
- `components/app-error-boundary.tsx` + `error-fallback.tsx`: two boundaries
(top-level outside the router, route-level inside) with a Card-based
fallback offering 'Go home' + 'Try again'. Route boundary resets on
pathname change and clears React Query error cache on retry.
- `main/ipc-utils.ts` + main.ts migration: `safeHandle` wraps ipcMain.handle
so main-process failures log with full stack and surface a sanitized
message to the renderer. `link:fetch-metadata` keeps its null contract
but now logs.
- `useCreateParticle` opts out of the global toast (compose-overlay renders
its own quota UX) so nothing double-toasts.
Render crashes now have a recovery UI, every mutation gets a free error
toast, and silent-catch cleanup + Sentry land in follow-up PRs.