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/api/transport.mdx b/api/transport.mdx index 444c6330..1a1b8081 100644 --- a/api/transport.mdx +++ b/api/transport.mdx @@ -185,6 +185,24 @@ 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` 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 | + ```rust TransportEvent::Disconnected(reason) => { tracing::info!(%reason, "transport closed"); diff --git a/concepts/events.mdx b/concepts/events.mdx index 988d0076..aab3c5f2 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 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)] -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