From 9439dc09f7f90b87cca8d3e537788d7fd0f25db2 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Jul 2026 01:41:05 +0000 Subject: [PATCH 1/4] docs: Disconnected event now carries a DisconnectReason (whatsapp-rust#956) Reflects the breaking change where events::Disconnected gained a reason field distinguishing routine server-initiated stream recycles from genuine transport failures, and documents the two additional spans (wa.conn.connect, wa.conn.read_loop) that now tag lid/pn and the err-level split between them. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0116e1C7aqCk2AdecsCpf6E6 --- advanced/observability.mdx | 4 ++-- concepts/events.mdx | 15 ++++++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/advanced/observability.mdx b/advanced/observability.mdx index e6d7fd7c..3fb064d8 100644 --- a/advanced/observability.mdx +++ b/advanced/observability.mdx @@ -105,13 +105,13 @@ Spans are grouped under a stable `wa..` naming scheme so you can filte ### Levels -Most spans are emitted at `debug` or `trace`. The connection-lifecycle spans (`wa.conn.connect`, `wa.conn.disconnect`, `wa.conn.reconnect`, `wa.conn.run`, `wa.conn.logout`) are at `info` so connection state is visible at the default level. Failures surface at `ERROR` via `err(Debug)` on the instrumented function, and the existing `warn!`/`error!` log calls surface through the bridge. +Most spans are emitted at `debug` or `trace`. The connection-lifecycle spans (`wa.conn.connect`, `wa.conn.disconnect`, `wa.conn.reconnect`, `wa.conn.run`, `wa.conn.logout`) are at `info` so connection state is visible at the default level. Failures surface at `ERROR` via `err(Debug)` on the instrumented function, and the existing `warn!`/`error!` log calls surface through the bridge — with two exceptions: `wa.conn.connect` surfaces failures at `WARN` instead (its caller already classifies the real failures as `error!`, so the default `ERROR` was double-reporting transient handshake retries), and `wa.conn.read_loop` returns `Ok` (not `Err`) for a routine server-initiated stream recycle, so its `ERROR` capture fires only for genuine failures — never for WhatsApp's normal periodic reconnects. A downstream binary can statically strip lower levels at compile time with `tracing`'s `release_max_level_info` / `release_max_level_warn` features. ### Account identity in spans -The `wa.conn.run`, `wa.iq`, and `wa.send.message` spans carry `lid` and `pn` fields for your own account, so traces are filterable and groupable per account in multi-account deployments. `pn` is redacted via `Jid::observe()` like any other phone-number field (see [PII handling](#pii-handling) below); `lid` is rendered in full since it is pseudonymous. +The `wa.conn.run`, `wa.conn.connect`, `wa.conn.read_loop`, `wa.iq`, and `wa.send.message` spans carry `lid` and `pn` fields for your own account, so traces are filterable and groupable per account in multi-account deployments. `pn` is redacted via `Jid::observe()` like any other phone-number field (see [PII handling](#pii-handling) below); `lid` is rendered in full since it is pseudonymous. `wa.conn.run` re-records both fields on every reconnect-loop iteration rather than once before the loop, since a freshly-paired device has no `lid`/`pn` yet on the first pass — pairing resolves them on a detached task with no ambient span, so the identity appears on the *next* reconnect iteration instead (a routine event: server-initiated stream-end, network blips). diff --git a/concepts/events.mdx b/concepts/events.mdx index 988d0076..ae56c238 100644 --- a/concepts/events.mdx +++ b/concepts/events.mdx @@ -227,17 +227,26 @@ Event::Connected(_) => { ### Disconnected -**Emitted:** When connection is lost +**Emitted:** When the connection is lost unexpectedly — not on an intentional client-initiated shutdown or reconnect ```rust #[derive(Debug, Clone, Serialize)] -pub struct Disconnected; +pub struct Disconnected { + pub reason: DisconnectReason, +} -Event::Disconnected(Disconnected) +Event::Disconnected(Disconnected { reason }) ``` +**Fields:** +- `reason: DisconnectReason` — why the transport ended. Check `reason.is_clean_shutdown()` to tell a routine server-initiated stream recycle (WhatsApp's normal reconnect path) apart from a genuine transport failure, without parsing logs. See [`DisconnectReason`](/api/transport#disconnected) for the variants. + **Behavior:** Client automatically attempts reconnection + +Breaking change: `Disconnected` gained the `reason` field (previously a unit struct). Update `Event::Disconnected(Disconnected)` patterns to `Event::Disconnected(Disconnected { reason })` or `Event::Disconnected(_)`. + + ### ConnectFailure **Emitted:** When connection fails with a specific reason From e788e12bfb32b197fa68738a84bd4b6242d63f94 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Jul 2026 01:44:02 +0000 Subject: [PATCH 2/4] docs: document DisconnectReason::is_clean_shutdown() concepts/events.mdx sends readers to /api/transport#disconnected for DisconnectReason details but the method it tells them to call wasn't documented there (caught by Greptile review on #365). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0116e1C7aqCk2AdecsCpf6E6 --- api/transport.mdx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/api/transport.mdx b/api/transport.mdx index 444c6330..c0d1f058 100644 --- a/api/transport.mdx +++ b/api/transport.mdx @@ -185,6 +185,22 @@ pub enum DisconnectReason { `DisconnectReason` implements `Display` for human-readable logging. Custom transports should emit the most specific variant they can determine. +```rust +impl DisconnectReason { + pub fn is_clean_shutdown(&self) -> bool; +} +``` + +`is_clean_shutdown()` tells a benign, server-initiated stream recycle (the normal WhatsApp reconnect path) apart from a genuine transport failure, so consumers of [`events::Disconnected`](/concepts/events#disconnected) don't have to parse logs to classify a disconnect. It's deliberately conservative — anything ambiguous returns `false` (treated as a real failure): + +| Variant | `is_clean_shutdown()` | +|---------|------------------------| +| `StreamEnded` | `true` — EOF with no close frame is how the WA server recycles a connection | +| `ServerClose { code: None \| Some(1000) \| Some(1001), .. }` | `true` — no code, normal closure, or going-away are graceful | +| `ServerClose { code: Some(_), .. }` (any other code) | `false` — protocol/server error, restart, etc. | +| `ReadError(_)` | `false` — a transport read/IO error is always a real failure | +| `Unknown` | `false` — an unreported reason is never assumed benign | + ```rust TransportEvent::Disconnected(reason) => { tracing::info!(%reason, "transport closed"); From 214baf772c818d579b928b618c137bf7e3a6f41a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Jul 2026 01:48:27 +0000 Subject: [PATCH 3/4] docs: fix impl block syntax and table pipe escaping in transport.mdx Give is_clean_shutdown() a real function body (a bare semicolon is invalid Rust in an inherent impl, only valid in traits/externs), and drop the escaped pipes inside table-cell code spans in favor of prose (caught by cubic and Greptile review on #365). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0116e1C7aqCk2AdecsCpf6E6 --- api/transport.mdx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/api/transport.mdx b/api/transport.mdx index c0d1f058..1a1b8081 100644 --- a/api/transport.mdx +++ b/api/transport.mdx @@ -187,7 +187,9 @@ pub enum DisconnectReason { ```rust impl DisconnectReason { - pub fn is_clean_shutdown(&self) -> bool; + pub fn is_clean_shutdown(&self) -> bool { + // ... + } } ``` @@ -196,8 +198,8 @@ impl DisconnectReason { | Variant | `is_clean_shutdown()` | |---------|------------------------| | `StreamEnded` | `true` — EOF with no close frame is how the WA server recycles a connection | -| `ServerClose { code: None \| Some(1000) \| Some(1001), .. }` | `true` — no code, normal closure, or going-away are graceful | -| `ServerClose { code: Some(_), .. }` (any other code) | `false` — protocol/server error, restart, etc. | +| `ServerClose` with code `None`, `Some(1000)`, or `Some(1001)` | `true` — no code, normal closure, or going-away are graceful | +| `ServerClose` with any other code | `false` — protocol/server error, restart, etc. | | `ReadError(_)` | `false` — a transport read/IO error is always a real failure | | `Unknown` | `false` — an unreported reason is never assumed benign | From fd640321502736b1fc4df8c89fc397bb415b97b3 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Jul 2026 01:51:45 +0000 Subject: [PATCH 4/4] docs: clarify Disconnected's Emitted wording (Greptile nit) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "unexpectedly" read as abnormal/error-only, but the event also fires for a routine clean server recycle — reword to say what actually distinguishes it (not client-initiated) instead. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0116e1C7aqCk2AdecsCpf6E6 --- concepts/events.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/concepts/events.mdx b/concepts/events.mdx index ae56c238..aab3c5f2 100644 --- a/concepts/events.mdx +++ b/concepts/events.mdx @@ -227,7 +227,7 @@ Event::Connected(_) => { ### Disconnected -**Emitted:** When the connection is lost unexpectedly — not on an intentional client-initiated shutdown or reconnect +**Emitted:** When the connection ends without the client itself intentionally closing or reconnecting it — covers both a routine server-initiated stream recycle and a genuine transport failure (see `reason` below to tell them apart) ```rust #[derive(Debug, Clone, Serialize)]