From ba5d17189fe708b8e7badfaa848a1abc4854e4a8 Mon Sep 17 00:00:00 2001 From: thewh1teagle <61390950+thewh1teagle@users.noreply.github.com> Date: Fri, 13 Feb 2026 17:38:54 +0200 Subject: [PATCH] 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 --- Cargo.toml | 2 + permissions/autogenerated/reference.md | 1 - permissions/schemas/schema.json | 10 +++-- src/{client.rs => client/mod.rs} | 60 +++++++++++++------------- src/client/tests.rs | 43 ++++++++++++++++++ src/lib.rs | 6 +-- 6 files changed, 85 insertions(+), 37 deletions(-) rename src/{client.rs => client/mod.rs} (68%) create mode 100644 src/client/tests.rs diff --git a/Cargo.toml b/Cargo.toml index be4338d..bf86f8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,8 @@ os_info = "3.9.2" rand = "0.9.0" log = "0.4.25" sys-locale = "0.3.2" +machine-uid = "0.5.4" +sha2 = "0.10.9" [build-dependencies] tauri-plugin = { version = "2.0.4", features = ["build"] } diff --git a/permissions/autogenerated/reference.md b/permissions/autogenerated/reference.md index 3c0486b..652ec58 100644 --- a/permissions/autogenerated/reference.md +++ b/permissions/autogenerated/reference.md @@ -1,4 +1,3 @@ - ## Permission Table diff --git a/permissions/schemas/schema.json b/permissions/schemas/schema.json index f13b11b..4c0ac2b 100644 --- a/permissions/schemas/schema.json +++ b/permissions/schemas/schema.json @@ -49,7 +49,7 @@ "minimum": 1.0 }, "description": { - "description": "Human-readable description of what the permission does. Tauri convention is to use

headings in markdown content for Tauri documentation generation purposes.", + "description": "Human-readable description of what the permission does. Tauri convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", "type": [ "string", "null" @@ -111,7 +111,7 @@ "type": "string" }, "description": { - "description": "Human-readable description of what the permission does. Tauri internal convention is to use

