Files
opencode-mobile/app/_layout.tsx
Den 30be6636c2 feat(diagnostics): active connect-failure triage + Sentry + in-app share (#2)
* feat(diagnostics): active connect-failure triage + Sentry + in-app share

Replaces the opaque "Connection Failed" / "Network request failed" dead-end
with on-device diagnostics that classify *why* a connect attempt failed.

On failure (quick connect and edit-connection test), the app now:
- runs parallel probes: target /global/health, target root, and a public
  204 endpoint (internet reachability check)
- classifies the cause: malformed-url, no-internet, server-unreachable,
  health-failed, tls-error, timeout
- shows a plain-English summary + a "Share report" button that copies a
  full report (target URL, per-probe results w/ error.cause, device/app
  info, recent log ring-buffer) to the clipboard and opens the share sheet
- captures the same structured context to Sentry (auto-upload), gated on
  EXPO_PUBLIC_SENTRY_DSN so dev/CI builds work without secrets

New: src/lib/logbuffer.ts (ring buffer + logger), src/lib/diagnostics.ts
(regex URL parse — Hermes URL is incomplete — probe + report + share),
src/lib/sentry.ts (no-op-without-DSN wrapper, scrubs basic-auth from URLs).

Wired Sentry.wrap around RootLayout and initSentry() at module load.

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

* ci(sentry): wire Sentry DSN + source-map upload env into build; bump to 0.2.2

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 17:07:44 -07:00

126 lines
3.9 KiB
TypeScript

import { useEffect, useRef } from "react"
import { Stack, router } from "expo-router"
import { StatusBar } from "expo-status-bar"
import { useColorScheme, View, ActivityIndicator } from "react-native"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { GestureHandlerRootView } from "react-native-gesture-handler"
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet"
import { useAuth } from "../src/stores/auth"
import { useConnections } from "../src/stores/connections"
import { useEvents } from "../src/stores/events"
import { useCatalog } from "../src/stores/catalog"
import { useSettings } from "../src/stores/settings"
import { AuthGate } from "../src/components/AuthGate"
import * as notifications from "../src/lib/notifications"
import { initSentry, wrap } from "../src/lib/sentry"
initSentry()
const queryClient = new QueryClient()
function RootLayout() {
const colorScheme = useColorScheme()
const isDark = colorScheme === "dark"
const { initialize: initAuth, isLoading: authLoading } = useAuth()
const { loadConnections, isLoading: connectionsLoading, client } = useConnections()
const sseStarted = useRef(false)
useEffect(() => {
initAuth()
loadConnections()
useSettings.getState().load()
// Connect notification preferences to the notification module
notifications.configure(() => useSettings.getState().notifications)
// Navigate to session when user taps a notification
return notifications.onTap((data) => {
router.push(`/session/${data.sessionId}`)
})
}, [])
// Connect/disconnect SSE and load catalog when client changes
useEffect(() => {
if (client && !sseStarted.current) {
sseStarted.current = true
useEvents.getState().connect()
useCatalog.getState().load()
} else if (!client && sseStarted.current) {
sseStarted.current = false
useEvents.getState().disconnect()
}
return () => {
if (sseStarted.current) {
sseStarted.current = false
useEvents.getState().disconnect()
}
}
}, [client])
const isLoading = authLoading || connectionsLoading
if (isLoading) {
return (
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
backgroundColor: isDark ? "#0a0a0a" : "#ffffff",
}}
>
<ActivityIndicator size="large" color={isDark ? "#ffffff" : "#0a0a0a"} />
</View>
)
}
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<BottomSheetModalProvider>
<QueryClientProvider client={queryClient}>
<AuthGate>
<Stack
screenOptions={{
headerStyle: {
backgroundColor: isDark ? "#0a0a0a" : "#ffffff",
},
headerTintColor: isDark ? "#ffffff" : "#0a0a0a",
contentStyle: {
backgroundColor: isDark ? "#0a0a0a" : "#ffffff",
},
}}
>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen
name="session/[id]"
options={{
title: "Session",
presentation: "card",
}}
/>
<Stack.Screen
name="connection/add"
options={{
title: "Add Connection",
presentation: "modal",
}}
/>
<Stack.Screen
name="connection/[id]"
options={{
title: "Edit Connection",
presentation: "modal",
}}
/>
</Stack>
<StatusBar style={isDark ? "light" : "dark"} />
</AuthGate>
</QueryClientProvider>
</BottomSheetModalProvider>
</GestureHandlerRootView>
)
}
export default wrap(RootLayout)