Skip to content

feat: add unified session telemetry - #238

Merged
jlucaso1 merged 1 commit into
mainfrom
feat-unified-session
Jan 24, 2026
Merged

feat: add unified session telemetry#238
jlucaso1 merged 1 commit into
mainfrom
feat-unified-session

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Jan 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add <ib><unified_session id="..."/> stanza support matching WhatsApp Web behavior
  • Server time sync, duplicate prevention, sequence counter (1-based, resets on new session)
  • Type-safe ProtocolNode implementation in wacore/src/ib.rs
  • Runtime manager in src/unified_session.rs

Test plan

  • Unit tests for protocol nodes and manager
  • Integration tests in client.rs
  • Verified against captured WhatsApp Web JS

Summary by CodeRabbit

  • New Features

    • Unified session management added: automatically syncs server time, maintains a session ID and sequence, and transmits session info during login, pairing, and presence availability.
  • Behavioral Changes

    • Session messages are sent automatically at key lifecycle events; duplicate sends are avoided and sequence numbers are tracked.
  • Tests

    • Extensive unit and integration tests covering session ID, time sync, send behavior, and reset semantics.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitai Bot commented Jan 24, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Introduces a unified session subsystem: new UnifiedSessionManager for server-time offset tracking, session ID calculation, deduplicated sends, IB stanza types for unified session, and wiring into Client, presence, and pairing flows with tests.

Changes

Cohort / File(s) Summary
Unified Session Core
src/unified_session.rs
New UnifiedSessionManager: tracks server_time_offset_ms, last_sent_id, sequence; methods update_server_time_offset, calculate_session_id, prepare_send, clear_last_sent, reset; unit/integration tests.
Protocol Definitions (IB)
wacore/src/ib.rs
New UnifiedSession and IbStanza types, calculate_id() logic, ProtocolNode implementations for Node serialization/deserialization, and tests.
Client Integration
src/client.rs
Added pub(crate) unified_session: UnifiedSessionManager; initialized in Client::new; added update_server_time_offset() and async send_unified_session() and integrated calls post-login/post-auth.
Feature Wiring
src/features/presence.rs, src/pair.rs
Presence now calls send_unified_session().await on Availability; pairing flow updates server time offset and spawns background send_unified_session() tasks.
Module Exports
src/lib.rs, wacore/src/lib.rs
Added pub mod unified_session; and pub mod ib; to expose new modules.
Tests & Misc
*/tests*, various new test code
New tests covering ID calculation, time offset parsing, prepare_send duplicate prevention, sequence behavior, and end-to-end integration.

Sequence Diagram

sequenceDiagram
    participant Client
    participant UnifiedSessionManager
    participant Server

    Client->>Server: Login / Pairing requests
    Server-->>Client: Response (node with attr t = server time)

    Client->>UnifiedSessionManager: update_server_time_offset(node)
    UnifiedSessionManager->>UnifiedSessionManager: compute server_time_offset_ms & reset sequence if needed

    Client->>UnifiedSessionManager: prepare_send()
    UnifiedSessionManager->>UnifiedSessionManager: calculate session_id, check last_sent_id, increment sequence
    UnifiedSessionManager-->>Client: (Node unified-session stanza, sequence) or None

    Client->>Server: Send IB UnifiedSession stanza (if produced)
    Server-->>Client: ACK
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • Feat pair code complete #202: Modifies pairing flow and client lifecycle; touches same pairing areas where unified-session update/send calls were added.

Poem

🐰 I hop on bytes where server clocks chime,
I stitch an ID from offset and time,
I guard against echoes, sequences climb,
A tidy send, then back to the rhyme. ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 68.89% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly and concisely summarizes the main feature addition: unified session telemetry support. It accurately reflects the primary change across all modified files.

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

✨ Finishing touches
  • 📝 Generate docstrings

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 force-pushed the feat-unified-session branch from 984e1bd to cd3659e Compare January 24, 2026 04:40
@github-actions

github-actions Bot commented Jan 24, 2026

Copy link
Copy Markdown

🐰 Bencher Report

Branchfeat-unified-session
Testbedubuntu-latest

🚨 1 Alert

BenchmarkMeasure
Units
ViewBenchmark Result
(Result Δ%)
Upper Boundary
(Limit %)
binary_benchmark::unpack_group::bench_unpack_compressedInstructions
instructions x 1e3
📈 plot
🚷 threshold
🚨 alert (🔔)
556.53 x 1e3
(+16.48%)Baseline: 477.80 x 1e3
501.69 x 1e3
(110.93%)

