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 * as SecureStore from "expo-secure-store"
|
||||
import type { ServerConnection, ConnectionType } from "../lib/types"
|
||||
import { createClient, type Client, type Project } from "../lib/sdk"
|
||||
import { addBreadcrumb } from "../lib/sentry"
|
||||
|
||||
const CONNECTIONS_KEY = "opencode_connections"
|
||||
const PASSWORDS_PREFIX = "opencode_password_"
|
||||
@@ -216,6 +217,11 @@ export const useConnections = create<ConnectionsState>((set, get) => ({
|
||||
}
|
||||
|
||||
set({ connections, activeConnection: active, client, clientBase: base, currentProject: project, serverHome: home })
|
||||
addBreadcrumb({
|
||||
category: "connection",
|
||||
message: active ? `active connection set: ${active.type}` : "active connection cleared",
|
||||
data: { id: active?.id, type: active?.type, hasProject: Boolean(project) },
|
||||
})
|
||||
},
|
||||
|
||||
testConnection: async (connection, password) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,6 +2,7 @@ import { create } from "zustand"
|
||||
import type { Session, Message, Part, Event, MessageWithParts, Client } from "../lib/sdk"
|
||||
import { useConnections } from "./connections"
|
||||
import { useSettings } from "./settings"
|
||||
import { addBreadcrumb } from "../lib/sentry"
|
||||
|
||||
// Helper to convert API response to our internal format
|
||||
function parseMessages(response: MessageWithParts[]): { messages: Message[]; parts: Record<string, Part[]> } {
|
||||
@@ -96,6 +97,7 @@ export const useSessions = create<SessionsState>((set, get) => ({
|
||||
return
|
||||
}
|
||||
|
||||
addBreadcrumb({ category: "session", message: "select", data: { sessionID, hasDirectory: Boolean(directory) } })
|
||||
try {
|
||||
// Reset optimistic sending — SSE sessionStatus is the source of truth
|
||||
set((state) => ({
|
||||
|
||||
Reference in New Issue
Block a user