diff --git a/mobile/app/connection-log.tsx b/mobile/app/connection-log.tsx index e2a9b73ff7f..61db4ac09a5 100644 --- a/mobile/app/connection-log.tsx +++ b/mobile/app/connection-log.tsx @@ -12,7 +12,8 @@ import { connectionLogStore } from '../src/transport/connection-log-buffer' import { useHostClient } from '../src/transport/client-context' import { useLastConnectedAt, - useReconnectAttempt + useReconnectAttempt, + useRpcUnresponsiveSince } from '../src/transport/client-context-connection-metrics' import { buildConnectionDiagnosticsReport } from '../src/diagnostics/connection-diagnostics-report' import type { ConnectionLogEntry, HostProfile } from '../src/transport/types' @@ -49,6 +50,7 @@ export default function ConnectionLogScreen() { const { state } = useHostClient(selected?.id) const reconnectAttempts = useReconnectAttempt(selected?.id) const lastConnectedAt = useLastConnectedAt(selected?.id) + const rpcUnresponsiveSince = useRpcUnresponsiveSince(selected?.id) const subscribe = useCallback( (listener: () => void) => @@ -71,6 +73,7 @@ export default function ConnectionLogScreen() { state, reconnectAttempts, lastConnectedAt, + rpcUnresponsiveSince, platform: `${Platform.OS} ${Platform.Version ?? ''}`.trim(), appVersion: Constants.expoConfig?.version ?? 'unknown', entries @@ -78,7 +81,7 @@ export default function ConnectionLogScreen() { await Clipboard.setStringAsync(report) setCopied(true) setTimeout(() => setCopied(false), 2000) - }, [selected, state, reconnectAttempts, lastConnectedAt, entries]) + }, [selected, state, reconnectAttempts, lastConnectedAt, rpcUnresponsiveSince, entries]) return ( @@ -112,7 +115,9 @@ export default function ConnectionLogScreen() { <> - {state} + {state === 'connected' && rpcUnresponsiveSince != null + ? 'Connected, not responding' + : state} {reconnectAttempts > 0 ? ` ยท attempt ${reconnectAttempts}` : ''} void copyDiagnostics()}> diff --git a/mobile/src/diagnostics/connection-diagnostics-report.test.ts b/mobile/src/diagnostics/connection-diagnostics-report.test.ts index d2326ffa022..1a25ef84a04 100644 --- a/mobile/src/diagnostics/connection-diagnostics-report.test.ts +++ b/mobile/src/diagnostics/connection-diagnostics-report.test.ts @@ -52,4 +52,24 @@ describe('buildConnectionDiagnosticsReport', () => { expect(report).toContain('Last connected: never this session') expect(report).toContain('No connection events recorded this session.') }) + + it('reports a connected application RPC stall', () => { + const report = buildConnectionDiagnosticsReport({ + hostName: 'Host 3', + endpoint: 'ws://100.65.9.106:6768', + state: 'connected', + reconnectAttempts: 0, + lastConnectedAt: NOW - 60_000, + rpcUnresponsiveSince: NOW - 30_000, + platform: 'ios 26.5.1', + appVersion: '0.0.33', + entries: [], + nowMs: NOW + }) + + expect(report).toContain( + 'State: connected, application RPC not responding (reconnect attempts: 0)' + ) + expect(report).toContain('Application RPC stalled: 2026-07-09T21:59:30.000Z (30s ago)') + }) }) diff --git a/mobile/src/diagnostics/connection-diagnostics-report.ts b/mobile/src/diagnostics/connection-diagnostics-report.ts index c762c69799e..b04adcbbf3f 100644 --- a/mobile/src/diagnostics/connection-diagnostics-report.ts +++ b/mobile/src/diagnostics/connection-diagnostics-report.ts @@ -11,6 +11,7 @@ export function buildConnectionDiagnosticsReport(args: { state: ConnectionState reconnectAttempts: number lastConnectedAt: number | null + rpcUnresponsiveSince?: number | null platform: string appVersion: string entries: readonly ConnectionLogEntry[] @@ -25,7 +26,16 @@ export function buildConnectionDiagnosticsReport(args: { lines.push( `Endpoint: ${formatEndpoint(args.endpoint)}${isTailscaleEndpoint(args.endpoint) ? ' (Tailscale)' : ''}` ) - lines.push(`State: ${args.state} (reconnect attempts: ${args.reconnectAttempts})`) + const stateLabel = + args.state === 'connected' && args.rpcUnresponsiveSince != null + ? 'connected, application RPC not responding' + : args.state + lines.push(`State: ${stateLabel} (reconnect attempts: ${args.reconnectAttempts})`) + if (args.rpcUnresponsiveSince != null) { + lines.push( + `Application RPC stalled: ${new Date(args.rpcUnresponsiveSince).toISOString()} (${formatAgo(now - args.rpcUnresponsiveSince)} ago)` + ) + } lines.push( args.lastConnectedAt == null ? 'Last connected: never this session'