Skip to content

feat(retry): WA Web log-level parity and retry-flow observability counters - #887

Merged
jlucaso1 merged 7 commits into
mainfrom
feat/retry-warn-levels-and-telemetry
Jun 17, 2026
Merged

feat(retry): WA Web log-level parity and retry-flow observability counters#887
jlucaso1 merged 7 commits into
mainfrom
feat/retry-warn-levels-and-telemetry

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

What

Two threads, one commit per change.

Log levels (2 commits): two retry-flow warn!s that fire for benign, remote-driven, fully-handled conditions move to the level WA Web uses for them. Refusing retry #N ... exceeds max attempts (the MAX_RETRY loop guard) goes to debug!, and Base key collision ... Forcing fresh session goes to info!. The collision line and its Base key changed sibling also start carrying the message_id.

Observability (4 commits): four low-cardinality, PII-safe, zero-cost-when-off counters on the retry flow, plus the retry count recorded on the existing receipt span.

Why

This came out of triaging ~14h of production logs from a group bot. The fleet was healthy (0 ERROR, 0 panic, 0 stanza errors), but the entire WARN budget was 25 lines of exactly two kinds, both produced by a single peer device thrashing prekeys during one group send. It was remote-driven and fully contained by the existing defenses (the MAX_RETRY cap, the per-chat resend rate limiter, the SKDM gate, base-key collision detection); the message was delivered and no ban occurred. A cross-model review (Codex/GPT-5.5) verified the oracle citations independently before this PR.

The core finding is that WA Web emits both of those conditions via WALogger.LOG (level 2, informational), not WARN (level 3):

  • WAWebHandleRetryRequest logs the retryCount >= MAX_RETRY (5) refusal via WALogger.LOG, and reserves WARN in the same module for real errors (no-requester, device-not-found).
  • WAWebUpdateLocalSignalSession logs the same-base-key session delete via WALogger.LOG.
  • WAWebLogger: LOG is level 2, WARN is level 3.

debug! (rather than info!) for the cap matches the lib's own precedent for a frequent, expected, remote-driven event: the mirror receive-side capped-retry path in message/retry.rs already logs at debug!, and decrypt_fail_log_level downgrades expected fan-out failures to debug to avoid WARN spam. info! for the collision matches the three sibling branches of update_local_signal_session, which already log at info. The message_id is added because the collision is keyed by (address, message_id), so the id makes the event correlatable to a specific message during a storm.

On observability: WA Web emits two dedicated WAM events on this flow that the lib had no aggregate equivalent for. MessageHighRetryCount (id 3132, committed at retryCount >= 5 from the send-receipt path) maps to wa_high_retry_total, and MdRetryFromUnknownDevice (id 2178, committed when a retry arrives from a device not in our registry, tagged primary vs companion) maps to wa_retry_unknown_device_total, placed at the retry call-site rather than inside schedule_unknown_device_sync (which the shared inbound path also drives, so it would over-count). Two more counters cover the responder events WA Web only logs and has no dedicated WAM for: wa_retry_refused_total and wa_base_key_collision_total, so a chronically thrashing peer shows up in aggregate instead of via log scraping. All four are off by default and compile to no-ops without the metrics feature. The span change records the retry count so storm depth is aggregable per sender even when the cap returns early.

What this PR deliberately leaves out

The retry logic itself is verbatim parity with the oracle and is untouched: the cap value and comparator, the base-key save/compare/delete, and the recovery path all stay as-is. Two latent parity divergences the review surfaced (peer regId-mismatch without <keys>, and offline-gating of the destructive key-bundle path) are intentionally not addressed here: both have zero occurrences in the analyzed logs, carry moderate regression risk on session recovery, and deserve a separate decision.

Tests

cargo clippy --all-targets -- -D warnings is clean both at default (no-op telemetry, tracing off) and with --features metrics,tracing (real counters plus the span field).
cargo test -p wacore --lib (991) and cargo test -p whatsapp-rust --lib (827) pass.
There is no behavior change: every line is a log level, an added log field, a counter increment, or a span field.

Review in cubic

