Skip to content

feat(metrics): optional metrics layer (off by default, Prometheus/OTLP-ready) - #734

Merged
jlucaso1 merged 2 commits into
mainfrom
feat/metrics
Jun 6, 2026
Merged

feat(metrics): optional metrics layer (off by default, Prometheus/OTLP-ready)#734
jlucaso1 merged 2 commits into
mainfrom
feat/metrics

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Jun 6, 2026

Copy link
Copy Markdown
Collaborator

What

Adds an opt-in metrics feature that emits wa_* metrics through the metrics facade for rates and latency percentiles, complementing the tracing spans from #733. Together: spans give the trace of one case, metrics give the rates/percentiles for dashboards.

Off by default: with the feature disabled there is no metrics dependency and zero overhead - every emit is an inlined no-op and the duration Timer is a zero-sized type that reads no clock. The library only emits; the application installs a recorder (Prometheus, OTLP, ...). See examples/metrics.rs.

Design

Same philosophy as the tracing work: the library emits through a facade, the app owns the recorder; no Prometheus/OTLP dependency in the library.

  • metrics = { optional = true } in wacore; feature metrics = ["wacore/metrics"] on the main crate, not in default.
  • wacore::telemetry (re-exported as whatsapp_rust::telemetry) provides typed helpers (counters/gauges + a record-on-drop Timer), so call sites stay clean. Durations use the pluggable wacore::time::Instant so WASM/deterministic builds are unaffected.

PII / cardinality

Labels are strictly low-cardinality categorical values (outcome/kind/result/reason). A JID, phone number or message id is never used as a label - it would explode the metrics backend and leak PII. The duration histograms are unlabeled (the matching _total counter carries the categorical breakdown).

Metrics

Counters: wa_recv_total{outcome}, wa_send_total{kind}, wa_retry_receipt_total{reason}, wa_iq_total{result}, wa_reconnect_total, wa_stream_error_total, wa_connect_total{outcome}, wa_appstate_sync_total{outcome}, wa_identity_change_total, wa_prekey_upload_total{outcome}.

Histograms: wa_iq_duration_seconds, wa_connect_duration_seconds, wa_decrypt_duration_seconds, wa_send_duration_seconds, wa_appstate_sync_duration_seconds.

Gauges: wa_connected (plus a wa_pending_retries helper).

Emitted at the same boundaries as the wa.* spans. RetryReason gained a stable as_str() for the reason label.

Verification

  • cargo clippy --all-targets -- -D warnings clean both with and without --features metrics.
  • cargo fmt --all -- --check clean.
  • cargo test --workspace --exclude e2e-tests: 1992 passed, 0 failed. The instrumentation is additive and cfg-gated, so no behavior change.

Usage

cargo run --example metrics --features metrics

…P-ready)

