Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
18 changes: 18 additions & 0 deletions api/transport.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
// ...
}
}
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` 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");
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 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.

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