Click to view all benchmark results
BenchmarkInstructionsBenchmark Result
instructions
(Result Δ%)
Upper Boundary
instructions
(Limit %)
binary_benchmark::attr_parser_group::bench_attr_parser attr_lookup:setup_attr_marshaled()📈 view plot
🚷 view threshold
7,031.00
(-5.46%)Baseline: 7,437.24
7,809.10
(90.04%)
binary_benchmark::marshal_group::bench_marshal_allocating📈 view plot
🚷 view threshold
124,647.00
(-14.56%)Baseline: 145,886.65
153,180.98
(81.37%)
binary_benchmark::marshal_group::bench_marshal_reusing_buffer📈 view plot
🚷 view threshold
124,747.00
(-14.55%)Baseline: 145,987.72
153,287.10
(81.38%)
binary_benchmark::roundtrip_group::bench_roundtrip large:setup_large_marshaled()📈 view plot
🚷 view threshold
106,274.00
(0.00%)Baseline: 106,274.00
111,587.70
(95.24%)
binary_benchmark::roundtrip_group::bench_roundtrip small:setup_small_marshaled()📈 view plot
🚷 view threshold
8,487.00
(0.00%)Baseline: 8,487.00
8,911.35
(95.24%)
binary_benchmark::unmarshal_group::bench_unmarshal large:setup_large_marshaled()📈 view plot
🚷 view threshold
49,338.00
(-0.46%)Baseline: 49,567.15
52,045.51
(94.80%)
binary_benchmark::unmarshal_group::bench_unmarshal small:setup_small_marshaled()📈 view plot
🚷 view threshold
3,025.00
(-6.78%)Baseline: 3,245.15
3,407.41
(88.78%)
binary_benchmark::unpack_group::bench_unpack_compressed📈 view plot
🚷 view threshold
🚨 view alert (🔔)
556,531.00
(+16.48%)Baseline: 477,798.37
501,688.29
(110.93%)

binary_benchmark::unpack_group::bench_unpack_uncompressed📈 view plot
🚷 view threshold
779.00
(+0.02%)Baseline: 778.85
817.79
(95.26%)
libsignal_benchmark::conversation_group::bench_full_dm_conversation full:setup_conversation_data()📈 view plot
🚷 view threshold
27,717,223.00
(-0.12%)Baseline: 27,751,677.40
29,139,261.27
(95.12%)
libsignal_benchmark::dm_group::bench_dm_decrypt_first_message decrypt_prekey:setup_dm_with_first_message()📈 view plot
🚷 view threshold
5,568,973.00
(-0.02%)Baseline: 5,570,054.47
5,848,557.19
(95.22%)
libsignal_benchmark::dm_group::bench_dm_encrypt_first_message first_msg:setup_dm_session()📈 view plot
🚷 view threshold
176,934.00
(-0.47%)Baseline: 177,774.33
186,663.05
(94.79%)
libsignal_benchmark::dm_group::bench_dm_encrypt_subsequent_message subsequent:setup_established_dm_session()📈 view plot
🚷 view threshold
177,750.00
(-0.46%)Baseline: 178,577.67
187,506.55
(94.80%)
libsignal_benchmark::dm_group::bench_dm_session_establishment setup:setup_dm_users()📈 view plot
🚷 view threshold
17,295,410.00
(+0.12%)Baseline: 17,274,180.87
18,137,889.91
(95.36%)
libsignal_benchmark::group_messaging_group::bench_group_create_distribution_message create:setup_group_sender()📈 view plot
🚷 view threshold
295,577.00
(-0.02%)Baseline: 295,640.33
310,422.35
(95.22%)
libsignal_benchmark::group_messaging_group::bench_group_decrypt_message decrypt:setup_group_with_encrypted_message()📈 view plot
🚷 view threshold
12,389,494.00
(-1.64%)Baseline: 12,596,009.73
13,225,810.22
(93.68%)
libsignal_benchmark::group_messaging_group::bench_group_encrypt_message encrypt:setup_group_with_distribution()📈 view plot
🚷 view threshold
715,209.00
(-0.12%)Baseline: 716,061.53
751,864.61
(95.12%)
reporting_token_benchmark::content_extraction_group::bench_content_extraction extended:setup_extended_message()📈 view plot
🚷 view threshold
11,819.00
(+0.08%)Baseline: 11,809.93
12,400.43
(95.31%)
reporting_token_benchmark::content_extraction_group::bench_content_extraction simple:setup_simple_message()📈 view plot
🚷 view threshold
3,837.00
(+0.20%)Baseline: 3,829.15
4,020.61
(95.43%)
reporting_token_benchmark::full_generation_group::bench_full_token_generation extended:setup_full_gen_extended()📈 view plot
🚷 view threshold
88,033.00
(+0.04%)Baseline: 88,002.15
92,402.26
(95.27%)
reporting_token_benchmark::full_generation_group::bench_full_token_generation simple:setup_full_gen_simple()📈 view plot
🚷 view threshold
80,074.00
(+0.04%)Baseline: 80,044.37
84,046.59
(95.27%)
reporting_token_benchmark::key_derivation_group::bench_key_derivation📈 view plot
🚷 view threshold
51,035.00
(0.00%)Baseline: 51,035.00
53,586.75
(95.24%)
reporting_token_benchmark::message_encoding_group::bench_message_encoding extended:setup_extended_message()📈 view plot
🚷 view threshold
5,738.00
(+0.16%)Baseline: 5,728.93
6,015.38
(95.39%)
reporting_token_benchmark::message_encoding_group::bench_message_encoding simple:setup_simple_message()📈 view plot
🚷 view threshold
2,121.00
(+0.37%)Baseline: 2,113.15
2,218.81
(95.59%)
reporting_token_benchmark::token_calculation_group::bench_token_calculation📈 view plot
🚷 view threshold
21,921.00
(+0.09%)Baseline: 21,902.09
22,997.19
(95.32%)
🐰 View full continuous benchmarking report in Bencher

