feat(crash): comprehensive crash + error reporting for v0.2.3

Adds full-stack crash capture so any unexpected failure — React render,
uncaught JS exception, unhandled promise rejection, or native — is
reported to Sentry with rich, scrubbed context. Expected operational
errors (timeouts, biometric cancel, etc.) stay local to preserve signal.

Changes:
- src/lib/sentry.ts: explicit native crash handlers, release/dist tags
  from app.json, beforeSend/beforeBreadcrumb URL+secret scrubbing,
  addBreadcrumb/captureException helpers, ErrorUtils + onunhandledrejection
  wrappers that always feed the in-memory log buffer (so offline Share
  Report includes the crash too).
- src/components/ErrorBoundary.tsx: new app-wide React boundary with a
  dark recovery screen — error message, top stack/component frames,
  Share Report (clipboard + native share sheet) and Try Again.
- src/lib/diagnostics.ts: buildCrashReport() reuses the existing
  DiagnosticReport pipeline so crashes and connect failures share one
  UI and one transport.
- _layout.tsx: wraps app in ErrorBoundary; emits app.lifecycle
  breadcrumb at startup.
- stores/{connections,events,sessions}.ts: high-signal breadcrumbs at
  connect, SSE connect/disconnect/reconnect, and session select.
- (tabs)/settings.tsx: fix unhandled promise on notificationsGranted().
- app.json: bump expo.version to 0.2.3.
- docs/prd.md, docs/tdd.md: new product + technical design docs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Dennis V
2026-05-23 09:00:26 +00:00
parent 678750d16f
commit 24667ee4f4
11 changed files with 676 additions and 21 deletions

View File

@@ -2,6 +2,7 @@ import { create } from "zustand"
import { useConnections } from "./connections"
import { useSessions } from "./sessions"
import { send as notify } from "../lib/notifications"
import { addBreadcrumb } from "../lib/sentry"
import type { Client, Part, Session, Message } from "../lib/sdk"
// Session status from the server
@@ -116,6 +117,7 @@ export const useEvents = create<EventsState>((set, get) => ({
const currentController = controller
set({ connected: true })
console.log("[SSE] Connecting to event stream...")
addBreadcrumb({ category: "sse", message: "connecting" })
// Run in background
;(async () => {
@@ -149,6 +151,12 @@ export const useEvents = create<EventsState>((set, get) => ({
const baseDelay = RECONNECT_DELAYS_MS[Math.min(reconnectAttempts - 1, RECONNECT_DELAYS_MS.length - 1)]
const jitteredDelay = Math.min(15_000, Math.round(baseDelay * (0.75 + Math.random() * 0.5)))
console.warn(`[SSE] Connection lost, reconnecting in ${jitteredDelay}ms:`, reason)
addBreadcrumb({
category: "sse",
level: "warning",
message: "reconnect scheduled",
data: { attempt: reconnectAttempts, delayMs: jitteredDelay, reason: String(reason).slice(0, 200) },
})
reconnectTimer = setTimeout(() => {
reconnectTimer = null
get().connect()
@@ -352,6 +360,7 @@ export const useEvents = create<EventsState>((set, get) => ({
disconnect: () => {
console.log("[SSE] Disconnecting")
addBreadcrumb({ category: "sse", message: "disconnected" })
if (reconnectTimer) {
clearTimeout(reconnectTimer)
reconnectTimer = null