headings in markdown content for Tauri documentation generation purposes.", + "description": "Human-readable description of what the permission does. Tauri internal convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", "type": [ "string", "null" @@ -297,12 +297,14 @@ { "description": "Enables the track_event command without any pre-configured scope.", "type": "string", - "const": "allow-track-event" + "const": "allow-track-event", + "markdownDescription": "Enables the track_event command without any pre-configured scope." }, { "description": "Denies the track_event command without any pre-configured scope.", "type": "string", - "const": "deny-track-event" + "const": "deny-track-event", + "markdownDescription": "Denies the track_event command without any pre-configured scope." } ] } diff --git a/src/client.rs b/src/client/mod.rs similarity index 68% rename from src/client.rs rename to src/client/mod.rs index da9d905..1136df6 100644 --- a/src/client.rs +++ b/src/client/mod.rs @@ -1,10 +1,7 @@ -use rand::Rng; use serde_json::{json, Value}; -use std::time::{SystemTime, UNIX_EPOCH}; -use std::{ - sync::{Arc, Mutex as SyncMutex}, - time::Duration, -}; +use sha2::{Digest, Sha256}; +use std::sync::{Arc, Mutex as SyncMutex}; +use std::time::Duration; use time::{format_description::well_known::Rfc3339, OffsetDateTime}; use crate::{ @@ -13,34 +10,36 @@ use crate::{ sys::{self, SystemProperties}, }; -static SESSION_TIMEOUT: Duration = Duration::from_secs(4 * 60 * 60); - -fn new_session_id() -> String { - let epoch_in_seconds = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("time went backwards") - .as_secs(); - - let mut rng = rand::rng(); - let random: u64 = rng.random_range(0..=99999999); +/// Computes a deterministic session ID from machine ID, app key, and date string. +pub(crate) fn build_session_id(machine_id: &str, app_key: &str, date: &str) -> String { + let mut hasher = Sha256::new(); + hasher.update(machine_id.as_bytes()); + hasher.update(app_key.as_bytes()); + hasher.update(date.as_bytes()); - let id = epoch_in_seconds * 100_000_000 + random; + format!("{:x}", hasher.finalize()) +} - id.to_string() +/// Creates a deterministic session ID from machine ID, app key, and current UTC date. +/// Same device + same app + same calendar day = same session ID. +pub(crate) fn create_session_id(app_key: &str) -> String { + let machine_id = machine_uid::get().unwrap_or_else(|_| "unknown".to_string()); + let today = OffsetDateTime::now_utc().date().to_string(); // e.g. "2026-02-13" + build_session_id(&machine_id, app_key, &today) } /// A tracking session. #[derive(Debug, Clone)] pub struct TrackingSession { pub id: String, - pub last_touch_ts: OffsetDateTime, + pub date: String, } impl TrackingSession { - fn new() -> Self { + fn new(app_key: &str) -> Self { Self { - id: new_session_id(), - last_touch_ts: OffsetDateTime::now_utc(), + id: create_session_id(app_key), + date: OffsetDateTime::now_utc().date().to_string(), } } } @@ -48,6 +47,7 @@ impl TrackingSession { /// The Aptabase client used to track events. pub struct AptabaseClient { is_enabled: bool, + app_key: String, session: SyncMutex, dispatcher: Arc, app_version: String, @@ -64,8 +64,9 @@ impl AptabaseClient { Self { is_enabled, + app_key: config.app_key.clone(), dispatcher, - session: SyncMutex::new(TrackingSession::new()), + session: SyncMutex::new(TrackingSession::new(&config.app_key)), app_version, sys_info, } @@ -83,15 +84,13 @@ impl AptabaseClient { }); } - /// Returns the current session ID, creating a new one if necessary. + /// Returns the current session ID, rotating to a new one if the UTC date has changed. pub(crate) fn eval_session_id(&self) -> String { let mut session = self.session.lock().expect("could not lock events"); - let now = OffsetDateTime::now_utc(); - if (now - session.last_touch_ts) > SESSION_TIMEOUT { - *session = TrackingSession::new(); - } else { - session.last_touch_ts = now; + let today = OffsetDateTime::now_utc().date().to_string(); + if session.date != today { + *session = TrackingSession::new(&self.app_key); } session.id.clone() @@ -146,3 +145,6 @@ impl AptabaseClient { }); } } + +#[cfg(test)] +mod tests; diff --git a/src/client/tests.rs b/src/client/tests.rs new file mode 100644 index 0000000..df9f9ff --- /dev/null +++ b/src/client/tests.rs @@ -0,0 +1,43 @@ +use super::*; + +#[test] +fn build_session_id_is_deterministic() { + let id1 = build_session_id("machine-123", "A-US-abc", "2026-02-13"); + let id2 = build_session_id("machine-123", "A-US-abc", "2026-02-13"); + assert_eq!(id1, id2); +} + +#[test] +fn build_session_id_changes_with_different_date() { + let id1 = build_session_id("machine-123", "A-US-abc", "2026-02-13"); + let id2 = build_session_id("machine-123", "A-US-abc", "2026-02-14"); + assert_ne!(id1, id2); +} + +#[test] +fn build_session_id_changes_with_different_app_key() { + let id1 = build_session_id("machine-123", "A-US-abc", "2026-02-13"); + let id2 = build_session_id("machine-123", "A-US-xyz", "2026-02-13"); + assert_ne!(id1, id2); +} + +#[test] +fn build_session_id_changes_with_different_machine() { + let id1 = build_session_id("machine-123", "A-US-abc", "2026-02-13"); + let id2 = build_session_id("machine-456", "A-US-abc", "2026-02-13"); + assert_ne!(id1, id2); +} + +#[test] +fn build_session_id_returns_valid_sha256_hex() { + let id = build_session_id("machine-123", "A-US-abc", "2026-02-13"); + assert_eq!(id.len(), 64); + assert!(id.chars().all(|c| c.is_ascii_hexdigit())); +} + +#[test] +fn create_session_id_is_deterministic_across_calls() { + let id1 = create_session_id("A-US-abc"); + let id2 = create_session_id("A-US-abc"); + assert_eq!(id1, id2); +} diff --git a/src/lib.rs b/src/lib.rs index a2b747c..824a794 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,7 @@ mod config; mod dispatcher; mod sys; -use std::{panic::PanicInfo, sync::Arc, time::Duration}; +use std::{panic::PanicHookInfo, sync::Arc, time::Duration}; use client::AptabaseClient; use config::Config; @@ -28,9 +28,9 @@ pub struct Builder { } pub type PanicHook = - Box, String) + 'static + Sync + Send>; + Box, String) + 'static + Sync + Send>; -fn get_panic_message(info: &PanicInfo) -> String { +fn get_panic_message(info: &PanicHookInfo) -> String { let payload = info.payload(); if let Some(s) = payload.downcast_ref::<&str>() { return s.to_string();