From e16414ac6a73659c24e2994819345695602e70c6 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Wed, 20 May 2026 13:51:05 +0530 Subject: [PATCH] fix(observability): classify socket transport wire-shape variants (Wave 4 Lane N) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend `is_network_unreachable_message` with three substring arms for wire shapes the existing `dns error` / status-bearing matchers miss: - `failed to lookup address` — libc `getaddrinfo()` rendering when tungstenite wraps the resolver fail as an `IO error` without the `dns error` prefix (OPENHUMAN-TAURI-44 ~50 events). - `nodename nor servname` — companion phrase from the macOS/BSD libc resolver — same OPENHUMAN-TAURI-44 wire shape, second anchor. - `http error: 200 ok` — tungstenite's `WsError::Http(200)` rendering when a captive portal / corporate proxy intercepts the WS upgrade handshake and returns a plain HTML 200 page (OPENHUMAN-TAURI-4P ~66 events). Tungstenite-only — reqwest renders HTTP 200 as `HTTP status server error (200)` so there is no collision with the regular HTTP path. A precedence test (`http_200_classifier_does_not_silence_unrelated_log_lines`) pins the substring against benign `HTTP/1.1 200 OK` / `status: 200 OK` prose so a future broadening does not silence success traces. Sentry has no remediation path for any of these — the user must change their network (firewall / proxy / DNS). Closes ~116 additional events. Closes OPENHUMAN-TAURI-44 Closes OPENHUMAN-TAURI-4P --- src/core/observability.rs | 64 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/src/core/observability.rs b/src/core/observability.rs index 2b285dcd2..cd74790af 100644 --- a/src/core/observability.rs +++ b/src/core/observability.rs @@ -296,15 +296,34 @@ fn is_loopback_unavailable(lower: &str) -> bool { /// through [`is_loopback_unavailable`] *before* this matcher so the /// boot-window race against the embedded core keeps its own bucket — see /// the precedence comment in [`expected_error_kind`]. +/// +/// Three additional substrings cover wire-shape variants observed in +/// Wave 4 that the original `"dns error"` / status-code matchers miss: +/// +/// - `"failed to lookup address"` / `"nodename nor servname"` — +/// `getaddrinfo()` failure renderings on macOS / BSD libc and POSIX +/// resolvers (`OPENHUMAN-TAURI-44` ~50 events, +/// `[socket] Connection failed: WebSocket connect: IO error: failed to +/// lookup address information: nodename nor servname provided, or not +/// known`). +/// - `"http error: 200 ok"` — tungstenite's `WsError::Http(200)` render +/// when a corporate proxy / captive portal intercepts the WebSocket +/// handshake and returns a plain HTML 200 page (`OPENHUMAN-TAURI-4P` +/// ~66 events). Tungstenite-only — reqwest renders HTTP 200 as +/// `"HTTP status server error (200)"`, so this can't collide with the +/// regular HTTP call path. fn is_network_unreachable_message(lower: &str) -> bool { lower.contains("error sending request for url") || lower.contains("dns error") + || lower.contains("failed to lookup address") + || lower.contains("nodename nor servname") || lower.contains("connection refused") || lower.contains("connection reset") || lower.contains("network is unreachable") || lower.contains("no route to host") || lower.contains("tls handshake") || lower.contains("certificate verify failed") + || lower.contains("http error: 200 ok") } /// Detect transient upstream HTTP failures that have bubbled up out of the @@ -1243,6 +1262,51 @@ mod tests { ); } + #[test] + fn classifies_wave4_socket_transport_wire_shapes() { + // OPENHUMAN-TAURI-44 (~50 events): libc `getaddrinfo()` rendering + // without the `dns error` token, wrapped by the socket emit site. + // The Wave 4 matcher arms catch the literal resolver phrases that + // the original `dns error` substring would miss when reqwest's + // wrapper isn't in the chain (e.g. tungstenite IO errors). + assert_eq!( + expected_error_kind( + "[socket] Connection failed (sustained outage after 5 attempts): \ + WebSocket connect: IO error: failed to lookup address information: \ + nodename nor servname provided, or not known" + ), + Some(ExpectedErrorKind::NetworkUnreachable) + ); + + // OPENHUMAN-TAURI-4P (~66 events): tungstenite renders a captive + // portal / corporate proxy that intercepts the WS handshake as + // `WsError::Http(200)` → `"HTTP error: 200 OK"`. Classify as + // network-unreachable since no amount of app-side retry can pierce + // an intercepting proxy. + assert_eq!( + expected_error_kind( + "[socket] Connection failed (sustained outage after 5 attempts): \ + WebSocket connect: HTTP error: 200 OK" + ), + Some(ExpectedErrorKind::NetworkUnreachable) + ); + } + + #[test] + fn http_200_classifier_does_not_silence_unrelated_log_lines() { + // The captive-portal arm anchors on `"http error: 200 ok"` (the + // exact tungstenite `WsError::Http(200)` Display rendering). + // Adjacent non-WebSocket log lines that mention `"HTTP/1.1 200 OK"` + // or `"status: 200 OK"` MUST NOT classify — those are normal-flow + // success traces, not failure events. Pin this precedence so a + // future refactor doesn't broaden the substring. + assert_eq!(expected_error_kind("HTTP/1.1 200 OK"), None); + assert_eq!( + expected_error_kind("upstream returned status: 200 OK after retry"), + None + ); + } + #[test] fn classifies_transient_upstream_http_errors() { // OPENHUMAN-TAURI-5Z: the canonical shape emitted by