Skip to content

feat(observability): optional tracing instrumentation (off by default, OTel-ready) - #733

Merged
jlucaso1 merged 6 commits into
mainfrom
feat/observability-tracing
Jun 6, 2026
Merged

feat(observability): optional tracing instrumentation (off by default, OTel-ready)#733
jlucaso1 merged 6 commits into
mainfrom
feat/observability-tracing

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Jun 6, 2026

Copy link
Copy Markdown
Collaborator

What

Adds an opt-in tracing feature that instruments the library for end-to-end observability across connection, receive/decrypt, send, IQ, app-state, pairing, media, receipts, retry, notifications and session/crypto flows. The goal is that a production error can be mapped to who / where / how / why.

Off by default: with the feature disabled there is no tracing dependency and zero overhead. The library only emits tracing and never installs a subscriber or depends on OpenTelemetry; the application wires that up (see examples/observability.rs). The existing log calls are unchanged and flow into a subscriber via the log to tracing bridge, so you get span-correlated structured logs even before adopting any new span.

~172 spans across 30+ files, grouped under a wa.{conn,recv,send,iq,appstate,pair,media,receipt,retry,pdo,notif,session,bot}.* taxonomy.

Design

Modeled on hyper's optional-tracing pattern and the tracing/OpenTelemetry guidance for libraries.

  • tracing = { optional = true, default-features = false, features = ["attributes"] } in wacore and the main crate; feature tracing = ["dep:tracing", "wacore/tracing"], deliberately not in default. default-features = false keeps the tracing log feature OFF so an app-side LogTracer bridge cannot recurse.
  • Every instrumentation site is #[cfg_attr(feature = "tracing", tracing::instrument(name = "wa.*", level = ..., skip_all, fields(...), err(Debug)))], so it compiles to nothing (and links no dependency) when the feature is off.
  • Spans are debug/trace, except the connection-lifecycle spans (connect/disconnect/reconnect/run/logout) which are info so connection state is visible at the default level. Failures surface at ERROR via err(Debug), and the existing warn/error logs surface through the bridge.
  • The wa.conn.run session-root span records our own LID (pseudonymous) so connection traces are attributable per account in multi-account deployments.

PII (first-class, since the library carries phone numbers)

  • Jid::observe() renders LID / group / broadcast / newsletter / bot JIDs in full (pseudonymous or non-personal, so the same peer/chat correlates across spans) and replaces phone-number users with a pn#<token>. The token is a keyed hash (SipHash with a process-lifetime random key), not a plain digest: an unkeyed hash of an E.164 number is reversible by precomputation, while the random key (kept only in memory) makes exported tokens non-recoverable. Legacy group ids <creator-phone>-<timestamp> get only the numeric prefix redacted.
  • observe_protocol_address() applies the same keyed scheme to Signal ProtocolAddress names (which embed a phone number) used in logs.
  • The library's own log! calls now render JIDs and addresses through these helpers (raw-Jid args wrapped with observe(), address logs with observe_protocol_address()), so the bridged logs carry the same redaction as the span fields.
  • The tracing-pii feature (off by default) renders raw numbers for local debugging only.

Overhead

  • Feature off: no dependency, the #[cfg_attr] attributes vanish, zero cost.
  • Feature on, no subscriber: near-zero (callsite caching).
  • Spans are debug/trace/info, so a downstream binary can statically strip lower levels with release_max_level_info/warn.

Verification

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

Usage

cargo run --example observability --features tracing
RUST_LOG="info,whatsapp_rust=debug" cargo run --example observability --features tracing

Review

Addresses the Codex and CodeRabbit comments: keyed (non-enumerable) phone token; legacy-group creator-phone redaction with an exact no-leak test; agent preserved in redacted output; the log->tracing bridge PII gap (raw JIDs/addresses in legacy logs are now redacted); and the example path/wiring docs.

Out of scope (deliberate follow-ups): a metrics layer (counters/histograms for rates and percentiles) is a separate capability from tracing spans; and the pre-existing modify_device note in prekeys.rs is unrelated to observability.

…, OTel-ready

