From 2d497f2cb78824452fd0c1aa4a6bde03ade565da Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Thu, 25 Sep 2025 01:01:53 -0400 Subject: [PATCH] log OS version --- Cargo.lock | 12 ++++++++++++ .../up.sql | 11 +++++++++++ _sql/migrations_squashed/analytics_totals.sql | 11 +++++++++++ .../src/analytics/log_active_user.rs | 8 +++++++- .../src/analytics/log_active_user.rs | 8 ++++++++ .../src/utils/basic_query_string_post_request.rs | 12 +++++++++++- crates/desktop/artcraft/Cargo.toml | 3 ++- .../desktop/artcraft/src/core/state/os_platform.rs | 9 +++++++++ .../threads/storyteller_activity_thread.rs | 14 ++++++++++++++ .../upsert_analytics_app_active_user.rs | 10 ++++++++++ .../analytics/log_app_active_user_handler.rs | 2 ++ 11 files changed, 97 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d19a448cff..4dad77c15c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -679,6 +679,7 @@ dependencies = [ "mimetypes", "once_cell", "openai_sora_client", + "os_info", "primitives", "rand 0.9.1", "reqwest 0.12.22", @@ -7610,6 +7611,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "os_info" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0e1ac5fde8d43c34139135df8ea9ee9465394b2d8d20f032d38998f64afffc3" +dependencies = [ + "log", + "plist", + "windows-sys 0.52.0", +] + [[package]] name = "overload" version = "0.1.1" diff --git a/_sql/migrations/2025-09-24-055943_analytics_app_active_users/up.sql b/_sql/migrations/2025-09-24-055943_analytics_app_active_users/up.sql index 85daa6f2fe..dd75f4fd17 100644 --- a/_sql/migrations/2025-09-24-055943_analytics_app_active_users/up.sql +++ b/_sql/migrations/2025-09-24-055943_analytics_app_active_users/up.sql @@ -17,6 +17,14 @@ CREATE TABLE analytics_app_active_users ( -- The "user agent" or app version string of the client. app_version VARCHAR(255) DEFAULT NULL, + -- Updated on every write. + -- The OS platform for the client. + os_platform VARCHAR(16) DEFAULT NULL, + + -- Updated on every write. + -- The OS platform version for the client. + os_version VARCHAR(64) DEFAULT NULL, + -- Updated on every write. -- How long the current user's session has been open session_duration_seconds INT(10) UNSIGNED DEFAULT NULL, @@ -41,6 +49,9 @@ CREATE TABLE analytics_app_active_users ( -- ========== INDICES ========== PRIMARY KEY (id), UNIQUE KEY (app_namespace, user_token), -- For now + INDEX idx_os_platform (os_platform), + INDEX idx_os_version (os_version), + INDEX idx_app_version (app_version), INDEX idx_last_active_at (last_active_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; diff --git a/_sql/migrations_squashed/analytics_totals.sql b/_sql/migrations_squashed/analytics_totals.sql index 85daa6f2fe..52b8de1b01 100644 --- a/_sql/migrations_squashed/analytics_totals.sql +++ b/_sql/migrations_squashed/analytics_totals.sql @@ -17,6 +17,14 @@ CREATE TABLE analytics_app_active_users ( -- The "user agent" or app version string of the client. app_version VARCHAR(255) DEFAULT NULL, + -- Updated on every write. + -- The OS platform for the client. + os_platform VARCHAR(16) DEFAULT NULL, + + -- Updated on every write. + -- The OS platform version for the client. + os_version VARCHAR(64) DEFAULT NULL, + -- Updated on every write. -- How long the current user's session has been open session_duration_seconds INT(10) UNSIGNED DEFAULT NULL, @@ -41,6 +49,9 @@ CREATE TABLE analytics_app_active_users ( -- ========== INDICES ========== PRIMARY KEY (id), UNIQUE KEY (app_namespace, user_token), -- For now + INDEX idx_app_version (app_version), + INDEX idx_os_platform (os_platform), + INDEX idx_os_version (os_version), INDEX idx_last_active_at (last_active_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; diff --git a/crates/api_clients/artcraft_api_defs/src/analytics/log_active_user.rs b/crates/api_clients/artcraft_api_defs/src/analytics/log_active_user.rs index efa9a40187..6fc0e4e7c8 100644 --- a/crates/api_clients/artcraft_api_defs/src/analytics/log_active_user.rs +++ b/crates/api_clients/artcraft_api_defs/src/analytics/log_active_user.rs @@ -5,11 +5,17 @@ pub const LOG_ACTIVE_USER_PATH: &str = "/v1/analytics/active_user"; #[derive(Serialize, Deserialize, ToSchema, Debug)] pub struct LogAppActiveUserRequest { + /// An override for the application platform/OS (windows, mac, linux). + pub maybe_os_platform: Option, + + /// An override for the version of the OS (e.g. 10.15.7, 11, 22.04). + pub maybe_os_version: Option, + /// An override for the application name. /// If set together with `maybe_app_version`, the two will be /// concatenated as `{maybe_app_name}/{maybe_app_version}`. pub maybe_app_name: Option, - + /// An override for the application version. /// If set together with `maybe_app_name`, the two will be /// concatenated as `{maybe_app_name}/{maybe_app_version}`. diff --git a/crates/api_clients/storyteller_client/src/analytics/log_active_user.rs b/crates/api_clients/storyteller_client/src/analytics/log_active_user.rs index 159656f6db..29f5165689 100644 --- a/crates/api_clients/storyteller_client/src/analytics/log_active_user.rs +++ b/crates/api_clients/storyteller_client/src/analytics/log_active_user.rs @@ -15,6 +15,14 @@ pub async fn log_active_user( let mut query = HashMap::new(); + if let Some(os_platform) = request.maybe_os_platform { + query.insert("maybe_os_platform".to_string(), os_platform); + } + + if let Some(os_version) = request.maybe_os_version { + query.insert("maybe_os_version".to_string(), os_version); + } + if let Some(app_name) = request.maybe_app_name { query.insert("maybe_app_name".to_string(), app_name); } diff --git a/crates/api_clients/storyteller_client/src/utils/basic_query_string_post_request.rs b/crates/api_clients/storyteller_client/src/utils/basic_query_string_post_request.rs index 1c25fb1d46..1c99351ff3 100644 --- a/crates/api_clients/storyteller_client/src/utils/basic_query_string_post_request.rs +++ b/crates/api_clients/storyteller_client/src/utils/basic_query_string_post_request.rs @@ -83,13 +83,23 @@ fn get_route(api_host: &ApiHost, route_path: &str, query_params: &HashMap String { + string.trim() + .replace(" ", "") + .replace("&", "") + .replace("=", "") + .replace("?", "") +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/desktop/artcraft/Cargo.toml b/crates/desktop/artcraft/Cargo.toml index 3c182f9d20..57385c8acb 100644 --- a/crates/desktop/artcraft/Cargo.toml +++ b/crates/desktop/artcraft/Cargo.toml @@ -57,15 +57,16 @@ image = "0.25.5" indexmap.workspace = true log.workspace = true once_cell.workspace = true +os_info = { version = "3.12.0", default-features = false } rand.workspace = true reqwest = { workspace = true, features = ["multipart", "stream"], default-features = false } # NB(bt,2025-03-10): default-features = false on Windows while I debug openssl / rustls serde.workspace = true serde_derive.workspace = true serde_json.workspace = true -tauri.workspace = true tauri-plugin-dialog.workspace = true tauri-plugin-log.workspace = true tauri-plugin-upload.workspace = true +tauri.workspace = true tempdir.workspace = true tempfile.workspace = true tokio = { version = "1.42", features = ["rt", "rt-multi-thread", "fs"] } diff --git a/crates/desktop/artcraft/src/core/state/os_platform.rs b/crates/desktop/artcraft/src/core/state/os_platform.rs index 0f8ca02107..1d4e933ae4 100644 --- a/crates/desktop/artcraft/src/core/state/os_platform.rs +++ b/crates/desktop/artcraft/src/core/state/os_platform.rs @@ -17,6 +17,15 @@ impl OsPlatform { _ => None, } } + + pub fn maybe_get_str() -> Option<&'static str> { + match Self::maybe_get() { + Some(Self::Linux) => Some("linux"), + Some(Self::Windows) => Some("windows"), + Some(Self::MacOs) => Some("macos"), // NB: "apple" can become "macos". + None => None, + } + } pub fn get() -> Self { Self::maybe_get().unwrap_or(Self::Linux) diff --git a/crates/desktop/artcraft/src/services/storyteller/threads/storyteller_activity_thread.rs b/crates/desktop/artcraft/src/services/storyteller/threads/storyteller_activity_thread.rs index dc6527a4f0..fcb7e13cb7 100644 --- a/crates/desktop/artcraft/src/services/storyteller/threads/storyteller_activity_thread.rs +++ b/crates/desktop/artcraft/src/services/storyteller/threads/storyteller_activity_thread.rs @@ -2,6 +2,7 @@ use crate::core::events::basic_sendable_event_trait::BasicSendableEvent; use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs; use crate::core::state::data_dir::app_data_root::AppDataRoot; use crate::core::state::data_dir::trait_data_subdir::DataSubdir; +use crate::core::state::os_platform::OsPlatform; use crate::core::state::task_database::TaskDatabase; use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager; use anyhow::anyhow; @@ -17,6 +18,7 @@ use reqwest::Url; use sqlite_tasks::queries::list_tasks_by_provider_and_tokens::{list_tasks_by_provider_and_tokens, ListTasksArgs, Task}; use sqlite_tasks::queries::update_task_status::{update_task_status, UpdateTaskArgs}; use std::time::Instant; +use os_info::Info; use storyteller_client::analytics::log_active_user::log_active_user; use storyteller_client::credentials::storyteller_credential_set::StorytellerCredentialSet; use storyteller_client::error::api_error::ApiError; @@ -36,11 +38,13 @@ pub async fn storyteller_activity_thread( storyteller_creds_manager: StorytellerCredentialManager, ) -> ! { let startup = Instant::now(); + let os_info = os_info::get(); loop { let res = polling_loop( &app_env_configs, &storyteller_creds_manager, startup, + &os_info, ).await; if let Err(err) = res { error!("An error occurred: {:?}", err); @@ -54,6 +58,7 @@ async fn polling_loop( app_env_configs: &AppEnvConfigs, storyteller_creds_manager: &StorytellerCredentialManager, startup: Instant, + os_info: &Info, ) -> AnyhowResult<()> { loop { let creds = storyteller_creds_manager.get_credentials()?; @@ -74,9 +79,18 @@ async fn polling_loop( let time_since_startup = Instant::now().duration_since(startup); + let maybe_os_platform = OsPlatform::maybe_get_str() + .map(|s| s.to_string()); + + let maybe_os_version = Some(os_info.version().to_string()) + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()); + let request = LogAppActiveUserRequest { maybe_app_name: Some(CLIENT_NAME.to_string()), maybe_app_version: Some(CLIENT_VERSION.to_string()), + maybe_os_platform, + maybe_os_version, maybe_session_duration_seconds: Some(time_since_startup.as_secs()), }; diff --git a/crates/schema/database/mysql_queries/src/queries/analytics_active_users/upsert_analytics_app_active_user.rs b/crates/schema/database/mysql_queries/src/queries/analytics_active_users/upsert_analytics_app_active_user.rs index 61d1a41a77..e1ac0a6218 100644 --- a/crates/schema/database/mysql_queries/src/queries/analytics_active_users/upsert_analytics_app_active_user.rs +++ b/crates/schema/database/mysql_queries/src/queries/analytics_active_users/upsert_analytics_app_active_user.rs @@ -13,6 +13,8 @@ pub struct UpsertAnalyticsAppActiveUser<'a> { pub user_token: &'a UserToken, pub ip_address: &'a str, pub app_version: Option<&'a str>, + pub os_platform: Option<&'a str>, + pub os_version: Option<&'a str>, pub session_duration_seconds: Option, } @@ -25,6 +27,8 @@ SET app_namespace = ?, user_token = ?, app_version = ?, + os_platform = ?, + os_version = ?, session_duration_seconds = ?, ip_address = ?, measurement_count = measurement_count + 1, @@ -32,6 +36,8 @@ SET last_active_at = NOW() ON DUPLICATE KEY UPDATE app_version = ?, + os_platform = ?, + os_version = ?, session_duration_seconds = ?, ip_address = ?, measurement_count = measurement_count + 1, @@ -41,10 +47,14 @@ ON DUPLICATE KEY UPDATE self.namespace.to_str(), self.user_token.as_str(), self.app_version, + self.os_platform, + self.os_version, self.session_duration_seconds, self.ip_address, // Update case self.app_version, + self.os_platform, + self.os_version, self.session_duration_seconds, self.ip_address, ) diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/analytics/log_app_active_user_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/analytics/log_app_active_user_handler.rs index eb8fe28c20..e52d2a0be7 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/analytics/log_app_active_user_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/analytics/log_app_active_user_handler.rs @@ -94,6 +94,8 @@ pub async fn log_app_active_user_handler( user_token: &user_token, ip_address: &ip_address, app_version: app_version.as_deref(), + os_platform: request.maybe_os_platform.as_deref(), + os_version: request.maybe_os_version.as_deref(), session_duration_seconds: request.maybe_session_duration_seconds, };