From 8cc4faa3e1db4fb89364c5db28541024b13de65f Mon Sep 17 00:00:00 2001 From: r33drichards <57335981+r33drichards@users.noreply.github.com> Date: Thu, 25 Jun 2026 05:29:13 +0000 Subject: [PATCH] feat(cua-driver): add telemetry session ids --- .../rust/crates/cua-driver/src/telemetry.rs | 34 ++++++++++++++++--- .../Telemetry/TelemetryClient.swift | 8 +++-- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/libs/cua-driver/rust/crates/cua-driver/src/telemetry.rs b/libs/cua-driver/rust/crates/cua-driver/src/telemetry.rs index 174967939..6613e4fba 100644 --- a/libs/cua-driver/rust/crates/cua-driver/src/telemetry.rs +++ b/libs/cua-driver/rust/crates/cua-driver/src/telemetry.rs @@ -24,8 +24,9 @@ //! ## Privacy posture (identical to Swift) //! //! We send: driver version, OS name, OS version, CPU arch, CI-environment -//! flag, and a stable per-install UUID. We do **NOT** send: usernames, -//! file paths, command arguments, tool args, or anything user-typed. +//! flag, a stable per-install UUID (`distinct_id`), and an ephemeral +//! per-process UUID (`session_id`). We do **NOT** send: usernames, file +//! paths, command arguments, tool args, or anything user-typed. use std::path::PathBuf; use std::sync::OnceLock; @@ -162,7 +163,8 @@ where // Build the payload directly (we still bypass the opt-out check here, // see module docs + capture_install rationale) and POST synchronously. let distinct_id = get_or_create_install_id(); - let payload = build_payload(event::INSTALL, None, &distinct_id); + let session_id = get_or_create_session_id(); + let payload = build_payload(event::INSTALL, None, &distinct_id, &session_id); let debug = debug_enabled(); if debug { eprintln!("[telemetry] sending event: {} (sync)", event::INSTALL); @@ -264,6 +266,15 @@ fn get_or_create_install_id() -> String { .clone() } +/// Read the per-process session UUID, creating it on first use and then +/// reusing it for the remainder of the process lifetime. +fn get_or_create_session_id() -> String { + static SESSION_ID_CACHE: OnceLock = OnceLock::new(); + SESSION_ID_CACHE + .get_or_init(|| uuid::Uuid::new_v4().to_string()) + .clone() +} + /// Disk-bound path. Read existing UUID if valid; otherwise generate and persist. /// Separated from the `OnceLock` wrapper so tests can exercise the path logic /// without contaminating process-global state. @@ -311,6 +322,7 @@ pub(crate) fn build_payload( event_name: &str, properties: Option<&serde_json::Value>, distinct_id: &str, + session_id: &str, ) -> serde_json::Value { let version = env!("CARGO_PKG_VERSION"); @@ -327,6 +339,7 @@ pub(crate) fn build_payload( event_properties.insert("os_version".into(), os_version().into()); event_properties.insert("arch".into(), arch().into()); event_properties.insert("is_ci".into(), is_ci().into()); + event_properties.insert("session_id".into(), session_id.into()); event_properties.insert("$lib".into(), "cua-driver-rs".into()); event_properties.insert("$lib_version".into(), version.into()); @@ -352,7 +365,8 @@ fn spawn_capture( return; } let distinct_id = get_or_create_install_id(); - let payload = build_payload(&event_name, properties.as_ref(), &distinct_id); + let session_id = get_or_create_session_id(); + let payload = build_payload(&event_name, properties.as_ref(), &distinct_id, &session_id); let debug = debug_enabled(); let task = move || { @@ -640,6 +654,7 @@ mod tests { "cua_driver_test", Some(&serde_json::json!({"extra_key": "extra_val"})), "test-distinct-id", + "test-session-id", ); // Top-level envelope. assert_eq!(payload["api_key"], POSTHOG_API_KEY); @@ -656,6 +671,7 @@ mod tests { assert_eq!(props["arch"], arch()); assert!(props["is_ci"].is_boolean()); assert!(props["os_version"].is_string()); + assert_eq!(props["session_id"], "test-session-id"); // Caller-provided property survives the merge. assert_eq!(props["extra_key"], "extra_val"); @@ -676,11 +692,21 @@ mod tests { "cua_driver_test", Some(&serde_json::json!({"os": "fake-os", "$lib": "fake-lib"})), "id", + "session", ); assert_eq!(payload["properties"]["os"], std::env::consts::OS); assert_eq!(payload["properties"]["$lib"], "cua-driver-rs"); } + #[test] + fn session_id_is_stable_within_a_process() { + let first = get_or_create_session_id(); + let second = get_or_create_session_id(); + + assert_eq!(first, second, "session id must stay stable within one process"); + assert_eq!(first.len(), 36, "session id must be a UUID"); + } + #[test] fn install_id_persists_across_reads() { let _g = ENV_LOCK.lock().unwrap(); diff --git a/libs/cua-driver/swift/Sources/CuaDriverCore/Telemetry/TelemetryClient.swift b/libs/cua-driver/swift/Sources/CuaDriverCore/Telemetry/TelemetryClient.swift index 4355a15c1..e159b5bae 100644 --- a/libs/cua-driver/swift/Sources/CuaDriverCore/Telemetry/TelemetryClient.swift +++ b/libs/cua-driver/swift/Sources/CuaDriverCore/Telemetry/TelemetryClient.swift @@ -15,8 +15,9 @@ import Foundation /// separate YAML like lume's. /// /// Privacy posture matches lume: we send the driver version, OS, -/// OS version, CPU architecture, CI-environment flag, and a stable -/// per-install UUID. We do **not** send usernames, file paths, +/// OS version, CPU architecture, CI-environment flag, a stable +/// per-install UUID (`distinct_id`), and an ephemeral per-process +/// UUID (`session_id`). We do **not** send usernames, file paths, /// command arguments, or anything user-typed. See `privacy` notes /// in the package for the audit trail. public final class TelemetryClient: @unchecked Sendable { @@ -39,6 +40,7 @@ public final class TelemetryClient: @unchecked Sendable { // MARK: - Properties private var installationId: String? + private let sessionId: String private let urlSession: URLSession // MARK: - Initialization @@ -48,6 +50,7 @@ public final class TelemetryClient: @unchecked Sendable { let config = URLSessionConfiguration.default config.timeoutIntervalForRequest = 10 config.timeoutIntervalForResource = 10 + self.sessionId = UUID().uuidString self.urlSession = URLSession(configuration: config) } @@ -128,6 +131,7 @@ public final class TelemetryClient: @unchecked Sendable { eventProperties["os_version"] = ProcessInfo.processInfo.operatingSystemVersionString eventProperties["arch"] = Self.architecture eventProperties["is_ci"] = Self.isCI + eventProperties["session_id"] = self.sessionId eventProperties["$lib"] = "cua-driver-swift" eventProperties["$lib_version"] = version