@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

🤖 Fix all issues with AI agents
In `@wacore/src/ib.rs`:
- Around line 24-33: The calculate_id function can produce negative IDs because
Rust’s % preserves the sign; replace the `% WEEK_MS` operation with a
non-negative modulo using rem_euclid to ensure the result is in [0, WEEK_MS). In
the calculate_id function, compute id as (adjusted_now +
OFFSET_MS).rem_euclid(WEEK_MS) (keeping DAY_MS, WEEK_MS, OFFSET_MS and
adjusted_now as-is) and return that value as the string so session IDs are
always non-negative.
- Around line 51-60: The try_from_node implementation currently silently
defaults the session id when parsing Node.attrs; replace the fallback logic in
try_from_node with the required_attr helper so missing ids return a clear error:
call required_attr(node, "id")? to obtain the id instead of
node.attrs.get("id").cloned().unwrap_or_default(), keeping the rest of the
function unchanged so it returns Ok(Self { id }) on success.
🧹 Nitpick comments (1)
wacore/src/ib.rs (1)

119-179: Consider adding tests for error paths and IbStanza::try_from_node.

The current tests cover happy paths well. Consider adding:

  • A test for IbStanza::try_from_node (parsing round-trip)
  • Error case tests (wrong tag, missing content)

These would improve confidence in the parsing logic.

Example additional tests
#[test]
fn test_ib_stanza_try_from_node() {
    let stanza = IbStanza::unified_session(UnifiedSession::new("123456789"));
    let node = stanza.into_node();
    let parsed = IbStanza::try_from_node(&node).unwrap();
    
    match parsed.content {
        IbContent::UnifiedSession(us) => assert_eq!(us.id, "123456789"),
    }
}

#[test]
fn test_ib_stanza_wrong_tag_error() {
    let node = NodeBuilder::new("wrong").build();
    assert!(IbStanza::try_from_node(&node).is_err());
}

#[test]
fn test_ib_stanza_missing_content_error() {
    let node = NodeBuilder::new("ib").build();
    assert!(IbStanza::try_from_node(&node).is_err());
}

