mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 23:14:37 +00:00
fix(observability): classify socket transport wire-shape variants (Wave 4 Lane N)
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
This commit is contained in:
@@ -296,15 +296,34 @@ fn is_loopback_unavailable(lower: &str) -> bool {
|
|||||||
/// through [`is_loopback_unavailable`] *before* this matcher so the
|
/// through [`is_loopback_unavailable`] *before* this matcher so the
|
||||||
/// boot-window race against the embedded core keeps its own bucket — see
|
/// boot-window race against the embedded core keeps its own bucket — see
|
||||||
/// the precedence comment in [`expected_error_kind`].
|
/// 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 {
|
fn is_network_unreachable_message(lower: &str) -> bool {
|
||||||
lower.contains("error sending request for url")
|
lower.contains("error sending request for url")
|
||||||
|| lower.contains("dns error")
|
|| lower.contains("dns error")
|
||||||
|
|| lower.contains("failed to lookup address")
|
||||||
|
|| lower.contains("nodename nor servname")
|
||||||
|| lower.contains("connection refused")
|
|| lower.contains("connection refused")
|
||||||
|| lower.contains("connection reset")
|
|| lower.contains("connection reset")
|
||||||
|| lower.contains("network is unreachable")
|
|| lower.contains("network is unreachable")
|
||||||
|| lower.contains("no route to host")
|
|| lower.contains("no route to host")
|
||||||
|| lower.contains("tls handshake")
|
|| lower.contains("tls handshake")
|
||||||
|| lower.contains("certificate verify failed")
|
|| lower.contains("certificate verify failed")
|
||||||
|
|| lower.contains("http error: 200 ok")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Detect transient upstream HTTP failures that have bubbled up out of the
|
/// 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]
|
#[test]
|
||||||
fn classifies_transient_upstream_http_errors() {
|
fn classifies_transient_upstream_http_errors() {
|
||||||
// OPENHUMAN-TAURI-5Z: the canonical shape emitted by
|
// OPENHUMAN-TAURI-5Z: the canonical shape emitted by
|
||||||
|
|||||||
Reference in New Issue
Block a user