Adds an opt-in `tracing` feature that instruments the library with spans for end-to-end visibility into connection, receive/decrypt, send, IQ, app-state, pairing, media and session/crypto flows. Off by default: with the feature disabled there is no `tracing` dependency and zero overhead. The library only emits `tracing` and never installs a subscriber or depends on OpenTelemetry; the application wires that up (see examples/observability.rs). The existing ~835 `log` calls are unchanged and flow into a subscriber via the log to tracing bridge.

Design (modeled on hyper's optional-tracing pattern and the tracing/OpenTelemetry library guidance):
- `tracing = { optional = true, default-features = false, features = ["attributes"] }` in wacore and the main crate; feature `tracing = ["dep:tracing", "wacore/tracing"]`, not in `default`. `default-features = false` keeps the tracing `log` feature OFF so an app-side LogTracer bridge cannot recurse.
- All instrumentation is `#[cfg_attr(feature = "tracing", tracing::instrument(name = "wa.*", level = "debug"/"trace", skip_all, fields(...), err(Debug)))]`, so it compiles to nothing (and links no dependency) when the feature is off. ~115 spans across 28 files, grouped under `wa.{conn,recv,send,appstate,pair,media,session}.*` plus `wa.iq`.

PII handling (first-class for a lib that carries phone numbers):
- `Jid::observe()` renders LID / group / broadcast / newsletter / bot JIDs in full (pseudonymous or non-personal, so the same peer/chat correlates across spans) and replaces phone-number users with a stable `pn#<fnv-hash>` token: traceable without leaking the number, allocation-free, and computed only while an enabled span is being recorded. The `tracing-pii` feature (off by default) renders raw numbers for local debugging only.
- Spans carry redacted correlation fields (chat/sender/peer via observe(), msg_id, ns/kind, reason) and `err(Debug)` on Result paths, so a production error maps to who/where/how/why.

examples/observability.rs shows subscriber + log-bridge wiring and OTLP guidance.

Verified: builds and `clippy --all-targets -- -D warnings` are clean both with and without `--features tracing` (and with `tracing-pii`); `cargo fmt --all -- --check` clean; full test suite green with no behavior change.
@coderabbitai

coderabbitai Bot commented Jun 6, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds workspace tracing features and an observability example, implements privacy-aware JID observation/redaction, and applies feature-gated tracing::instrument spans and observe()-based structured logging across client and wacore codepaths.

Changes

Observability instrumentation with privacy-aware JID redaction

Layer / File(s) Summary
Workspace features and example
Cargo.toml, wacore/Cargo.toml, wacore/binary/Cargo.toml, examples/observability.rs
Adds workspace tracing dependency and crate features tracing/tracing-pii, tracing-subscriber dev-dep, and an observability example gated by tracing.
Observed JID redaction contract
wacore/binary/src/jid.rs, wacore/binary/Cargo.toml, wacore/src/types/jid.rs
Introduces Server::carries_phone_number(), ObservedJid/Jid::observe(), observe_token()/observe_str(), observe_protocol_address(), and a unit test validating redaction behavior.
Connection lifecycle and node IO
src/client/lifecycle.rs, src/handshake.rs, src/keepalive.rs, src/client/node_io.rs, src/handlers/*
Adds feature-gated tracing::instrument spans to lifecycle, handshake, keepalive, node I/O, and stanza handlers; updates some logs to use observe().
App-state and history sync
wacore/src/appstate_sync.rs, src/client/app_state.rs, src/history_sync.rs
Instruments app-state sync/build/patch flows and history sync handlers with spans and contextual fields.
Device registry & LID↔PN mapping
src/client/device_registry.rs, src/client/lid_pn.rs
Adds spans to registry updates/patches and LID–PN mapping/migration flows; switches logging to observe() formatting.
Sessions, sender-keys, and prekeys
src/client/sessions.rs, src/client/sender_keys.rs, src/prekeys.rs
Instruments session establishment/checks, sender-key ops, and pre-key fetch/upload/refresh/validate with counts/peer fields and error capture.
Receive pipeline, secret, retry, special handlers
src/message/receive.rs, src/message/msg_secret.rs, src/message/retry.rs, src/message/special.rs
Instruments incoming pipeline, secret decrypt handlers, retry/undecryptable flows, and special message handlers with structured chat/sender/msg_id fields; updates many logs to use observe().
Dispatch and send paths
src/message/dispatch.rs, src/send.rs, src/client/messaging.rs
Instruments dispatch and outgoing send helpers and high-level send flows; uses observe() for recipient/group identifiers in spans/logs.
wacore send and encryption helpers
wacore/src/send/{dm,peer,group,encrypt}.rs
Adds tracing to DM/peer/group stanza preparation and encrypt fan-out (device count) and updates logged JIDs to use observe().
Media upload/download
src/upload.rs, src/download.rs
Adds spans to upload/upload_stream (kind/len) and multiple download variants (file/writer/params) and sticker-pack fetch.
Handlers, presence, bot lifecycle, PDO/usync
src/handlers/*, src/bot.rs, src/pdo.rs, src/usync.rs
Adds tracing to many handlers and notifications, instruments bot run/build, PDO flows, and usync; updates many logs to use observe()/observe_protocol_address().

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

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

@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: 62aa3d8455

ℹ️ 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 wacore/binary/src/jid.rs Outdated
@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() 112,967 112,956 +0.0%
send_receive_benchmark::dm_recv::bench_dm_recv text:setup_dm_recv() 1,656,620 1,656,732 -0.0%
send_receive_benchmark::group_send::bench_group_send group_10:setup_group_send_10() 651,830 651,703 +0.0%
send_receive_benchmark::group_send::bench_group_send group_50:setup_group_send_50() 875,960 875,828 +0.0%
send_receive_benchmark::group_send::bench_group_send group_256:setup_group_send_256() 2,083,781 2,083,560 +0.0%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_10:setup_group_skdm_10() 748,890 749,208 -0.0%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_50:setup_group_skdm_50() 1,329,695 1,326,331 +0.3%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_256:setup_group_skdm_256() 4,373,863 4,376,133 -0.1%
send_receive_benchmark::group_recv::bench_group_recv text:setup_group_recv() 517,557 517,401 +0.0%
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,140,131 4,138,513 +0.0%
libsignal_benchmark::dm_group::bench_dm_encrypt_first_message first_msg:setup_dm_session() 100,131 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,262 210,262 +0.0%
libsignal_benchmark::group_messaging_group::bench_group_encrypt_message encrypt:setup_group_with_distribution() 496,921 496,908 +0.0%
libsignal_benchmark::group_messaging_group::bench_group_decrypt_message decrypt:setup_group_with_encrypted_message() 512,762 508,503 +0.8%
libsignal_benchmark::conversation_group::bench_full_dm_conversation full:setup_conversation_data() 11,978,828 11,979,015 -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,910,012 4,910,162 -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,658 -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.

@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: 3

Caution

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

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

311-313: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Stop mutating Device directly in pre-key upload path

This write bypasses the required command pipeline. Replace modify_device(...) with a DeviceCommand and process_command() so state transitions stay serialized and consistent.

Suggested fix
-        self.persistence_manager
-            .modify_device(|d| d.server_has_prekeys = true)
-            .await;
+        self.persistence_manager
+            .process_command(DeviceCommand::SetServerHasPrekeys(true))
+            .await;

As per coding guidelines: "Never modify Device state directly; always use DeviceCommand + PersistenceManager::process_command() for state mutations and get_device_snapshot() for reading state".

🤖 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/prekeys.rs` around lines 311 - 313, The code is directly mutating Device
via self.persistence_manager.modify_device(|d| d.server_has_prekeys = true),
which violates the rule to only mutate state through DeviceCommand and
PersistenceManager::process_command; replace the modify_device call by
constructing an appropriate DeviceCommand variant (e.g., SetServerHasPrekeys or
similar) that sets server_has_prekeys to true and invoke
self.persistence_manager.process_command(command).await (handling the
Result/error as other command usages do); when reading state elsewhere use
get_device_snapshot() instead of direct mutation/reads.

Source: Coding guidelines

🤖 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 `@Cargo.toml`:
- Line 102: Update the stale example path in the feature documentation inside
Cargo.toml: replace the reference string "examples/otel.rs" with the new example
path "examples/observability.rs" so feature docs correctly point to the
observability example (search for the literal "examples/otel.rs" in Cargo.toml
to locate the doc text to edit).

In `@wacore/binary/src/jid.rs`:
- Around line 875-881: The current Display code in jid.rs uses an unsalted
FNV-1a over jid.user (the loop over jid.user.as_bytes() and write!(f,
"pn#{hash:08x")) which is reversible; replace this with a keyed
pseudonymization: compute an HMAC-SHA256 (or keyed BLAKE3) over
jid.user.as_bytes() using the service secret key (load from config/env),
truncate the hex output to a fixed length (e.g., 8-16 hex chars) and write that
as the pseudonym (keep the "pn#" prefix), and ensure the secret key is obtained
from your existing config loader or init path rather than hardcoding so the
token is not deterministic across deployments without the key.
- Around line 866-885: The redacted branch in fmt::Display for ObservedJid
currently writes "pn#{hash}[:device]`@server`" but omits jid.agent, which
collapses distinct identities for Server::Interop/Server::Messenger; update the
redacted formatting to include the agent portion exactly as the non-redacted
path does (i.e., after optional :device write the agent component when present)
so ObservedJid::fmt preserves .agent in the redacted output for
Server::Interop/Server::Messenger.

---

Outside diff comments:
In `@src/prekeys.rs`:
- Around line 311-313: The code is directly mutating Device via
self.persistence_manager.modify_device(|d| d.server_has_prekeys = true), which
violates the rule to only mutate state through DeviceCommand and
PersistenceManager::process_command; replace the modify_device call by
constructing an appropriate DeviceCommand variant (e.g., SetServerHasPrekeys or
similar) that sets server_has_prekeys to true and invoke
self.persistence_manager.process_command(command).await (handling the
Result/error as other command usages do); when reading state elsewhere use
get_device_snapshot() instead of direct mutation/reads.
🪄 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: a38c848b-dd09-4270-b05f-aea7feee8bbd

📥 Commits

Reviewing files that changed from the base of the PR and between a4f8b45 and 62aa3d8.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (33)
  • Cargo.toml
  • examples/observability.rs
  • src/client/app_state.rs
  • src/client/device_registry.rs
  • src/client/lid_pn.rs
  • src/client/lifecycle.rs
  • src/client/messaging.rs
  • src/client/node_io.rs
  • src/client/sender_keys.rs
  • src/client/sessions.rs
  • src/download.rs
  • src/handshake.rs
  • src/history_sync.rs
  • src/keepalive.rs
  • src/message/dispatch.rs
  • src/message/msg_secret.rs
  • src/message/receive.rs
  • src/message/retry.rs
  • src/message/special.rs
  • src/pair.rs
  • src/pair_code.rs
  • src/prekeys.rs
  • src/request.rs
  • src/send.rs
  • src/upload.rs
  • wacore/Cargo.toml
  • wacore/binary/Cargo.toml
  • wacore/binary/src/jid.rs
  • wacore/src/appstate_sync.rs
  • wacore/src/send/dm.rs
  • wacore/src/send/encrypt.rs
  • wacore/src/send/group.rs
  • wacore/src/send/peer.rs

Comment thread Cargo.toml Outdated
Comment thread wacore/binary/src/jid.rs
Comment thread wacore/binary/src/jid.rs Outdated
…eview)

Addresses the PR #733 reviews (Codex P1 + CodeRabbit).

Jid::observe(): replace the unsalted FNV-1a phone token with a keyed hash (SipHash via a process-lifetime random key). An unkeyed digest of an E.164 number is reversible by precomputation, so it would still leak recoverable numbers in exported traces; the random key lives only in process memory, making tokens non-recoverable while staying stable within a run for correlation (a fresh key each restart is an intentional privacy tradeoff).

Preserve `.agent` in the redacted output where the agent is part of the identity (Interop/Messenger), so distinct IDs do not collapse into one token.

Fix the stale `examples/otel.rs` reference in the Cargo.toml feature docs; the example is `examples/observability.rs`.

Add a unit test asserting observe() never leaks the raw number, preserves device, and renders LID/group JIDs in full.

Not changed: CodeRabbit's prekeys.rs `modify_device` note is pre-existing code this PR does not touch (it only added a span there); it is a separate concern.

@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: 0d8088ab93

ℹ️ 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 wacore/binary/src/jid.rs
Legacy group/broadcast ids have the form "<creator-phone>-<timestamp>", so rendering them in full leaked the creator's phone number in the new span fields. Jid::observe() now redacts the numeric prefix as `pn#<keyed-token>-<timestamp>` for Server::Group/Broadcast while keeping the timestamp (not PII) and group-level correlation. Modern opaque group ids (no numeric "<digits>-" prefix) still render in full. The keyed-token helper is shared with the phone-number path. Extends the unit test with the legacy-group case.

@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: dc83a33162

ℹ️ 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 examples/observability.rs
//
// Every `wa.*` span is then exported as an OTLP span with its fields
// (chat/peer/msg_id are already privacy-redacted via `Jid::observe()`).
.init();

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 Avoid bridging raw PII logs into traces

When an app copies this OTLP example with tracing enabled, .init() installs the default tracing-log bridge, so the existing log! messages are exported alongside the new redacted span fields. Those legacy logs still format JIDs with normal Display rather than Jid::observe() (for example src/client/sessions.rs:247/:256 logs jid directly), so a phone-number JID can be emitted as raw PII even though the surrounding wa.* fields are redacted. Disable the log bridge for production tracing or scrub the existing log messages before recommending this wiring.

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: 1

🤖 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 `@wacore/binary/src/jid.rs`:
- Around line 982-990: The test currently only checks the redacted ID's shape
but can still leak the creator number; update the test around
Jid::from_str("123456789-1620000000@g.us") / legacy.observe().to_string()
(variables legacy and ls) to assert explicitly that the original numeric prefix
("123456789") does not appear in ls (and/or in legacy.observe().to_string()),
i.e., add a no-leak assertion that the raw creator phone substring is not
contained in the observed/redacted string in addition to the existing shape and
inequality checks.
🪄 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: 55f45582-f4a7-4847-bd8c-aeea0bb9b2ed

📥 Commits

Reviewing files that changed from the base of the PR and between 0d8088a and dc83a33.

📒 Files selected for processing (1)
  • wacore/binary/src/jid.rs

Comment thread wacore/binary/src/jid.rs Outdated
… (review pass)

Second observability pass on PR #733: resolves the remaining gaps from the self-review and the Codex/CodeRabbit comments.

Coverage: instrument the areas the first pass missed - retry/pdo, receipts and the incoming-stanza handlers (notification/receipt/message/presence/call/iq/ib/chatstate), the Bot API, usync, mediaconn, and assorted wacore leaf logic. ~57 new spans (172 total) under the wa.* taxonomy.

PII in logs: the existing log!() calls rendered JIDs - and Signal ProtocolAddress names, which embed phone numbers - with raw Display, which would leak through the log->tracing bridge. Wrapped 107 raw-Jid log arguments with Jid::observe(), and added wacore::types::jid::observe_protocol_address (a keyed token reusing the now-public observe_token) to redact the 8 ProtocolAddress log sites. The example documents the bridge/PII tradeoff.

Levels: promoted the connection-lifecycle spans (connect/disconnect/reconnect/run/logout) to info so connection state is visible at the default level; failures already surface via err(Debug) at ERROR and via the existing warn/error logs through the bridge.

Account attribution: the wa.conn.run session-root span now records our own LID (pseudonymous) so connection traces are attributable per account in multi-account deployments.

Hardening (CodeRabbit): the legacy-group redaction test now asserts the exact no-leak invariant (the raw creator number must not appear) plus the fixed-width keyed-token shape.

Verified: cargo clippy --all-targets -- -D warnings clean with and without --features tracing; fmt clean; cargo test --workspace --exclude e2e-tests green (1992 passed, 0 failed).

@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: 20a0fdbb5c

ℹ️ 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/send.rs Outdated
/// For LID mode, uses `group_info.phone_jid_for_lid_user` to query devices
/// via PN when available (LID usync is unreliable for own JID), then
/// converts the result back to LID. Same fallback as `prepare_group_stanza`.
#[cfg_attr(feature = "tracing", tracing::instrument(name = "wa.send.resolve_skdm_targets", level = "debug", skip_all, fields(group = %group_jid)))]

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 Redact group_jid before recording the SKDM span

When the tracing feature is enabled and this path handles a legacy group JID like <creator-phone>-<timestamp>@g.us, this span records the raw group_jid string and bypasses the Jid::observe() redaction that now handles those IDs. Fresh evidence for the existing PII concern is that this newly added field uses %group_jid directly, so group sends through resolve_skdm_targets can still export the creator phone number in traces.

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: 2

Caution

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

⚠️ Outside diff range comments (2)
src/pair.rs (1)

279-281: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Replace direct device mutation with DeviceCommand flow.

This path mutates persisted device state via modify_device, which breaks the repository rule requiring command-based mutations. Route this through a DeviceCommand (e.g., a dedicated SetServerHasPrekeys(false) command) and process_command() so state transitions stay consistent.

As per coding guidelines: "Never modify Device state directly; always use DeviceCommand + PersistenceManager::process_command() for state mutations and get_device_snapshot() for reading state."

🤖 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/pair.rs` around lines 279 - 281, The code is directly mutating device
state via persistence_manager.modify_device; instead, create and dispatch a
DeviceCommand (e.g., a new enum variant like SetServerHasPrekeys(false) or
existing equivalent) and call persistence_manager.process_command(command).
Replace the modify_device(...) call with construction of the proper
DeviceCommand and await persistence_manager.process_command(...) to perform the
state change, and use persistence_manager.get_device_snapshot(...) when you need
to read the device state elsewhere.

Source: Coding guidelines

src/handlers/basic.rs (1)

86-105: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

AckHandler has no instrumentation while peer handlers do.

The three handlers above are instrumented but AckHandler is not. If acks are too frequent to trace without noise, document that decision. Otherwise, add instrumentation for consistency.

🤖 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/handlers/basic.rs` around lines 86 - 105, AckHandler's handle method
lacks the tracing/instrumentation used by the other peer handlers; update the
AckHandler::handle implementation to add the same instrumentation (e.g., the
tracing::instrument or tracing::debug_span usage) around the function body so
the call to client.handle_ack_response(node.get()).await is traced, or if you
intentionally want to suppress noisy ack traces, add a short comment/docstring
on AckHandler explaining that instrumentation was omitted for noise reasons.
Ensure you modify the handle method on AckHandler (and keep the tag() method
unchanged) so the span name/fields follow the same convention used by the other
handlers.
🤖 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 `@examples/observability.rs`:
- Around line 27-30: Update the explanatory sentence that currently suggests
dropping `whatsapp_rust`/`wacore` log targets — instead instruct users to drop
the calling application's log targets (the modules that actually emit raw
JIDs/phone numbers) because suppressing only the library bridge doesn't prevent
leaks from application code; replace the phrase "drop the
`whatsapp_rust`/`wacore` `log` targets" with wording that points to the caller's
targets (e.g., "drop your application's or caller module log targets that emit
raw JIDs") and keep the surrounding guidance about using `Jid::observe()` and
the `tracing-pii` feature intact.

In `@wacore/src/types/jid.rs`:
- Around line 136-140: The helper observe_protocol_address always hashes
addr.name() via wacore_binary::jid::observe_token, violating the workspace
contract to show raw phone numbers when the tracing-pii feature is enabled;
update observe_protocol_address to return the raw addr.name() (as a String) when
tracing-pii is enabled (e.g., cfg!(feature = "tracing-pii") or the appropriate
tracing-pii runtime check) and only call
wacore_binary::jid::observe_token(addr.name()) and format the "addr#..." hashed
form when tracing-pii is not enabled, keeping the function signature and using
the ProtocolAddress::name() accessor.

---

Outside diff comments:
In `@src/handlers/basic.rs`:
- Around line 86-105: AckHandler's handle method lacks the
tracing/instrumentation used by the other peer handlers; update the
AckHandler::handle implementation to add the same instrumentation (e.g., the
tracing::instrument or tracing::debug_span usage) around the function body so
the call to client.handle_ack_response(node.get()).await is traced, or if you
intentionally want to suppress noisy ack traces, add a short comment/docstring
on AckHandler explaining that instrumentation was omitted for noise reasons.
Ensure you modify the handle method on AckHandler (and keep the tag() method
unchanged) so the span name/fields follow the same convention used by the other
handlers.

In `@src/pair.rs`:
- Around line 279-281: The code is directly mutating device state via
persistence_manager.modify_device; instead, create and dispatch a DeviceCommand
(e.g., a new enum variant like SetServerHasPrekeys(false) or existing
equivalent) and call persistence_manager.process_command(command). Replace the
modify_device(...) call with construction of the proper DeviceCommand and await
persistence_manager.process_command(...) to perform the state change, and use
persistence_manager.get_device_snapshot(...) when you need to read the device
state elsewhere.
🪄 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: bfc65640-913d-4e44-b65e-81645a7cfd3b

📥 Commits

Reviewing files that changed from the base of the PR and between dc83a33 and 20a0fdb.

📒 Files selected for processing (36)
  • examples/observability.rs
  • src/bot.rs
  • src/client/app_state.rs
  • src/client/device_registry.rs
  • src/client/lid_pn.rs
  • src/client/lifecycle.rs
  • src/client/messaging.rs
  • src/client/node_io.rs
  • src/client/offline_resume.rs
  • src/client/sender_keys.rs
  • src/client/sessions.rs
  • src/handlers/basic.rs
  • src/handlers/call.rs
  • src/handlers/chatstate.rs
  • src/handlers/ib.rs
  • src/handlers/iq.rs
  • src/handlers/message.rs
  • src/handlers/notification.rs
  • src/handlers/presence.rs
  • src/mediaconn.rs
  • src/message/msg_secret.rs
  • src/message/receive.rs
  • src/message/retry.rs
  • src/message/special.rs
  • src/pair.rs
  • src/pdo.rs
  • src/prekeys.rs
  • src/receipt.rs
  • src/retry.rs
  • src/send.rs
  • src/usync.rs
  • wacore/binary/src/jid.rs
  • wacore/src/send/encrypt.rs
  • wacore/src/send/group.rs
  • wacore/src/types/jid.rs
  • wacore/src/usync.rs

Comment thread examples/observability.rs Outdated
Comment thread wacore/src/types/jid.rs
…t group_jid (review)

Addresses the latest Codex/CodeRabbit comments on PR #733.

- observe_protocol_address now honors the tracing-pii feature (renders the raw address name when enabled), matching Jid::observe(); the feature is forwarded wacore -> wacore-binary. (CodeRabbit)
- The wa.send.resolve_skdm_targets span took a raw group_jid: &str and would leak a legacy group's creator phone; it now goes through a new wacore_binary::jid::observe_str helper (parse + Jid::observe, keyed-token fallback, honors tracing-pii). (Codex P2)
- examples/observability.rs: clarify that dropping the library log targets only silences the library bridge; the application's own raw-JID logs are a separate leak path the app must scrub. (CodeRabbit)
- Raise recursion_limit to 512 in lib.rs and main.rs: instrumenting large async fns (process_sync_task) overflows the default depth limit when the tracing and tracing-pii paths combine.

Verified: clippy --all-targets -- -D warnings clean for default, --features tracing, and --features "tracing,tracing-pii"; fmt clean; observe tests pass.

@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: 02a18383b7

ℹ️ 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/usync.rs
Error values themselves embedded raw JIDs (e.g. usync's "<device-list> not found for user {jid}"), so err(Debug) on a span - and the existing log::error!("{e}") - would export a raw phone number even with redacted span fields. The two error-message sites that interpolate a JID (wacore/src/usync.rs and src/client/sessions.rs) now use Jid::observe(). A sweep of anyhow!/bail!/format!/.context/thiserror confirmed these were the only two (no thiserror enum embeds a raw JID).
@jlucaso1
jlucaso1 merged commit 3c3da27 into main Jun 6, 2026
10 checks passed
@jlucaso1
jlucaso1 deleted the feat/observability-tracing branch June 6, 2026 16:56
@mintlify

mintlify Bot commented Jun 6, 2026

Copy link
Copy Markdown

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

Added an Observability page, two feature-flag rows, and a changelog entry covering the optional tracing instrumentation feature.

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