Skip to content

feat(client): network reports show how connections hold up on each network - #876

Merged
incognitojam merged 7 commits into
mainfrom
styal/mobile-network-condition-reports
Oct 17, 2026
Merged

incognitojam merged 7 commits into
mainfrom
styal/mobile-network-condition-reports

Conversation

@incognitojam

@incognitojam incognitojam commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

Note

Clients now summarize how each connection behaves on each network type: time connected, drops and why, reconnect time, heartbeat round trips, data, and route switches. Mobile lists them under Settings → About → Diagnostics, and servers record them as client.network.report so a dashboard can show whether styal holds up on trains and weak cellular. Reports name the network type, never the network. No native modules, so mobile ships by OTA.

We had no way to tell how styal behaves on a train or a weak signal. Nothing on the client measured round trips, drops, reconnect time or data, and relay traces only cover T3 Connect attempts. Upstream has no client-side connection measurement either (checked against pingdotgg/t3code main at cfafb4b).

What it measures

packages/client-runtime/src/networkReports follows every environment connection in web, desktop and mobile and splits it into windows of foreground use, at most 15 minutes, starting a new one when the network type or route changes. Each window records:

  • time connected, reconnecting and offline, and the time to a working connection when the window started without one (after a dead zone, or when a session died in the background)
  • drops by cause: heartbeats unanswered (the client closed the socket), socket closed, health check timed out, other
  • failed attempts, and how many ran out of setup time
  • route switches without a failure, and switches back after the switched-to session failed
  • how long each reconnect took after its drop, listed individually (up to 20 per window) so dashboards can take exact percentiles
  • heartbeat round trips (p50, p90, max), taken from the existing RPC ping and pong, so measuring adds no traffic
  • payload bytes and messages each way, and the peak receive rate per second

The socket meter shares the RPC session's WebSocket constructor wrapper with the received-frame counter from pingdotgg#19426.

A drop is a session that fails after answering in the foreground. A socket the OS closed while the app was in the background is found on return and counts toward the time to a working connection instead, so every reopen does not read as a network failure. On mobile, network type changes wait 1.5 s and then read the state afresh: expo-network briefly reports "none" during a Wi-Fi/cellular handover.

Reports carry the network interface type (Wi-Fi, cellular, ethernet, VPN, none) and route kind (styal Link, LAN, Tailscale, public, SSH). They never include network or carrier names, addresses, location, or environment identifiers. Connections to the same device are not reported, so the desktop app's own server adds nothing.

Where reports go

  • Server: environments advertise a networkReports capability and accept server.reportNetworkConditions. Each report becomes a client.network.report event with the connection's client dimensions, dated when its window ended, so reports sent after a dead zone land in the period they describe. T3CODE_TELEMETRY_ENABLED=false still stops them.
  • Mobile: reports persist for 7 days in a file and are listed under Settings → About → Diagnostics with totals per network type and a Clear action. Removing an environment forgets its reports. Unsent reports wait for the next connection to their own environment.
  • Web: a hidden tab ends the current window. The network type comes from navigator.connection, which only Chromium on Android provides. Reports are kept in memory only.

Validation

Android emulator (pixel_7_api_37_b, styal Dev) against an isolated dev server seeded from real data, with PostHog pointed at a local collector:

  • Simulated train trip (Wi-Fi, then cellular, a 35 s dead zone, cellular back): one report per network. The Wi-Fi to cellular switch dropped and reconnected in 0.8 s. The dead zone recorded one drop, and recovery came 35 s after it. The server dated each event at its window end.
  • Latency through a TCP proxy adding a fixed delay: 300 ms each way gave a round-trip p50 of 606 ms; 1,500 ms each way gave 3,007 ms.
  • Resume after 41 s in the background: no drop, "first connected after 2.3 s". A dead zone during use still counted one drop, with recovery after 24.4 s.
  • Drop causes through the proxy: a 30 s delay left heartbeats unanswered and recorded "1 drop (stopped responding)", reconnected in 4.4 s. Tapping Reconnect with the delay on recorded "health check timed out", reconnected in 3.4 s. Closing the proxy's sockets recorded "disconnected". A failure before the session was ready counted as a failed attempt, not a drop.
  • Route switches: the server's reported direct addresses became learned routes. The app switched to one ("1 route switch"); that route's health check then failed and the app went back ("1 switch back after a failure"). A later drop failed over to the learned Tailscale route, which is not counted as a switch.
  • Persistence and clearing: reports survived an app restart, removing the environment cleared them, and Clear network reports emptied the list.

Settings → About → Diagnostics on Android with per-network totals and reports showing drop causes, a route switch and a switch back

Unit tests cover window splitting, drop causes, route switches, delivery (sent once, kept on device for servers without the capability, restored from storage), the backdated analytics event, and the mobile wording. The mobile native fingerprint is unchanged: no native modules were added, and none of the changed files is among the fingerprint inputs, so mobile ships by OTA.

