fix(mobile): report stalled RPC diagnostics

This commit is contained in:
Brennan Benson
2026-08-03 17:25:15 -07:00
parent c6b21f44e4
commit eb287ddf3e
3 changed files with 39 additions and 4 deletions
+8 -3
View File
@@ -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 (
<View style={[styles.container, { paddingTop: insets.top + spacing.sm }]}>
@@ -112,7 +115,9 @@ export default function ConnectionLogScreen() {
<>
<View style={styles.statusRow}>
<Text style={styles.statusText}>
{state}
{state === 'connected' && rpcUnresponsiveSince != null
? 'Connected, not responding'
: state}
{reconnectAttempts > 0 ? ` · attempt ${reconnectAttempts}` : ''}
</Text>
<Pressable style={styles.copyButton} onPress={() => void copyDiagnostics()}>
@@ -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)')
})
})
@@ -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'