Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
1 change: 0 additions & 1 deletion permissions/autogenerated/reference.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

## Permission Table

<table>
Expand Down
10 changes: 6 additions & 4 deletions permissions/schemas/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"minimum": 1.0
},
"description": {
"description": "Human-readable description of what the permission does. Tauri convention is to use <h4> headings in markdown content for Tauri documentation generation purposes.",
"description": "Human-readable description of what the permission does. Tauri convention is to use `<h4>` headings in markdown content for Tauri documentation generation purposes.",
"type": [
"string",
"null"
Expand Down Expand Up @@ -111,7 +111,7 @@
"type": "string"
},
"description": {
"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.",
"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.",
"type": [
"string",
"null"
Expand Down Expand Up @@ -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."
}
]
}
Expand Down
60 changes: 31 additions & 29 deletions src/client.rs → src/client/mod.rs
Original file line number Diff line number Diff line change
@@ -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::{
Expand All @@ -13,41 +10,44 @@ 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(),
}
}
}

/// The Aptabase client used to track events.
pub struct AptabaseClient {
is_enabled: bool,
app_key: String,
session: SyncMutex<TrackingSession>,
dispatcher: Arc<EventDispatcher>,
app_version: String,
Expand All @@ -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,
}
Expand All @@ -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()
Expand Down Expand Up @@ -146,3 +145,6 @@ impl AptabaseClient {
});
}
}

#[cfg(test)]
mod tests;
43 changes: 43 additions & 0 deletions src/client/tests.rs
Original file line number Diff line number Diff line change
@@ -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);
}
6 changes: 3 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -28,9 +28,9 @@ pub struct Builder {
}

pub type PanicHook =
Box<dyn Fn(&AptabaseClient, &PanicInfo<'_>, String) + 'static + Sync + Send>;
Box<dyn Fn(&AptabaseClient, &PanicHookInfo<'_>, 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();
Expand Down