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:
66
src/lib/sentry.ts
Normal file
66
src/lib/sentry.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
// Thin Sentry wrapper. No-ops cleanly when no DSN is configured so dev/CI
|
||||
// builds work without secrets. DSN comes from EXPO_PUBLIC_SENTRY_DSN
|
||||
// (Expo inlines EXPO_PUBLIC_* at build time).
|
||||
import * as Sentry from "@sentry/react-native"
|
||||
import { log } from "./logbuffer"
|
||||
import type { DiagnosticReport } from "./diagnostics"
|
||||
|
||||
const DSN = process.env.EXPO_PUBLIC_SENTRY_DSN
|
||||
let enabled = false
|
||||
|
||||
export function initSentry() {
|
||||
if (!DSN) {
|
||||
log.info("sentry", "no DSN configured — telemetry disabled")
|
||||
return
|
||||
}
|
||||
try {
|
||||
Sentry.init({
|
||||
dsn: DSN,
|
||||
// Capture breadcrumbs but keep performance tracing off by default.
|
||||
tracesSampleRate: 0,
|
||||
enableAutoSessionTracking: true,
|
||||
// Don't send PII; connection URLs are attached explicitly + scrubbed below.
|
||||
sendDefaultPii: false,
|
||||
})
|
||||
enabled = true
|
||||
log.info("sentry", "initialized")
|
||||
} catch (e) {
|
||||
log.warn("sentry", "init failed", String(e))
|
||||
}
|
||||
}
|
||||
|
||||
// Strip basic-auth credentials from a URL before it leaves the device.
|
||||
function scrubUrl(url: string): string {
|
||||
return url.replace(/\/\/[^@/]+@/, "//<redacted>@")
|
||||
}
|
||||
|
||||
export function captureDiagnostic(report: DiagnosticReport, rawError?: unknown) {
|
||||
log.info("sentry", "capture", report.classification, enabled ? "(uploading)" : "(local only)")
|
||||
if (!enabled) return
|
||||
Sentry.withScope((scope) => {
|
||||
scope.setTag("connect.classification", report.classification)
|
||||
scope.setTag("connect.scheme", report.scheme ?? "n/a")
|
||||
scope.setContext("connection", {
|
||||
url: scrubUrl(report.url),
|
||||
host: report.host,
|
||||
port: report.port,
|
||||
isHostname: report.isHostname,
|
||||
summary: report.summary,
|
||||
})
|
||||
scope.setContext("probes", {
|
||||
attempts: report.attempts.map((a) => ({
|
||||
name: a.name,
|
||||
ok: a.ok,
|
||||
status: a.status,
|
||||
durationMs: a.durationMs,
|
||||
error: a.error,
|
||||
cause: a.errorCause,
|
||||
})),
|
||||
})
|
||||
scope.setContext("device", report.device)
|
||||
const err = rawError instanceof Error ? rawError : new Error(`connect ${report.classification}: ${report.summary}`)
|
||||
Sentry.captureException(err)
|
||||
})
|
||||
}
|
||||
|
||||
export const wrap = Sentry.wrap
|
||||
Reference in New Issue
Block a user