Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions advanced/observability.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,13 @@ Spans are grouped under a stable `wa.<area>.<op>` 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).

Expand Down
16 changes: 16 additions & 0 deletions api/transport.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
```

`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");
Expand Down
15 changes: 12 additions & 3 deletions concepts/events.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 is_clean_shutdown() not documented in the linked DisconnectReason reference

The text directs users to call reason.is_clean_shutdown(), then links them to /api/transport#disconnected for the variants. However, api/transport.mdx only documents the four enum variants (ServerClose, StreamEnded, ReadError, Unknown) — there is no helper-methods section and no mention of is_clean_shutdown() anywhere in the file. A developer following the link will find the variants but no explanation of which variant(s) the method considers a clean shutdown, or that the method exists at all. The helper should be documented in api/transport.mdx alongside the enum definition.

Prompt To Fix With AI
This is a comment left during a code review.
Path: concepts/events.mdx
Line: 242

Comment:
`is_clean_shutdown()` not documented in the linked `DisconnectReason` reference

The text directs users to call `reason.is_clean_shutdown()`, then links them to `/api/transport#disconnected` for the variants. However, `api/transport.mdx` only documents the four enum variants (`ServerClose`, `StreamEnded`, `ReadError`, `Unknown`) — there is no helper-methods section and no mention of `is_clean_shutdown()` anywhere in the file. A developer following the link will find the variants but no explanation of which variant(s) the method considers a clean shutdown, or that the method exists at all. The helper should be documented in `api/transport.mdx` alongside the enum definition.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code


**Behavior:** Client automatically attempts reconnection

<Note>
Breaking change: `Disconnected` gained the `reason` field (previously a unit struct). Update `Event::Disconnected(Disconnected)` patterns to `Event::Disconnected(Disconnected { reason })` or `Event::Disconnected(_)`.
</Note>

### ConnectFailure

**Emitted:** When connection fails with a specific reason
Expand Down