Adds an opt-in `metrics` feature that emits wa_* metrics through the `metrics` facade for rates and latency percentiles, complementing the tracing spans (PR #733). Off by default: no dependency and zero overhead - every emit is an inlined no-op and the duration Timer is a zero-sized type that reads no clock. The library only emits; the application installs a recorder (Prometheus, OTLP, ...). See examples/metrics.rs.

wacore::telemetry provides typed helpers (counters/gauges plus a record-on-drop Timer), so call sites stay clean and labels are strictly low-cardinality categorical values (outcome/kind/result/reason) - never a JID, phone number or message id, which would explode the backend and leak PII.

Counters: wa_recv_total{outcome}, wa_send_total{kind}, wa_retry_receipt_total{reason}, wa_iq_total{result}, wa_reconnect_total, wa_stream_error_total, wa_connect_total{outcome}, wa_appstate_sync_total{outcome}, wa_identity_change_total, wa_prekey_upload_total{outcome}.
Histograms: wa_iq_duration_seconds, wa_connect_duration_seconds, wa_decrypt_duration_seconds, wa_send_duration_seconds, wa_appstate_sync_duration_seconds.
Gauges: wa_connected (plus wa_pending_retries helper).

Emitted at the same boundaries as the wa.* spans (connect/reconnect, recv/decrypt, send, IQ, app-state, retry, identity, prekey). Durations use the pluggable wacore::time::Instant so WASM/deterministic builds are unaffected. RetryReason gained a stable as_str() for the reason label.

Verified: clippy --all-targets -- -D warnings clean both with and without --features metrics; fmt clean; cargo test --workspace --exclude e2e-tests green (1992 passed, 0 failed; additive and cfg-gated, no behavior change).
@coderabbitai

coderabbitai Bot commented Jun 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: fe2ab722-e26d-4137-a178-a08fa9797aa3

📥 Commits

Reviewing files that changed from the base of the PR and between bd29059 and 0454264.

📒 Files selected for processing (10)
  • src/client/app_state.rs
  • src/client/lifecycle.rs
  • src/handlers/notification.rs
  • src/message/receive.rs
  • src/message/retry.rs
  • src/prekeys.rs
  • src/request.rs
  • src/send.rs
  • wacore/src/protocol/retry.rs
  • wacore/src/telemetry.rs

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Optional metrics/observability added with a telemetry facade and re-export; metrics are no-ops when disabled.
    • Instrumentation added for connection lifecycle, message send/receive, retries, app-state sync, pre-key uploads, and stream errors.
    • Prometheus exporter support and a gated example demonstrating metric collection.
  • Tests

    • Added a test to ensure stable string labels for retry reasons.

Walkthrough

This PR adds a feature-gated telemetry facade (wacore::telemetry), wires a workspace-level metrics feature and dev Prometheus exporter, and instruments connection lifecycle, message processing, IQ requests, sends, app-state sync, and pre-key uploads with metric emissions.

Changes

Metrics Instrumentation & Observability

Layer / File(s) Summary
Telemetry Module & Helper Methods
wacore/src/telemetry.rs, wacore/src/protocol/retry.rs, wacore/src/lib.rs
Implements a feature-gated metrics facade (counters, gauges, timers, describe()). Adds histogram name constants and a drop-based Timer. Adds RetryReason::as_str() and unit test. Exports telemetry from wacore.
Cargo Features & Public Re-exports
Cargo.toml, wacore/Cargo.toml, src/lib.rs, examples/metrics.rs
Adds workspace and wacore metrics features/dependencies, metrics-exporter-prometheus in dev-deps, an example gated by the feature, and re-exports wacore::telemetry at crate root.
Connection Lifecycle Instrumentation
src/client/lifecycle.rs, src/client/node_io.rs
Emits connect outcome markers and CONNECT_DURATION timer, sets/clears wa_connected gauge on connect/disconnect/cleanup, emits reconnect() and stream_error() markers.
Message Processing Instrumentation
src/message/receive.rs, src/message/dispatch.rs, src/message/retry.rs, src/handlers/notification.rs
Adds DECRYPT_DURATION timer (post-lock), emits recv("decrypted") and recv("undecryptable"), records retry_receipt(reason) using stable strings, and emits identity_change().
IQ Request Instrumentation
src/request.rs
Wraps send_and_wait_iq with IQ_DURATION timer, captures select outcome, and emits `iq("ok"
Send & State Sync Instrumentation
src/send.rs, src/client/app_state.rs
Records SEND_DURATION and categorical send(kind) for message sends (including status/newsletter cases). Times app-state sync (APPSTATE_SYNC_DURATION), emits `appstate_sync("ok"
Pre-key Upload Instrumentation
src/prekeys.rs
Emits `prekey_upload("ok"

Sequence Diagram(s)

sequenceDiagram
  participant App as Client Application
  participant Lifecycle as Lifecycle
  participant Message as Message Handler
  participant Request as IQ Handler
  participant Telemetry as wacore::telemetry

  App->>Lifecycle: connect()
  Lifecycle->>Telemetry: timer(CONNECT_DURATION)
  Lifecycle->>Telemetry: connect("ok" | "fail")

  App->>Message: process_session_enc_batch()
  Message->>Telemetry: timer(DECRYPT_DURATION)
  Message->>Telemetry: recv("decrypted")

  App->>Request: send_and_wait_iq()
  Request->>Telemetry: timer(IQ_DURATION)
  Request->>Telemetry: iq(status)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

api-design

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the primary change: adding an optional metrics layer with Prometheus/OTLP support, which is the core objective of this changeset.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining what metrics were added, design philosophy, PII considerations, and specific metric names/types emitted.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/metrics

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@jlucaso1

jlucaso1 commented Jun 6, 2026

Copy link
Copy Markdown
Collaborator Author

@claude review

@claude

claude Bot commented Jun 6, 2026

Copy link
Copy Markdown

Claude finished @jlucaso1's task in 4m 32s —— View job


Claude PR Review — feat(metrics)

Nice surface — the dep:metrics gating + zero-sized Timer give the no-op promise that the PR claims, and the wacore re-export means call sites stay clean. The story I'd push back on is that the emit sites don't all match the metric names and docs: a few outcomes are silently never recorded, and one counter is double-counted. Below are the concrete findings.

Findings

1. wa_appstate_sync_total{outcome="fail"} is never emittedsrc/client/app_state.rs:65-117

let _t = wacore::telemetry::timer(wacore::telemetry::APPSTATE_SYNC_DURATION);
// ...
Ok(()) => { wacore::telemetry::appstate_sync("ok"); return Ok(()); }
Err(e) => { /* ... */ return Err(e); }   // ← no appstate_sync("fail")

The duration histogram captures both branches but only the ok counter ever increments. The success rate will read 100% even when sync repeatedly fails. Add appstate_sync("fail") on the terminal return Err(e) at L116. (sync_collections_batched_inner has no telemetry at all — if that's intentional, document it; otherwise the critical-sync path is uninstrumented.)

2. wa_iq_total misses every transport-error pathsrc/request.rs:211-267

let _t = wacore::telemetry::timer(wacore::telemetry::IQ_DURATION);  // L211
if !self.is_running.load(...) { return Err(IqError::NotConnected); } // L212 — bypasses counter
// ...
if let Err(e) = send_fn.await { /* Socket / EncryptSend errors */ return ... } // L231 — bypasses counter
// ...
wacore::telemetry::iq(match &result { Ok(_) => "ok", Err(IqError::Timeout) => "timeout", Err(_) => "error" }); // L263

The histogram ticks on every path (good), but the counter only ticks after the futures::select. All early returns for NotConnected, socket failure, encrypt-send failure, or ClientState errors record a duration but no result label — so the per-result rate is wrong and total - ok - timeout - error ≠ histogram count. Either hoist the iq(...) call into the early-return branches or wrap the body so the counter ticks unconditionally before returning.

3. wa_retry_receipt_total counts attempts, not sendssrc/message/retry.rs:218

The docstring on retry_receipt in wacore/src/telemetry.rs:37 says "Retry receipt sent, by reason." The call site:

async fn run_retry_receipt(...) -> bool {
    wacore::telemetry::retry_receipt(reason.as_str());   // ← unconditional
    let Some(retry_count) = self.increment_retry_count(...).await else {
        // max retries hit → PDO instead, no receipt sent
        ...
    };
    let retry_sent = match self.send_retry_receipt(...).await { Ok(()) => true, Err(_) => false };

This increments for capped-out attempts (which send a PDO, not a receipt) and for failed sends. Either move the emit past the cap check + send success, or rename the metric and docstring to "retry-receipt attempts".

4. wa_send_total mis-buckets newsletters and skips revoke/pin/status-postsrc/send.rs:399-404

wacore::telemetry::send(match to.server {
    wacore_binary::Server::Group => "group",
    wacore_binary::Server::Broadcast => "status",
    _ => "dm",  // ← Newsletter / Hosted / Bot / Messenger / Interop all bucketed as "dm"
});

Server::Newsletter falls into _ => "dm" even though the newsletter branch at L428 takes a separate plaintext path. More importantly, send_status_message (public client.status()) and send_message_impl (revoke / pin / edit) emit nothing — the timer + counter only fire from send_message_with_options. Either route every send through a single helper that records, or add explicit emits at each entry point, and add a newsletter arm to the kind label.

5. wa_pending_retries gauge is declared but never setwacore/src/telemetry.rs:73, 168

pub fn set_pending_retries(n: u64) exists in both branches; grepping the whole workspace, nothing calls it. The gauge will sit at 0 forever. The natural call sites are pending_retries.insert/.clear in src/client/lifecycle.rs and src/retry.rs. Either wire it in (size the gauge on insert/remove) or drop the helper.

6. wa_prekey_upload_total misses the login-time uploadsrc/prekeys.rs:111-138, 137, 162-164

upload_pre_keys_at_login calls upload_pre_keys_inner() directly; the prekey_upload(if ok…) emit sits inside upload_pre_keys (L163), which is only on the count-guard path and refresh_pre_keys. Initial-upload outcomes never count. Move the emit into upload_pre_keys_inner (the actual shared chokepoint).

7. Documented recv outcomes that never firewacore/src/telemetry.rs:28

Docstring lists decrypted/duplicate/undecryptable/skmsg. Grep over the tree shows only decrypted (dispatch.rs:14) and undecryptable (retry.rs:35). duplicate and skmsg are documented but never emitted, so dashboards built off the docstring will silently miss two important outcomes (duplicates during reconnect, SKDM-only sessions). Either emit them at the matching points in receive.rs (the DuplicatedMessage branches, the SKDM-only outcome path) or trim the docstring.

8. wa_identity_change_total counts received, not appliedsrc/handlers/notification.rs:312

The counter ticks at the top of handle_identity_change, before the companion-device filter (L316) and self-identity filter (L334). Counter ≠ "identity changes handled" (telemetry.rs:143). Move below the filters or rename.

Smaller things

  • metrics-exporter-prometheus = "0.16" is in unconditional [dev-dependencies] (Cargo.toml:179). The example only needs it under --features metrics. Every cargo test pulls a fat exporter that won't run. Workarounds: move the example into its own examples-only crate, or accept the dev-time cost; can't make a dev-dep optional.
  • connect("ok"|"fail") is recorded in lifecycle.rs:281/291, one frame above the _t = timer(CONNECT_DURATION) inside connect(). Functional, but means a panic inside connect() records a histogram sample with no matching counter. Minor.
  • app_state.rs:660 import wacore_binary::Server is unused since this file's edit is only recv("decrypted") → wait that's send.rs. Skip.

Verdict

The shape (feature-gated metrics dep, ZST Timer, low-cardinality labels, application-installed recorder) is right. Before this lands I'd want findings 1-4 addressed since they make the resulting dashboards mislead about the very thing the metrics are supposed to surface (failure rates, send mix). The rest can be follow-ups.

Happy to apply any subset on this branch if you confirm. Apply findings 1-4 →

  • Read PR diff against origin/main
  • Review wacore/src/telemetry.rs (core helpers, Timer, no-op macros)
  • Review feature wiring in Cargo.toml files
  • Review emit sites (request.rs, send.rs, receive.rs, lifecycle.rs, app_state.rs, prekeys.rs, retry.rs, dispatch.rs, notification.rs)
  • Review examples/metrics.rs and RetryReason::as_str()
  • Post review
    · branch: feat/metrics

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bd29059c9a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/client/lifecycle.rs
)]
pub async fn disconnect(self: &Arc<Self>) {
info!("Disconnecting client intentionally.");
wacore::telemetry::set_connected(false);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clear connected gauge during all connection cleanup

When the socket drops through the normal run-loop cleanup path (for example a read loop error, stream error, or auto-reconnect) run() calls cleanup_connection_state() without going through disconnect(), so this new wa_connected gauge remains at 1 for the entire offline/backoff window and can stay stale after shutdown with auto-reconnect disabled. Since cleanup_connection_state() is the authoritative place that clears is_connected, the gauge needs to be cleared there rather than only in the explicit disconnect API.

Useful? React with 👍 / 👎.

Comment thread src/request.rs
}
}
};
wacore::telemetry::iq(match &result {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Count IQ send failures as error outcomes

With the counter only emitted after the select!, any IQ whose send_fn fails (socket error, encrypt/send error, or not connected after the waiter was registered) returns above this point and never increments wa_iq_total{result="error"}. Those are completed IQ attempts at the same chokepoint, so dashboards will undercount IQ errors exactly during connection/send failures.

Useful? React with 👍 / 👎.

Comment thread src/client/app_state.rs
match res {
Ok(()) => return Ok(()),
Ok(()) => {
wacore::telemetry::appstate_sync("ok");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Emit failed app-state sync outcomes

This records only successful app-state syncs; when process_app_state_sync_task ultimately returns a non-retryable error (or a DB-lock error after the retry limit), the function returns Err(e) later without ever calling appstate_sync("fail"). The advertised wa_appstate_sync_total{outcome} metric therefore has no failure samples, hiding failed syncs from metrics users.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/client/lifecycle.rs (1)

575-680: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Critical: The wa_connected gauge isn't being reset when the connection drops unexpectedly.

Look, when cleanup_connection_state() clears is_connected at line 602, it needs to also call wacore::telemetry::set_connected(false). Right now if the read loop crashes or the transport dies, we clean up the atomic state but leave the Prometheus gauge sitting at 1.0. That means your monitoring dashboards will show the client connected when it's actually offline.

The disconnect() path works fine because you call set_connected(false) at line 482 before cleanup. But when run() calls cleanup directly at line 314 after an unexpected disconnect, the gauge never gets cleared.

This is a data integrity issue—the telemetry layer and the connection state must stay synchronized. Production monitoring depends on these metrics being accurate.

🔧 Fix: Add telemetry marker to cleanup
 pub(crate) async fn cleanup_connection_state(&self) {
+    // Clear the connected gauge to match is_connected atomic state.
+    // Ensures wa_connected stays synchronized on both expected and unexpected disconnects.
+    wacore::telemetry::set_connected(false);
+
     // Note: node_waiters are intentionally NOT cleared here — they are
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/lifecycle.rs` around lines 575 - 680, The
cleanup_connection_state() path clears the atomic is_connected flag but does not
update the Prometheus gauge; add a call to
wacore::telemetry::set_connected(false) immediately after the line that sets
self.is_connected.store(false, Ordering::Release) so telemetry stays in sync on
unexpected disconnects (ensure the wacore::telemetry symbol is in scope or
fully-qualified if necessary).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/client/app_state.rs`:
- Around line 74-77: The code only emits a success metric via
wacore::telemetry::appstate_sync("ok") on the OK path; update the
error/early-return paths to emit a failure metric as well (e.g., call
wacore::telemetry::appstate_sync("error") immediately before every Err(e) return
in the same function), specifically add the call just before the terminal return
that currently returns Err(e) (and any other early Err returns) so failures are
counted alongside successes.

In `@src/handlers/notification.rs`:
- Line 312: The telemetry call wacore::telemetry::identity_change() is invoked
too early and currently counts every identity-change push received rather than
only those that pass validation and trigger a session reset; move the telemetry
invocation inside the branch gated by had_prior_identity (the logic that
actually performs the session reset) after the validation checks so it only
increments for processed identity changes, or alternatively rename the metric to
reflect it tracks received notifications (e.g., identity_change_received) if you
intend to keep the current placement.

In `@src/message/receive.rs`:
- Line 512: The DECRYPT_DURATION timer in process_session_enc_batch is started
before awaiting per-sender locks (session_lock_for(...).await /
lock_arc().await), so wa_decrypt_duration_seconds currently includes lock wait
time; either move the let _t =
wacore::telemetry::timer(wacore::telemetry::DECRYPT_DURATION) line to after the
session_guard is acquired (i.e., after session_lock_for and lock_arc complete)
to measure crypto-only time, or rename/update the metric/constant
(DECRYPT_DURATION / wa_decrypt_duration_seconds) and its documentation to
explicitly state it includes lock/queue wait time so the metric semantics are
correct.

In `@src/message/retry.rs`:
- Line 218: The telemetry call wacore::telemetry::retry_receipt is being emitted
too early; change the flow so you only increment the retry_receipt metric after
a retry receipt is actually sent and succeeds: call
wacore::telemetry::retry_receipt inside the branch where
increment_retry_count(Some/Ok) returns a counter and where the subsequent send
of the retry receipt (the code path that currently follows that check, not the
PDO fallback) succeeds; leave the PDO fallback branch unchanged (or
alternatively rename the metric to wa_retry_attempt_total if you intend to count
attempts rather than sent receipts). Ensure you update the location around
increment_retry_count and the send/retry-success branch so the metric is not
incremented before the retry-cap check or before the send completes.

In `@src/prekeys.rs`:
- Around line 162-164: The login path calls upload_pre_keys_inner() directly
from upload_pre_keys_at_login(), bypassing the telemetry emitted by
upload_pre_keys(); modify upload_pre_keys_at_login() to either (A) wrap the
inner call with the same telemetry call (use wacore::telemetry::prekey_upload
with "ok"/"fail" based on the Result returned by upload_pre_keys_inner()), or
(B, preferred) call the instrumented wrapper upload_pre_keys(true).await instead
of upload_pre_keys_inner().await so the prekey_upload metric is always emitted
for login flows—if you choose option B, confirm that passing force=true
preserves the intended login semantics (i.e., bypasses the count-guard) before
changing the call.
- Around line 162-164: The telemetry is emitted per attempt in upload_pre_keys
(the block calling upload_pre_keys_inner and wacore::telemetry::prekey_upload),
causing multiple metrics for retries; move the wacore::telemetry::prekey_upload
call out of upload_pre_keys_inner/upload_pre_keys and into
upload_pre_keys_with_retry so only the final operation outcome is recorded:
remove the per-attempt prekey_upload emission from upload_pre_keys/_inner and
have upload_pre_keys_with_retry call wacore::telemetry::prekey_upload(if
final_result.is_ok() { "ok" } else { "fail" }) after retries complete.

In `@wacore/src/protocol/retry.rs`:
- Around line 64-84: Add an exhaustive unit test for RetryReason::as_str() that
asserts each enum variant (UnknownError, NoSession, InvalidKey, InvalidKeyId,
InvalidMessage, InvalidSignature, FutureMessage, BadMac, InvalidSession,
InvalidMsgKey, BadBroadcastEphemeralSetting, UnknownCompanionNoPrekey,
AdvFailure, StatusRevokeDelay) returns the exact expected static string;
implement the test by explicitly listing every variant and comparing
variant.as_str() to its canonical label (e.g.,
RetryReason::UnknownError.as_str() == "unknown"), and mark the test as failing
if any mapping changes or if new variants are added so maintainers must update
the expected labels accordingly.

---

Outside diff comments:
In `@src/client/lifecycle.rs`:
- Around line 575-680: The cleanup_connection_state() path clears the atomic
is_connected flag but does not update the Prometheus gauge; add a call to
wacore::telemetry::set_connected(false) immediately after the line that sets
self.is_connected.store(false, Ordering::Release) so telemetry stays in sync on
unexpected disconnects (ensure the wacore::telemetry symbol is in scope or
fully-qualified if necessary).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 619373a8-2520-4bce-a3c2-7172926d7f8a

📥 Commits

Reviewing files that changed from the base of the PR and between 3c3da27 and bd29059.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (17)
  • Cargo.toml
  • examples/metrics.rs
  • src/client/app_state.rs
  • src/client/lifecycle.rs
  • src/client/node_io.rs
  • src/handlers/notification.rs
  • src/lib.rs
  • src/message/dispatch.rs
  • src/message/receive.rs
  • src/message/retry.rs
  • src/prekeys.rs
  • src/request.rs
  • src/send.rs
  • wacore/Cargo.toml
  • wacore/src/lib.rs
  • wacore/src/protocol/retry.rs
  • wacore/src/telemetry.rs

Comment thread src/client/app_state.rs
Comment thread src/handlers/notification.rs Outdated
Comment thread src/message/receive.rs Outdated
Comment thread src/message/retry.rs Outdated
Comment thread src/prekeys.rs Outdated
Comment thread wacore/src/protocol/retry.rs
@github-actions

github-actions Bot commented Jun 6, 2026

Copy link
Copy Markdown

Benchmark Results

67 unchanged benchmark(s)
Benchmark Current Baseline Change
reporting_token_benchmark::content_extraction_group::bench_content_extraction simple:setup_simple_message() 2,925 2,925 +0.0%
reporting_token_benchmark::content_extraction_group::bench_content_extraction extended:setup_extended_message() 8,446 8,446 +0.0%
reporting_token_benchmark::key_derivation_group::bench_key_derivation 31,317 31,317 +0.0%
reporting_token_benchmark::token_calculation_group::bench_token_calculation 13,827 13,827 +0.0%
reporting_token_benchmark::full_generation_group::bench_full_token_generation simple:setup_full_gen_simple() 49,485 49,485 +0.0%
reporting_token_benchmark::full_generation_group::bench_full_token_generation extended:setup_full_gen_extended() 55,001 55,001 +0.0%
reporting_token_benchmark::message_encoding_group::bench_message_encoding simple:setup_simple_message() 1,679 1,679 +0.0%
reporting_token_benchmark::message_encoding_group::bench_message_encoding extended:setup_extended_message() 4,393 4,393 +0.0%
send_receive_benchmark::dm_send::bench_dm_send text:setup_dm_send() 113,050 113,210 -0.1%
send_receive_benchmark::dm_recv::bench_dm_recv text:setup_dm_recv() 1,656,619 1,656,620 -0.0%
send_receive_benchmark::group_send::bench_group_send group_10:setup_group_send_10() 651,607 651,832 -0.0%
send_receive_benchmark::group_send::bench_group_send group_50:setup_group_send_50() 875,922 875,820 +0.0%
send_receive_benchmark::group_send::bench_group_send group_256:setup_group_send_256() 2,083,489 2,083,623 -0.0%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_10:setup_group_skdm_10() 749,069 749,069 +0.0%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_50:setup_group_skdm_50() 1,329,906 1,326,042 +0.3%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_256:setup_group_skdm_256() 4,373,843 4,373,838 +0.0%
send_receive_benchmark::group_recv::bench_group_recv text:setup_group_recv() 514,739 518,700 -0.8%
binary_benchmark::marshal_group::bench_marshal_allocating 45,395 45,395 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_allocating 45,445 45,445 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_allocating 66,348 66,348 +0.0%
binary_benchmark::marshal_group::bench_marshal_reusing_buffer 43,506 43,506 +0.0%
binary_benchmark::marshal_group::bench_marshal_reusing_buffer_vec_writer 45,501 45,501 +0.0%
binary_benchmark::marshal_group::bench_marshal_long_string 4,936 4,936 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_long_string 4,967 4,967 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_long_string 6,738 6,738 +0.0%
binary_benchmark::marshal_group::bench_marshal_huge_bytes_allocating 528,539 528,539 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_huge_bytes_allocating 528,152 528,152 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_huge_bytes_allocating 529,398 529,398 +0.0%
binary_benchmark::marshal_group::bench_marshal_many_children_allocating 5,417,742 5,417,742 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_many_children_allocating 5,362,091 5,362,091 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_many_children_allocating 13,276,430 13,276,430 +0.0%
binary_benchmark::unmarshal_group::bench_unmarshal small:setup_small_marshaled() 1,850 1,850 +0.0%
binary_benchmark::unmarshal_group::bench_unmarshal large:setup_large_marshaled() 29,217 29,217 +0.0%
binary_benchmark::unpack_group::bench_unpack_uncompressed 618 618 +0.0%
binary_benchmark::unpack_group::bench_unpack_compressed 672,890 672,890 +0.0%
binary_benchmark::attr_parser_group::bench_attr_parser attr_lookup:setup_attr_marshaled() 3,736 3,736 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip small:setup_small_marshaled() 3,840 3,840 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip large:setup_large_marshaled() 48,274 48,274 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_auto small:setup_small_marshaled() 3,866 3,866 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_auto large:setup_large_marshaled() 48,335 48,335 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_exact small:setup_small_marshaled() 5,206 5,206 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_exact large:setup_large_marshaled() 66,659 66,659 +0.0%
binary_benchmark::child_iteration_group::bench_get_children_by_tag 310,312 310,312 +0.0%
binary_benchmark::jid_optimization_group::bench_jid_to_owned_access jid_access:setup_jid_heavy_marshaled() 8,282 8,282 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_u32 254 254 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_u32 91 91 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_u64 292 292 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_u64 137 137 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_i64 317 317 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_i64 145 145 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_loop_100_u64 27,425 27,425 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_loop_100_u64 10,725 10,725 +0.0%
libsignal_benchmark::dm_group::bench_dm_session_establishment setup:setup_dm_users() 4,139,116 4,140,286 -0.0%
libsignal_benchmark::dm_group::bench_dm_encrypt_first_message first_msg:setup_dm_session() 100,133 100,133 +0.0%
libsignal_benchmark::dm_group::bench_dm_decrypt_first_message decrypt_prekey:setup_dm_with_first_message() 4,264,189 4,264,189 +0.0%
libsignal_benchmark::dm_group::bench_dm_encrypt_subsequent_message subsequent:setup_established_dm_session() 100,399 100,399 +0.0%
libsignal_benchmark::group_messaging_group::bench_group_create_distribution_message create:setup_group_sender() 210,249 210,262 -0.0%
libsignal_benchmark::group_messaging_group::bench_group_encrypt_message encrypt:setup_group_with_distribution() 496,921 496,921 +0.0%
libsignal_benchmark::group_messaging_group::bench_group_decrypt_message decrypt:setup_group_with_encrypted_message() 510,182 506,534 +0.7%
libsignal_benchmark::conversation_group::bench_full_dm_conversation full:setup_conversation_data() 11,974,850 11,974,604 +0.0%
libsignal_benchmark::signature_group::bench_signature_creation sign:setup_keypair_with_message() 2,466,138 2,466,138 +0.0%
libsignal_benchmark::signature_group::bench_signature_verification verify:setup_keypair_with_message() 4,931,312 4,930,802 +0.0%
libsignal_benchmark::signature_group::bench_key_generation keygen 2,043,397 2,043,397 +0.0%
libsignal_benchmark::session_optimization_group::bench_decrypt_with_previous_session previous_session:setup_with_archived_sessions() 37,404 37,404 +0.0%
libsignal_benchmark::session_optimization_group::bench_out_of_order_decryption out_of_order:setup_out_of_order_messages() 3,617,967 3,617,967 +0.0%
libsignal_benchmark::session_optimization_group::bench_promote_matching_session promote:setup_promote_matching_session() 230,648 230,648 +0.0%
libsignal_benchmark::session_optimization_group::bench_message_key_eviction eviction:setup_message_key_eviction() 9,980,959 9,980,959 +0.0%
No significant changes detected.

Addresses the Codex/CodeRabbit/Claude review: several counters recorded a
duration but no outcome, or counted attempts instead of completed operations.

- wa_connected: cleared in cleanup_connection_state() so it drops on every
  disconnect (run-loop drop / reconnect), not only the explicit disconnect() API.
- wa_iq_total: emit "error" on the three early-return paths (NotConnected,
  send failure) so the counter matches the duration histogram on every exit.
- wa_appstate_sync_total: emit "fail" on the terminal Err so the success rate
  isn't pinned at 100%; also wire wa_appstate_mutations_total at the two
  mutation-dispatch sites (it was declared but never emitted).
- wa_retry_receipt_total: move the emit into the send-success branch so it counts
  receipts actually sent, not capped-out attempts (which send a PDO) or failed
  sends. Matches the "Retry receipt sent" docstring.
- wa_send_total: add a "newsletter" arm (was bucketed as "dm") and count status
  posts in send_status_message. Deliberately not instrumenting send_message_impl:
  it is shared with internal protocol traffic (PDO, app-state key requests) that
  must not inflate send counts.
- wa_prekey_upload_total: emit once per logical operation (the retry wrapper's
  final outcome + the login path) instead of once per retry attempt.
- wa_identity_change_total: count past the companion/self/no-prior-identity gates
  so it reflects actual session resets, not every push received; describe updated.
- wa_decrypt_duration_seconds: start the timer after the per-sender session lock
  so it measures crypto time, not lock/queue contention.
- Drop the unused wa_pending_retries gauge (high-churn transient state, no call site).
- Add an exhaustive RetryReason::as_str() stability test (label drift guard).

Verified: clippy --all-targets -- -D warnings clean with and without
--features metrics; fmt clean; cargo test --workspace --exclude e2e-tests green.
@jlucaso1
jlucaso1 merged commit f6c94fa into main Jun 6, 2026
11 checks passed
@jlucaso1
jlucaso1 deleted the feat/metrics branch June 6, 2026 18:14
@mintlify

mintlify Bot commented Jun 6, 2026

Copy link
Copy Markdown

Docs PR opened: oxidezap/whatsapp-rust-docs#270

Added an Advanced page documenting the new opt-in metrics feature, its wa_* metric catalogue, recorder wiring, and overhead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant