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>
This commit is contained in:
Den
2026-05-22 17:07:44 -07:00
committed by GitHub
parent e9991cb61b
commit 30be6636c2
11 changed files with 796 additions and 11 deletions

View File

@@ -12,10 +12,13 @@ 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()
export default function RootLayout() {
function RootLayout() {
const colorScheme = useColorScheme()
const isDark = colorScheme === "dark"
@@ -118,3 +121,5 @@ export default function RootLayout() {
</GestureHandlerRootView>
)
}
export default wrap(RootLayout)

View File

@@ -14,6 +14,8 @@ import { router, useLocalSearchParams } from "expo-router"
import { Ionicons } from "@expo/vector-icons"
import { useConnections } from "../../src/stores/connections"
import type { ConnectionType } from "../../src/lib/types"
import { probeConnection, shareReport } from "../../src/lib/diagnostics"
import { captureDiagnostic } from "../../src/lib/sentry"
const CONNECTION_TYPES: Array<{
type: ConnectionType
@@ -79,14 +81,25 @@ export default function EditConnectionScreen() {
},
password || undefined,
)
if (result.ok) {
setIsTesting(false)
Alert.alert("Success", "Connection successful!")
return
}
// Failed: run active diagnostics, capture to Sentry, offer a shareable report.
const report = await probeConnection(
url.trim(),
username.trim() && password ? { username: username.trim(), password } : undefined,
)
captureDiagnostic(report, result.error ? new Error(result.error) : undefined)
setIsTesting(false)
Alert.alert(
result.ok ? "Success" : "Failed",
result.ok
? "Connection successful!"
: `Could not connect to ${url.trim()}\n\n${result.error || "Check the URL and credentials."}`,
)
Alert.alert("Connection Failed", `${report.summary}\n\n(${result.error || "no detail"})`, [
{ text: "OK", style: "cancel" },
{ text: "Share report", onPress: () => shareReport(report) },
])
}
const handleSave = async () => {

View File

@@ -14,6 +14,8 @@ import { router } from "expo-router"
import { Ionicons } from "@expo/vector-icons"
import { useConnections } from "../../src/stores/connections"
import type { ConnectionType } from "../../src/lib/types"
import { probeConnection, shareReport } from "../../src/lib/diagnostics"
import { captureDiagnostic } from "../../src/lib/sentry"
export default function AddConnectionScreen() {
const colorScheme = useColorScheme()
@@ -90,11 +92,20 @@ export default function AddConnectionScreen() {
setIsConnecting(false)
router.back()
} else {
// Failed: run active diagnostics, capture to Sentry, offer a shareable report.
const report = await probeConnection(
serverUrl,
username.trim() && password ? { username: username.trim(), password } : undefined,
)
captureDiagnostic(report, result.error ? new Error(result.error) : undefined)
setIsConnecting(false)
Alert.alert(
"Connection Failed",
`Could not connect to ${serverUrl}\n\n${result.error || "Unknown error"}\n\nMake sure:\n1. OpenCode is running: opencode serve --hostname 0.0.0.0\n2. You're on the same network (or Tailscale is connected)\n3. The address is correct (use the IP for tailnet if MagicDNS isn't enabled)`,
[{ text: "OK" }],
`${report.summary}\n\nTarget: ${serverUrl}\nError: ${result.error || "Unknown error"}`,
[
{ text: "OK", style: "cancel" },
{ text: "Share report", onPress: () => shareReport(report) },
],
)
}
}