jlucaso1 added 6 commits June 17, 2026 14:12
The MAX_RETRY cap refusal is a remote-driven, expected and fully-handled
condition (the requester's own count attribute reached 5). WA Web emits it
via WALogger.LOG (informational, level 2), not WARN (level 3) — see
WAWebHandleRetryRequest. It dominated the WARN budget in production logs
(23 of 25) despite being benign. debug! also matches the sibling
receive-side path (message/retry.rs already logs the capped case at debug).
The base-key collision branch forced a fresh session at warn!, while the
three sibling branches of update_local_signal_session (regId-mismatch
delete, base-key save, base-key changed) already log at info. WA Web logs
this same-base-key delete via WALogger.LOG, not WARN, so info restores both
internal and oracle parity. Also thread message_id into both base-key log
lines: the collision is keyed by (address, message_id), so the id makes the
event correlatable to a specific message during a retry storm.
WA Web emits the MessageHighRetryCount WAM event (id 3132) from the
send-receipt path when retryCount >= MAX (5). The lib had no aggregate
signal for this. Add a wa_high_retry_total counter (labelled by retry
reason, low-cardinality, PII-safe) emitted from the requester side
(run_retry_receipt) at the same watermark. Zero-cost when the metrics
feature is off (no-op sibling).
WA Web emits the MdRetryFromUnknownDevice WAM event (id 2178) when a retry
receipt arrives from a device that hasDevice() reports as unknown, tagged
primary vs companion. The lib's responder side had no aggregate signal. Add
a wa_retry_unknown_device_total counter at the same site (the !device_known
branch in handle_retry_receipt), labelled by sender type. Placed at the
retry call-site, not inside schedule_unknown_device_sync, since that helper
is also driven by the shared inbound path and would over-count.
The two responder-side retry health events were log-only, so measuring the
refuse/total ratio or spotting a chronically thrashing peer in aggregate
needed log scraping. Add wa_retry_refused_total (at the MAX_RETRY guard) and
wa_base_key_collision_total (at the forced-fresh-session branch). WA Web logs
both via WALogger.LOG rather than a dedicated WAM, so these are health
counters consistent with that, not a direct WAM port. Zero-cost when off.
The wa.retry.handle_receipt span already carries chat and sender but not the
retry count, and the cap path returns early before any other field is set, so
storm depth was invisible to span aggregation. Record count right after the
parse (field::Empty + Span::record, the established pattern), gated on the
tracing feature. PII-safe; no behavior change.
@jlucaso1

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

@jlucaso1 Look, I need things to work right. I'm going to review these changes — the log-level parity and observability counters need to be exactly right. Triggering the review now.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@jlucaso1

Copy link
Copy Markdown
Collaborator Author

@claude review

@claude

claude Bot commented Jun 17, 2026

Copy link
Copy Markdown

Claude finished @jlucaso1's task in 2m 37s —— View job


Code Review

I've read the three changed files, cross-checked the placement claims against the rest of the tree, and verified the cardinality of the new labels.

TL;DR: ship it. This is a tight, well-scoped, behavior-neutral PR. Log levels are justified against both the WA Web oracle citations and the lib's own precedent; the four counters are correctly placed, low-cardinality, PII-safe, and zero-cost when metrics is off; the span field is added in the canonical "Empty → record later" pattern.

Findings checklist

  • Read changed files
  • Verify log-level claims (debug! for cap, info! for collision)
  • Verify retry_unknown_device is placed at the call-site, not in the shared helper
  • Check feature-gating / zero-cost-when-off
  • Check span field change for retry count
  • Check label cardinality and PII

Correctness

wa_retry_unknown_device_total placement (src/retry.rs:289-300) — correct

The comment justifies putting the counter at the retry call-site instead of inside schedule_unknown_device_sync. Grepping confirms the helper is also driven from the shared inbound receive path (src/message/receive.rs:1244), so wrapping it would over-count. Placement matches the WA Web oracle (MdRetryFromUnknownDevice id 2178) and the prose claim in the PR body.

wa_high_retry_total placement (src/message/retry.rs:247-254) — correct, with one consistency nit

increment_retry_count short-circuits with None once the cap is reached (retry.rs:140), so retry_count == MAX_DECRYPT_RETRIES is only ever observed on the single call that crosses the threshold. The counter therefore increments exactly once per message that hits the cap. Good single-shot semantics matching WA Web's MessageHighRetryCount (id 3132).

Minor consistency observation (not blocking): the immediately-following wa_retry_receipt_total increment fires only inside the Ok(()) arm (line 258), while high_retry fires before send_retry_receipt is attempted. The PR's framing ("commits at retryCount >= MAX from the send-receipt path") is consistent with WA Web's intent-at-attempt semantics, so this is defensible — but a future reader comparing the two adjacent counters might wonder. Either is fine; if you want strict parity with the sibling, moving the high_retry call into the Ok(()) arm would do it.

wa_retry_refused_total and wa_base_key_collision_total — correct

Both are correctly co-located with their associated log line (which is the same site whose level was adjusted), so future log/metric correlation is one grep away.

Span field record (src/retry.rs:204, 227-228)

Canonical tracing pattern: declare as tracing::field::Empty at attach time, Span::current().record("count", retry_count) later. The #[cfg(feature = "tracing")] gate on the body line is necessary (tracing::Span is feature-gated in this crate) and is present. Good.

Label cardinality / PII
  • wa_high_retry_total{reason} — uses RetryReason::as_str() which is the same 14-value stable label set already used by wa_retry_receipt_total (wacore/src/protocol/retry.rs:66). Consistent. ✓
  • wa_retry_unknown_device_total{sender_type} — two values: "primary" / "companion". ✓
  • wa_retry_refused_total, wa_base_key_collision_total — no labels. ✓
  • All four label values are &'static str literals. No JID, message id, or user-controlled string leaks. ✓
cfg(feature = "metrics") parity

The no-op branch (wacore/src/telemetry.rs:222-229) adds matching #[inline] stubs for all four new functions with the same arg shapes — so the build is correct in both configurations. The PR description's claim that clippy is clean with --features metrics,tracing and at default is plausible given this is what it would have failed on.

Log-level claims

Both downgrades are well-supported:

  • Refusing retry #N ... exceeds max attemptsdebug! — WA Web emits via WALogger.LOG. The lib's own precedent for the mirror receive-side capped path (src/message/retry.rs:228, the Max retries reached ... Requesting PDO fallback line) is already at debug!. This is consistent with both.
  • Base key collision detected ... Forcing fresh sessioninfo! — WA Web's WAWebUpdateLocalSignalSession logs the sibling delete via WALogger.LOG, and the three other branches of the same routine in this file (Saved base key, Base key changed, Recreating session) all already log at info!. The local convention agrees.

Adding (msg {}) to both the collision and the Base key changed lines is a real improvement: the operation is keyed by (address, message_id) (see delete_base_key(addr_str, message_id) two lines below), so without the id the log line and the DB row weren't correlatable during a storm. Good catch.

What I checked but did not flag

  • wa_high_retry_total could in principle be expressed as wa_retry_receipt_total{at_cap="true"}. Splitting it out matches WA Web's distinct event id and avoids requiring downstream aggregation; fine call.
  • The PR explicitly enumerates what it leaves out (peer regId-mismatch without <keys>, offline-gating). Reasonable scope discipline; agreed those deserve separate decisions.

Recommendation

Approve. No required changes; the consistency nit on high_retry's placement vs. retry_receipt is take-it-or-leave-it.
· branch: feat/retry-warn-levels-and-telemetry

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jun 17, 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: d10b4e4f-d26b-4b61-9695-4290153da309

📥 Commits

Reviewing files that changed from the base of the PR and between e487ea8 and 56e46d3.

📒 Files selected for processing (1)
  • src/message/retry.rs

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added telemetry metrics for retry events, including high-retry attempts, unknown device detection, refused retries, and session key collisions.
    • Enhanced distributed tracing with additional context fields for retry receipt handling operations.

Walkthrough

Four new telemetry counter functions (high_retry, retry_unknown_device, retry_refused, base_key_collision) are added to wacore/src/telemetry.rs with feature-gated implementations and no-op stubs. These are then called from src/retry.rs and src/message/retry.rs alongside log severity adjustments and expanded tracing span fields.

Changes

Retry Telemetry Instrumentation

Layer / File(s) Summary
New telemetry counter functions and no-op stubs
wacore/src/telemetry.rs
Adds high_retry, retry_unknown_device, retry_refused, and base_key_collision emission functions behind the metrics feature, registers counter descriptions in describe(), and provides #[inline] no-op stubs when the feature is disabled.
handle_retry_receipt and update_local_signal_session instrumentation
src/retry.rs
Adds count field to the handle_retry_receipt tracing span and records retry_count into it; downgrades max-attempts refusal log from warn! to debug! and calls retry_refused(); emits retry_unknown_device("primary"/"companion") for unknown-device retries; downgrades base-key collision log from warn! to info! and calls base_key_collision(); adds message id to the session-regenerated log line.
run_retry_receipt high-retry telemetry call
src/message/retry.rs
Calls wacore::telemetry::high_retry(reason) when retry_count >= MAX_DECRYPT_RETRIES, matching the WhatsApp Web capped retry event behavior.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • oxidezap/whatsapp-rust#734: The metrics facade and feature wiring in wacore/src/telemetry.rs that this PR extends was introduced there.
  • oxidezap/whatsapp-rust#802: Both PRs touch the unknown-device path in handle_retry_receipt in src/retry.rs; that PR added the detection logic that this PR now instruments.
  • oxidezap/whatsapp-rust#559: Both PRs modify the retry_count limit and base-key collision branches in src/retry.rs that this PR now instruments with telemetry.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main changes: log-level adjustments for parity with WA Web and new retry-flow observability counters.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining the motivations, implementation details, and testing approach.
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/retry-warn-levels-and-telemetry

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.

@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown

📦 Binary size report

Metric main PR Δ
bin size (stripped) 10.61 MiB 10.61 MiB +64 B (+0.00%) 🔺
bin .text 8.74 MiB 8.74 MiB +64 B (+0.00%) 🔺
bin allocated (text+data+bss) 10.61 MiB 10.61 MiB 0
llvm-lines wacore 645,636 645,636 0
llvm-lines wacore copies 17,671 17,671 0
llvm-lines whatsapp-rust lib 657,120 657,189 +69 (+0.01%) 🔺
llvm-lines whatsapp-rust lib copies 20,008 20,013 +5 (+0.02%) 🔺
deps crates (Cargo.lock) 354 354 0
.text per crate
Crate main PR Δ
.text whatsapp_rust 1.48 MiB 1.48 MiB +64 B (+0.00%) 🔺
.text wacore 544.63 KiB 544.63 KiB 0
.text wacore_binary 158.83 KiB 158.83 KiB 0
.text wacore_libsignal 170.85 KiB 170.85 KiB 0
.text wacore_appstate 35.26 KiB 35.26 KiB 0
.text wacore_noise 30.68 KiB 30.68 KiB 0
.text waproto 895.34 KiB 895.34 KiB 0
.text whatsapp_rust_sqlite_storage 206.21 KiB 206.21 KiB 0
.text whatsapp_rust_tokio_transport 33.09 KiB 33.09 KiB 0
.text whatsapp_rust_ureq_http_client 6.19 KiB 6.19 KiB 0
.text std 1.14 MiB 1.14 MiB 0
.text other deps 4.02 MiB 4.02 MiB 0
Top movers (cargo-bloat attribution)
Crate main PR Δ
rustix 1.41 KiB 131 B -1.28 KiB (-90.93%)
regex_automata 2.88 KiB 4.16 KiB +1.28 KiB (+44.54%)

Baseline: d441e5fa2 (latest main run) · Head: ed3c58a4c · Graphs

@codspeed-hq

codspeed-hq Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 176 untouched benchmarks


Comparing feat/retry-warn-levels-and-telemetry (56e46d3) with main (d441e5f)

Open in CodSpeed

WAWebHandleMsgSendReceipt awaits sendRetryReceipt and only then calls
maybePostMessageHighRetryCountMetric (a sequence expression after the yield),
so WA Web commits the high-retry metric on a sent receipt, not on intent. Move
wa_high_retry_total into the Ok arm of send_retry_receipt, next to the
wa_retry_receipt_total sibling, so a failed send no longer counts and the two
adjacent counters share the same success semantics. Surfaced by the PR review
cross-checking the metric's call site against the oracle.
@jlucaso1
jlucaso1 merged commit 3e704d2 into main Jun 17, 2026
14 checks passed
@jlucaso1
jlucaso1 deleted the feat/retry-warn-levels-and-telemetry branch June 17, 2026 17:57
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