Comment thread wacore/src/ib.rs
Comment on lines +24 to +33
pub fn calculate_id(server_time_offset_ms: i64) -> String {
const DAY_MS: i64 = 24 * 60 * 60 * 1000;
const WEEK_MS: i64 = 7 * DAY_MS;
const OFFSET_MS: i64 = 3 * DAY_MS;

let now = chrono::Utc::now().timestamp_millis();
let adjusted_now = now + server_time_offset_ms;
let id = (adjusted_now + OFFSET_MS) % WEEK_MS;
id.to_string()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use rem_euclid() to ensure non-negative session IDs.

Rust's % operator preserves the sign of the dividend. With a sufficiently negative server_time_offset_ms, (adjusted_now + OFFSET_MS) could be negative, producing a negative session ID. Use rem_euclid() for a mathematically correct modulo that always returns a value in [0, WEEK_MS).

Proposed fix
     fn calculate_id(server_time_offset_ms: i64) -> String {
         const DAY_MS: i64 = 24 * 60 * 60 * 1000;
         const WEEK_MS: i64 = 7 * DAY_MS;
         const OFFSET_MS: i64 = 3 * DAY_MS;

         let now = chrono::Utc::now().timestamp_millis();
         let adjusted_now = now + server_time_offset_ms;
-        let id = (adjusted_now + OFFSET_MS) % WEEK_MS;
+        let id = (adjusted_now + OFFSET_MS).rem_euclid(WEEK_MS);
         id.to_string()
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub fn calculate_id(server_time_offset_ms: i64) -> String {
const DAY_MS: i64 = 24 * 60 * 60 * 1000;
const WEEK_MS: i64 = 7 * DAY_MS;
const OFFSET_MS: i64 = 3 * DAY_MS;
let now = chrono::Utc::now().timestamp_millis();
let adjusted_now = now + server_time_offset_ms;
let id = (adjusted_now + OFFSET_MS) % WEEK_MS;
id.to_string()
}
pub fn calculate_id(server_time_offset_ms: i64) -> String {
const DAY_MS: i64 = 24 * 60 * 60 * 1000;
const WEEK_MS: i64 = 7 * DAY_MS;
const OFFSET_MS: i64 = 3 * DAY_MS;
let now = chrono::Utc::now().timestamp_millis();
let adjusted_now = now + server_time_offset_ms;
let id = (adjusted_now + OFFSET_MS).rem_euclid(WEEK_MS);
id.to_string()
}
🤖 Prompt for AI Agents
In `@wacore/src/ib.rs` around lines 24 - 33, The calculate_id function can produce
negative IDs because Rust’s % preserves the sign; replace the `% WEEK_MS`
operation with a non-negative modulo using rem_euclid to ensure the result is in
[0, WEEK_MS). In the calculate_id function, compute id as (adjusted_now +
OFFSET_MS).rem_euclid(WEEK_MS) (keeping DAY_MS, WEEK_MS, OFFSET_MS and
adjusted_now as-is) and return that value as the string so session IDs are
always non-negative.

Comment thread wacore/src/ib.rs
Comment on lines +51 to +60
fn try_from_node(node: &Node) -> Result<Self> {
if node.tag != "unified_session" {
return Err(anyhow::anyhow!(
"expected <unified_session>, got <{}>",
node.tag
));
}
let id = node.attrs.get("id").cloned().unwrap_or_default();
Ok(Self { id })
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if required_attr and helper functions exist in wacore/src/iq/node.rs
cat wacore/src/iq/node.rs | head -100

Repository: jlucaso1/whatsapp-rust

Length of output: 3768


🏁 Script executed:

#!/bin/bash
# Search for usage of required_attr, optional_attr in the codebase
echo "=== required_attr usage ==="
rg "required_attr" --type rust -B2 -A2 | head -50
echo ""
echo "=== optional_attr usage ==="
rg "optional_attr" --type rust -B2 -A2 | head -50

Repository: jlucaso1/whatsapp-rust

Length of output: 4737


🏁 Script executed:

#!/bin/bash
# Check the UnifiedSession struct and its full context
cat -n wacore/src/ib.rs | head -70

Repository: jlucaso1/whatsapp-rust

Length of output: 2448


Use required_attr helper for consistent parsing.

The id attribute should be parsed using the required_attr helper from wacore/src/iq/node.rs to comply with coding guidelines and provide proper error handling. Replace line 58 with:

let id = required_attr(node, "id")?;

This ensures clear error messages when the id attribute is missing, rather than silently defaulting to an empty string.

🤖 Prompt for AI Agents
In `@wacore/src/ib.rs` around lines 51 - 60, The try_from_node implementation
currently silently defaults the session id when parsing Node.attrs; replace the
fallback logic in try_from_node with the required_attr helper so missing ids
return a clear error: call required_attr(node, "id")? to obtain the id instead
of node.attrs.get("id").cloned().unwrap_or_default(), keeping the rest of the
function unchanged so it returns Ok(Self { id }) on success.

Comment thread src/unified_session.rs Dismissed
Comment thread src/unified_session.rs Dismissed
@jlucaso1
jlucaso1 merged commit 5ec8fe3 into main Jan 24, 2026
7 of 8 checks passed
@aameliaaa74-sys

Copy link
Copy Markdown

How can I reopen WhatsApp view once

@jlucaso1

Copy link
Copy Markdown
Collaborator Author

@aameliaaa74-sys this is related to this change? I think opening a issue is better

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants