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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user