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();