Compare commits

...
Author SHA1 Message Date
Brandon Thomas 2d497f2cb7 log OS version 2025-09-25 01:01:53 -04:00
11 changed files with 97 additions and 3 deletions
Generated
+12
View File
@@ -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"
@@ -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;
@@ -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;
@@ -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<String>,
/// An override for the version of the OS (e.g. 10.15.7, 11, 22.04).
pub maybe_os_version: Option<String>,
/// 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<String>,
/// 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}`.
@@ -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);
}
@@ -83,13 +83,23 @@ fn get_route(api_host: &ApiHost, route_path: &str, query_params: &HashMap<String
// NB: This is not safe. It doesn't handle URL encoding, "?", "&", etc.
for (key, value) in query_params {
builder.add_param(key, value);
let value = filter_string(value);
builder.add_param(key, &value);
}
// NB: This isn't very safe.
builder.build()
}
// TODO: Please stop using this URL Builder library. It's not very safe or intuitive.
fn filter_string(string: &str) -> String {
string.trim()
.replace(" ", "")
.replace("&", "")
.replace("=", "")
.replace("?", "")
}
#[cfg(test)]
mod tests {
use super::*;
+2 -1
View File
@@ -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"] }
@@ -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)
@@ -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()),
};
@@ -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<u64>,
}
@@ -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,
)
@@ -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,
};