Skip to content

Commit ba5d171

Browse files
committed
feat: deterministic daily session ID using sha256(machine_id + app_key + date)
Replace the 4-hour sliding inactivity timeout with a deterministic session ID derived from the machine ID, app key, and UTC date. Same device + same app + same calendar day always produces the same session ID, enabling accurate DAU counting server-side. - Add machine-uid and sha2 dependencies - Extract tests into separate file (src/client/tests.rs) - Fix deprecated PanicInfo -> PanicHookInfo
1 parent e896cce commit ba5d171

File tree

6 files changed

+85
-37
lines changed

6 files changed

+85
-37
lines changed

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ os_info = "3.9.2"
2323
rand = "0.9.0"
2424
log = "0.4.25"
2525
sys-locale = "0.3.2"
26+
machine-uid = "0.5.4"
27+
sha2 = "0.10.9"
2628

2729
[build-dependencies]
2830
tauri-plugin = { version = "2.0.4", features = ["build"] }

permissions/autogenerated/reference.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
## Permission Table
32

43
<table>

permissions/schemas/schema.json

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"minimum": 1.0
5050
},
5151
"description": {
52-
"description": "Human-readable description of what the permission does. Tauri convention is to use <h4> headings in markdown content for Tauri documentation generation purposes.",
52+
"description": "Human-readable description of what the permission does. Tauri convention is to use `<h4>` headings in markdown content for Tauri documentation generation purposes.",
5353
"type": [
5454
"string",
5555
"null"
@@ -111,7 +111,7 @@
111111
"type": "string"
112112
},
113113
"description": {
114-
"description": "Human-readable description of what the permission does. Tauri internal convention is to use <h4> headings in markdown content for Tauri documentation generation purposes.",
114+
"description": "Human-readable description of what the permission does. Tauri internal convention is to use `<h4>` headings in markdown content for Tauri documentation generation purposes.",
115115
"type": [
116116
"string",
117117
"null"
@@ -297,12 +297,14 @@
297297
{
298298
"description": "Enables the track_event command without any pre-configured scope.",
299299
"type": "string",
300-
"const": "allow-track-event"
300+
"const": "allow-track-event",
301+
"markdownDescription": "Enables the track_event command without any pre-configured scope."
301302
},
302303
{
303304
"description": "Denies the track_event command without any pre-configured scope.",
304305
"type": "string",
305-
"const": "deny-track-event"
306+
"const": "deny-track-event",
307+
"markdownDescription": "Denies the track_event command without any pre-configured scope."
306308
}
307309
]
308310
}

src/client.rs renamed to src/client/mod.rs

Lines changed: 31 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1-
use rand::Rng;
21
use serde_json::{json, Value};
3-
use std::time::{SystemTime, UNIX_EPOCH};
4-
use std::{
5-
sync::{Arc, Mutex as SyncMutex},
6-
time::Duration,
7-
};
2+
use sha2::{Digest, Sha256};
3+
use std::sync::{Arc, Mutex as SyncMutex};
4+
use std::time::Duration;
85
use time::{format_description::well_known::Rfc3339, OffsetDateTime};
96

107
use crate::{
@@ -13,41 +10,44 @@ use crate::{
1310
sys::{self, SystemProperties},
1411
};
1512

16-
static SESSION_TIMEOUT: Duration = Duration::from_secs(4 * 60 * 60);
17-
18-
fn new_session_id() -> String {
19-
let epoch_in_seconds = SystemTime::now()
20-
.duration_since(UNIX_EPOCH)
21-
.expect("time went backwards")
22-
.as_secs();
23-
24-
let mut rng = rand::rng();
25-
let random: u64 = rng.random_range(0..=99999999);
13+
/// Computes a deterministic session ID from machine ID, app key, and date string.
14+
pub(crate) fn build_session_id(machine_id: &str, app_key: &str, date: &str) -> String {
15+
let mut hasher = Sha256::new();
16+
hasher.update(machine_id.as_bytes());
17+
hasher.update(app_key.as_bytes());
18+
hasher.update(date.as_bytes());
2619

27-
let id = epoch_in_seconds * 100_000_000 + random;
20+
format!("{:x}", hasher.finalize())
21+
}
2822

29-
id.to_string()
23+
/// Creates a deterministic session ID from machine ID, app key, and current UTC date.
24+
/// Same device + same app + same calendar day = same session ID.
25+
pub(crate) fn create_session_id(app_key: &str) -> String {
26+
let machine_id = machine_uid::get().unwrap_or_else(|_| "unknown".to_string());
27+
let today = OffsetDateTime::now_utc().date().to_string(); // e.g. "2026-02-13"
28+
build_session_id(&machine_id, app_key, &today)
3029
}
3130

3231
/// A tracking session.
3332
#[derive(Debug, Clone)]
3433
pub struct TrackingSession {
3534
pub id: String,
36-
pub last_touch_ts: OffsetDateTime,
35+
pub date: String,
3736
}
3837

3938
impl TrackingSession {
40-
fn new() -> Self {
39+
fn new(app_key: &str) -> Self {
4140
Self {
42-
id: new_session_id(),
43-
last_touch_ts: OffsetDateTime::now_utc(),
41+
id: create_session_id(app_key),
42+
date: OffsetDateTime::now_utc().date().to_string(),
4443
}
4544
}
4645
}
4746

4847
/// The Aptabase client used to track events.
4948
pub struct AptabaseClient {
5049
is_enabled: bool,
50+
app_key: String,
5151
session: SyncMutex<TrackingSession>,
5252
dispatcher: Arc<EventDispatcher>,
5353
app_version: String,
@@ -64,8 +64,9 @@ impl AptabaseClient {
6464

6565
Self {
6666
is_enabled,
67+
app_key: config.app_key.clone(),
6768
dispatcher,
68-
session: SyncMutex::new(TrackingSession::new()),
69+
session: SyncMutex::new(TrackingSession::new(&config.app_key)),
6970
app_version,
7071
sys_info,
7172
}
@@ -83,15 +84,13 @@ impl AptabaseClient {
8384
});
8485
}
8586

86-
/// Returns the current session ID, creating a new one if necessary.
87+
/// Returns the current session ID, rotating to a new one if the UTC date has changed.
8788
pub(crate) fn eval_session_id(&self) -> String {
8889
let mut session = self.session.lock().expect("could not lock events");
8990

90-
let now = OffsetDateTime::now_utc();
91-
if (now - session.last_touch_ts) > SESSION_TIMEOUT {
92-
*session = TrackingSession::new();
93-
} else {
94-
session.last_touch_ts = now;
91+
let today = OffsetDateTime::now_utc().date().to_string();
92+
if session.date != today {
93+
*session = TrackingSession::new(&self.app_key);
9594
}
9695

9796
session.id.clone()
@@ -146,3 +145,6 @@ impl AptabaseClient {
146145
});
147146
}
148147
}
148+
149+
#[cfg(test)]
150+
mod tests;

src/client/tests.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
use super::*;
2+
3+
#[test]
4+
fn build_session_id_is_deterministic() {
5+
let id1 = build_session_id("machine-123", "A-US-abc", "2026-02-13");
6+
let id2 = build_session_id("machine-123", "A-US-abc", "2026-02-13");
7+
assert_eq!(id1, id2);
8+
}
9+
10+
#[test]
11+
fn build_session_id_changes_with_different_date() {
12+
let id1 = build_session_id("machine-123", "A-US-abc", "2026-02-13");
13+
let id2 = build_session_id("machine-123", "A-US-abc", "2026-02-14");
14+
assert_ne!(id1, id2);
15+
}
16+
17+
#[test]
18+
fn build_session_id_changes_with_different_app_key() {
19+
let id1 = build_session_id("machine-123", "A-US-abc", "2026-02-13");
20+
let id2 = build_session_id("machine-123", "A-US-xyz", "2026-02-13");
21+
assert_ne!(id1, id2);
22+
}
23+
24+
#[test]
25+
fn build_session_id_changes_with_different_machine() {
26+
let id1 = build_session_id("machine-123", "A-US-abc", "2026-02-13");
27+
let id2 = build_session_id("machine-456", "A-US-abc", "2026-02-13");
28+
assert_ne!(id1, id2);
29+
}
30+
31+
#[test]
32+
fn build_session_id_returns_valid_sha256_hex() {
33+
let id = build_session_id("machine-123", "A-US-abc", "2026-02-13");
34+
assert_eq!(id.len(), 64);
35+
assert!(id.chars().all(|c| c.is_ascii_hexdigit()));
36+
}
37+
38+
#[test]
39+
fn create_session_id_is_deterministic_across_calls() {
40+
let id1 = create_session_id("A-US-abc");
41+
let id2 = create_session_id("A-US-abc");
42+
assert_eq!(id1, id2);
43+
}

src/lib.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ mod config;
44
mod dispatcher;
55
mod sys;
66

7-
use std::{panic::PanicInfo, sync::Arc, time::Duration};
7+
use std::{panic::PanicHookInfo, sync::Arc, time::Duration};
88

99
use client::AptabaseClient;
1010
use config::Config;
@@ -28,9 +28,9 @@ pub struct Builder {
2828
}
2929

3030
pub type PanicHook =
31-
Box<dyn Fn(&AptabaseClient, &PanicInfo<'_>, String) + 'static + Sync + Send>;
31+
Box<dyn Fn(&AptabaseClient, &PanicHookInfo<'_>, String) + 'static + Sync + Send>;
3232

33-
fn get_panic_message(info: &PanicInfo) -> String {
33+
fn get_panic_message(info: &PanicHookInfo) -> String {
3434
let payload = info.payload();
3535
if let Some(s) = payload.downcast_ref::<&str>() {
3636
return s.to_string();

0 commit comments

Comments
 (0)