The Connection reliability by network dashboard in PostHog splits connected share, drops per foreground hour by cause, reconnect p50/p95, round-trip p50/p95 and route switches by network type and route kind. Its queries run against the event and stay empty until a release ships it. docs/operations/observability.md links to it.

Not verified: iOS, the web client in a browser, and a styal Link route (the emulator ran against a dev server with LAN and Tailscale routes only).


Written by an agent (Claude Code, claude-opus-5-5).

@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Upstream divergence

Both sides are compared with upstream at the in-order tip 54b6b667f3. Excludes .repos/ and pnpm-lock.yaml.

Measure Base This PR Change
Upstream files the fork changes 1,413 1,415 +2
Lines changed in them 67,988 68,200 +212
Upstream files the fork removes 41 41 0
Fork-only files 475 491 +16
Fork-only lines 83,159 85,370 +2,211

Upstream files that start to differ (2)

  • apps/mobile/src/features/diagnostics/SettingsDiagnosticsRouteScreen.tsx: +3 −0 from upstream
  • packages/client-runtime/src/connection/layer.ts: +5 −0 from upstream
Upstream files whose difference changes (17)
File Change Now differs by
apps/server/src/telemetry/AnalyticsService.test.ts +46 182 lines
packages/client-runtime/src/connection/routes.ts +37 41 lines
docs/internals/product-analytics.md +23 55 lines
apps/server/src/telemetry/AnalyticsService.ts +19 53 lines
docs/user/remote-access.md +13 126 lines
packages/client-runtime/src/rpc/session.ts +13 46 lines
docs/operations/observability.md +9 35 lines
docs/user/telemetry.md +9 35 lines
packages/contracts/src/rpc.ts +8 182 lines
apps/server/src/ws.ts +7 231 lines
apps/mobile/src/connection/platform.ts +5 38 lines
apps/web/src/connection/platform.ts +5 31 lines
packages/client-runtime/package.json +4 20 lines
packages/contracts/src/environment.ts +3 17 lines
apps/server/src/auth/RpcAuthorization.ts +1 17 lines
apps/server/src/environment/ServerEnvironment.ts +1 16 lines
packages/contracts/src/index.ts +1 7 lines

@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 4.9 KiB 5.0 KiB +23 B (+0.5%) 6.8 KiB ✅
Codex Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅
Codex Live turn WebSocket wire 1.2 KiB 1.2 KiB +23 B (+1.9%) 2.0 KiB ✅
Codex Live turn WebSocket decoded 20.8 KiB 20.9 KiB +41 B (+0.2%) 29.3 KiB ✅
Codex Live turn messages 1 2 +1 (+100.0%) 8 ✅
Claude Total thread wire 5.0 KiB 5.0 KiB +24 B (+0.5%) 6.8 KiB ✅
Claude Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅
Claude Live turn WebSocket wire 1.2 KiB 1.2 KiB +24 B (+2.0%) 2.0 KiB ✅
Claude Live turn WebSocket decoded 21.2 KiB 21.2 KiB +41 B (+0.2%) 29.3 KiB ✅
Claude Live turn messages 1 2 +1 (+100.0%) 8 ✅

Baseline: 31a4a25 · PR result: bbd8e98 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 108.5 KiB
  • Claude decoded thread snapshot: 108.8 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@incognitojam
incognitojam force-pushed the styal/mobile-network-condition-reports branch from 72dbb0b to 6a37453 Compare October 16, 2026 16:26
…twork

Clients summarize each environment connection into windows of foreground
use, split when the network type or route changes and capped at 15 minutes:
time connected, drops, failed attempts, reconnect time, heartbeat round trips,
and data received and sent. Round trips come from the existing RPC
heartbeat, so measuring adds no traffic. Reports name the network type and
route kind only, never a network or carrier name.

Servers that advertise `networkReports` record each report as
`client.network.report`, dated when its window ended. Mobile keeps reports
for 7 days and lists them under Settings → About → Diagnostics.
Each window now splits drops by cause: heartbeats unanswered (the client
closed the socket), socket closed, and health check timed out. It counts
failed attempts that ran out of setup time, switches to a different route
without a failure, and switches back after the switched-to session failed.
These replace the separate heartbeat timeout count.
A report now carries the duration of each reconnect, up to 20 per window,
instead of the window's median and slowest. Dashboards can then take exact
reconnect percentiles across many windows. Mobile shows a reconnect even when
its drop fell in the previous report.
@incognitojam
incognitojam force-pushed the styal/mobile-network-condition-reports branch from 2b6b699 to f0c689c Compare October 17, 2026 14:59
@incognitojam
incognitojam marked this pull request as ready for review October 17, 2026 15:16
@incognitojam
incognitojam merged commit 8ec9187 into main Oct 17, 2026
34 checks passed
@incognitojam
incognitojam deleted the styal/mobile-network-condition-reports branch October 17, 2026 15:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant