From ac8fc59ccc1897a1d7077f3e482d48722a682dbc Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Wed, 25 Feb 2026 20:09:38 +0900 Subject: [PATCH 1/2] refactor: extract AnalyticsRuntime trait from plugin layer Signed-off-by: Yujong Lee --- crates/analytics/Cargo.toml | 2 +- crates/analytics/src/error.rs | 2 + crates/analytics/src/lib.rs | 63 ++++++++++++++ crates/analytics/src/runtime.rs | 8 ++ plugins/analytics/src/ext.rs | 116 +++++-------------------- plugins/analytics/src/lib.rs | 10 ++- plugins/analytics/src/tauri_runtime.rs | 84 ++++++++++++++++++ 7 files changed, 188 insertions(+), 97 deletions(-) create mode 100644 crates/analytics/src/runtime.rs create mode 100644 plugins/analytics/src/tauri_runtime.rs diff --git a/crates/analytics/Cargo.toml b/crates/analytics/Cargo.toml index 9505ff7fde..93d49763af 100644 --- a/crates/analytics/Cargo.toml +++ b/crates/analytics/Cargo.toml @@ -11,7 +11,7 @@ serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } specta = { workspace = true, features = ["derive", "serde_json"] } thiserror = { workspace = true } -tokio = { workspace = true, features = ["sync"] } +tokio = { workspace = true, features = ["sync", "rt"] } tracing = { workspace = true } [dev-dependencies] diff --git a/crates/analytics/src/error.rs b/crates/analytics/src/error.rs index 5528a9cd89..83bb151c31 100644 --- a/crates/analytics/src/error.rs +++ b/crates/analytics/src/error.rs @@ -4,6 +4,8 @@ use serde::{Serialize, ser::Serializer}; pub enum Error { #[error("posthog error: {0}")] PosthogError(String), + #[error("runtime error: {0}")] + Runtime(String), } impl From for Error { diff --git a/crates/analytics/src/lib.rs b/crates/analytics/src/lib.rs index 5f10676ed8..4e1f8c6170 100644 --- a/crates/analytics/src/lib.rs +++ b/crates/analytics/src/lib.rs @@ -4,8 +4,10 @@ use std::time::Duration; mod error; mod outlit; +pub mod runtime; pub use error::*; +pub use runtime::AnalyticsRuntime; use outlit::OutlitClient; use posthog_rs::{ClientOptions, Event}; @@ -394,6 +396,67 @@ impl AnalyticsPayloadBuilder { } } +#[derive(Clone)] +pub struct AnalyticsService { + client: AnalyticsClient, + runtime: Arc, +} + +impl AnalyticsService { + pub fn new(client: AnalyticsClient, runtime: Arc) -> Self { + Self { client, runtime } + } + + pub async fn event(&self, mut payload: AnalyticsPayload) -> Result<(), Error> { + if self.runtime.is_disabled() { + return Ok(()); + } + self.runtime.enrich(&mut payload); + let distinct_id = self.runtime.distinct_id(); + self.client.event(distinct_id, payload).await + } + + pub fn event_fire_and_forget(&self, mut payload: AnalyticsPayload) { + if self.runtime.is_disabled() { + return; + } + self.runtime.enrich(&mut payload); + let distinct_id = self.runtime.distinct_id(); + let client = self.client.clone(); + tokio::spawn(async move { + let _ = client.event(distinct_id, payload).await; + }); + } + + pub async fn set_properties(&self, payload: PropertiesPayload) -> Result<(), Error> { + if self.runtime.is_disabled() { + return Ok(()); + } + let distinct_id = self.runtime.distinct_id(); + self.client.set_properties(distinct_id, payload).await + } + + pub async fn identify( + &self, + user_id: impl Into, + payload: PropertiesPayload, + ) -> Result<(), Error> { + if self.runtime.is_disabled() { + return Ok(()); + } + let distinct_id = self.runtime.distinct_id(); + self.client.identify(user_id, distinct_id, payload).await + } + + pub fn is_disabled(&self) -> bool { + self.runtime.is_disabled() + } + + pub fn set_disabled(&self, disabled: bool) -> Result<(), Error> { + self.runtime.set_disabled(disabled) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/analytics/src/runtime.rs b/crates/analytics/src/runtime.rs new file mode 100644 index 0000000000..ca17af1f50 --- /dev/null +++ b/crates/analytics/src/runtime.rs @@ -0,0 +1,8 @@ +use crate::{AnalyticsPayload, Error}; + +pub trait AnalyticsRuntime: Send + Sync + 'static { + fn enrich(&self, payload: &mut AnalyticsPayload); + fn distinct_id(&self) -> String; + fn is_disabled(&self) -> bool; + fn set_disabled(&self, disabled: bool) -> Result<(), Error>; +} diff --git a/plugins/analytics/src/ext.rs b/plugins/analytics/src/ext.rs index 2f77b2a976..0710380603 100644 --- a/plugins/analytics/src/ext.rs +++ b/plugins/analytics/src/ext.rs @@ -1,109 +1,45 @@ -use tauri_plugin_misc::MiscPluginExt; -use tauri_plugin_store2::Store2PluginExt; - pub struct Analytics<'a, R: tauri::Runtime, M: tauri::Manager> { manager: &'a M, _runtime: std::marker::PhantomData R>, } impl<'a, R: tauri::Runtime, M: tauri::Manager> Analytics<'a, R, M> { + fn service(&self) -> tauri::State<'_, crate::ManagedState> { + self.manager.state::() + } + pub async fn event( &self, - mut payload: hypr_analytics::AnalyticsPayload, + payload: hypr_analytics::AnalyticsPayload, ) -> Result<(), crate::Error> { - Self::enrich_payload(self.manager, &mut payload); - - if self.is_disabled().unwrap_or(true) { - return Ok(()); - } - - let machine_id = hypr_host::fingerprint(); - let client = self.manager.state::(); - client - .event(machine_id, payload) + self.service() + .event(payload) .await - .map_err(crate::Error::HyprAnalytics)?; - - Ok(()) + .map_err(crate::Error::HyprAnalytics) } - pub fn event_fire_and_forget(&self, mut payload: hypr_analytics::AnalyticsPayload) { - Self::enrich_payload(self.manager, &mut payload); - - if self.is_disabled().unwrap_or(true) { - return; - } - - let machine_id = hypr_host::fingerprint(); - let client = self.manager.state::().inner().clone(); - - tauri::async_runtime::spawn(async move { - let _ = client.event(machine_id, payload).await; - }); - } - - fn enrich_payload(manager: &M, payload: &mut hypr_analytics::AnalyticsPayload) { - let app_version = env!("APP_VERSION"); - let app_identifier = manager.config().identifier.clone(); - let git_hash = manager.misc().get_git_hash(); - let bundle_id = manager.config().identifier.clone(); - - payload - .props - .entry("app_version".into()) - .or_insert(app_version.into()); - - payload - .props - .entry("app_identifier".into()) - .or_insert(app_identifier.into()); - - payload - .props - .entry("git_hash".into()) - .or_insert(git_hash.into()); - - payload - .props - .entry("bundle_id".into()) - .or_insert(bundle_id.into()); - - payload.props.entry("$set".into()).or_insert_with(|| { - serde_json::json!({ - "app_version": app_version - }) - }); + pub fn event_fire_and_forget(&self, payload: hypr_analytics::AnalyticsPayload) { + self.service().event_fire_and_forget(payload); } pub fn set_disabled(&self, disabled: bool) -> Result<(), crate::Error> { - { - let store = self.manager.store2().scoped_store(crate::PLUGIN_NAME)?; - store.set(crate::StoreKey::Disabled, disabled)?; - } - Ok(()) + self.service() + .set_disabled(disabled) + .map_err(crate::Error::HyprAnalytics) } pub fn is_disabled(&self) -> Result { - let store = self.manager.store2().scoped_store(crate::PLUGIN_NAME)?; - let v = store.get(crate::StoreKey::Disabled)?.unwrap_or(false); - Ok(v) + Ok(self.service().is_disabled()) } pub async fn set_properties( &self, payload: hypr_analytics::PropertiesPayload, ) -> Result<(), crate::Error> { - if !self.is_disabled()? { - let machine_id = hypr_host::fingerprint(); - - let client = self.manager.state::(); - client - .set_properties(machine_id, payload) - .await - .map_err(crate::Error::HyprAnalytics)?; - } - - Ok(()) + self.service() + .set_properties(payload) + .await + .map_err(crate::Error::HyprAnalytics) } pub async fn identify( @@ -111,18 +47,10 @@ impl<'a, R: tauri::Runtime, M: tauri::Manager> Analytics<'a, R, M> { user_id: impl Into, payload: hypr_analytics::PropertiesPayload, ) -> Result<(), crate::Error> { - if !self.is_disabled()? { - let machine_id = hypr_host::fingerprint(); - let user_id = user_id.into(); - - let client = self.manager.state::(); - client - .identify(user_id, machine_id, payload) - .await - .map_err(crate::Error::HyprAnalytics)?; - } - - Ok(()) + self.service() + .identify(user_id, payload) + .await + .map_err(crate::Error::HyprAnalytics) } } diff --git a/plugins/analytics/src/lib.rs b/plugins/analytics/src/lib.rs index b06c6dbb3f..191aa74d00 100644 --- a/plugins/analytics/src/lib.rs +++ b/plugins/analytics/src/lib.rs @@ -1,9 +1,12 @@ +use std::sync::Arc; + use tauri::Manager; mod commands; mod error; mod ext; mod store; +mod tauri_runtime; pub use error::{Error, Result}; pub use ext::*; @@ -11,7 +14,7 @@ use store::*; pub use hypr_analytics::*; -pub type ManagedState = hypr_analytics::AnalyticsClient; +pub type ManagedState = hypr_analytics::AnalyticsService; const PLUGIN_NAME: &str = "analytics"; @@ -62,7 +65,10 @@ pub fn init() -> tauri::plugin::TauriPlugin { builder.build() }; - assert!(app.manage(client)); + let runtime = Arc::new(tauri_runtime::TauriAnalyticsRuntime::new(app)); + let service = hypr_analytics::AnalyticsService::new(client, runtime); + + assert!(app.manage(service)); Ok(()) }) .build() diff --git a/plugins/analytics/src/tauri_runtime.rs b/plugins/analytics/src/tauri_runtime.rs new file mode 100644 index 0000000000..6be7e99f82 --- /dev/null +++ b/plugins/analytics/src/tauri_runtime.rs @@ -0,0 +1,84 @@ +use hypr_analytics::{AnalyticsPayload, AnalyticsRuntime}; +use tauri_plugin_store2::Store2PluginExt; + +pub struct TauriAnalyticsRuntime { + app_handle: tauri::AppHandle, + app_version: String, + git_hash: String, + bundle_id: String, + app_identifier: String, +} + +impl TauriAnalyticsRuntime { + pub fn new(app_handle: &tauri::AppHandle) -> Self { + use tauri_plugin_misc::MiscPluginExt; + + let app_version = env!("APP_VERSION").to_string(); + let git_hash = app_handle.misc().get_git_hash(); + let bundle_id = app_handle.config().identifier.clone(); + let app_identifier = app_handle.config().identifier.clone(); + + Self { + app_handle: app_handle.clone(), + app_version, + git_hash, + bundle_id, + app_identifier, + } + } +} + +impl AnalyticsRuntime for TauriAnalyticsRuntime { + fn enrich(&self, payload: &mut AnalyticsPayload) { + payload + .props + .entry("app_version".into()) + .or_insert(self.app_version.clone().into()); + + payload + .props + .entry("app_identifier".into()) + .or_insert(self.app_identifier.clone().into()); + + payload + .props + .entry("git_hash".into()) + .or_insert(self.git_hash.clone().into()); + + payload + .props + .entry("bundle_id".into()) + .or_insert(self.bundle_id.clone().into()); + + payload.props.entry("$set".into()).or_insert_with(|| { + serde_json::json!({ + "app_version": self.app_version + }) + }); + } + + fn distinct_id(&self) -> String { + hypr_host::fingerprint() + } + + fn is_disabled(&self) -> bool { + let result: Result = (|| { + let store = self.app_handle.store2().scoped_store(crate::PLUGIN_NAME)?; + let v: bool = store.get(crate::StoreKey::Disabled)?.unwrap_or(false); + Ok(v) + })(); + result.unwrap_or(true) + } + + fn set_disabled(&self, disabled: bool) -> Result<(), hypr_analytics::Error> { + let store = self + .app_handle + .store2() + .scoped_store(crate::PLUGIN_NAME) + .map_err(|e| hypr_analytics::Error::Runtime(e.to_string()))?; + store + .set(crate::StoreKey::Disabled, disabled) + .map_err(|e| hypr_analytics::Error::Runtime(e.to_string()))?; + Ok(()) + } +} From a4909fd350581e8b48f09608ce40e9b15c9ad020 Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Wed, 25 Feb 2026 20:49:40 +0900 Subject: [PATCH 2/2] fix? Signed-off-by: Yujong Lee --- crates/analytics/src/lib.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/analytics/src/lib.rs b/crates/analytics/src/lib.rs index 4e1f8c6170..2dfdedb3a8 100644 --- a/crates/analytics/src/lib.rs +++ b/crates/analytics/src/lib.rs @@ -400,11 +400,16 @@ impl AnalyticsPayloadBuilder { pub struct AnalyticsService { client: AnalyticsClient, runtime: Arc, + tokio_handle: tokio::runtime::Handle, } impl AnalyticsService { pub fn new(client: AnalyticsClient, runtime: Arc) -> Self { - Self { client, runtime } + Self { + client, + runtime, + tokio_handle: tokio::runtime::Handle::current(), + } } pub async fn event(&self, mut payload: AnalyticsPayload) -> Result<(), Error> { @@ -423,7 +428,7 @@ impl AnalyticsService { self.runtime.enrich(&mut payload); let distinct_id = self.runtime.distinct_id(); let client = self.client.clone(); - tokio::spawn(async move { + self.tokio_handle.spawn(async move { let _ = client.event(distinct_id, payload).await; }); }