diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f0645d8ec..6e017ecda 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,6 +64,7 @@ jobs: AIRDROP_BASE_URL: http://localhost:4000 AIRDROP_API_BASE_URL: http://localhost:3004 AIRDROP_TWITTER_AUTH_URL: http://localhost:3004/auth/twitter + TELEMETRY_API_URL: http://localhost:3004 run: | cargo install cargo-lints cargo lints clippy --all-targets --all-features diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d38173e7d..bd6812f0a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -59,6 +59,7 @@ jobs: AIRDROP_BASE_URL: ${{ secrets.BETA_AIRDROP_BASE_URL }} AIRDROP_API_BASE_URL: ${{ secrets.BETA_AIRDROP_API_BASE_URL }} AIRDROP_TWITTER_AUTH_URL: ${{ secrets.BETA_AIRDROP_TWITTER_AUTH_URL }} + TELEMETRY_API_URL: ${{ secrets.BETA_TELEMETRY_API_URL }} AIRDROP_WEBSOCKET_CRYPTO_KEY: ${{ secrets.DEV_AIRDROP_WEBSOCKET_CRYPTO_KEY }} shell: bash run: | @@ -68,6 +69,7 @@ jobs: echo "AIRDROP_BASE_URL=${{ env.AIRDROP_BASE_URL }}" >> $GITHUB_ENV echo "AIRDROP_API_BASE_URL=${{ env.AIRDROP_API_BASE_URL }}" >> $GITHUB_ENV echo "AIRDROP_TWITTER_AUTH_URL=${{ env.AIRDROP_TWITTER_AUTH_URL }}" >> $GITHUB_ENV + echo "TELEMETRY_API_URL=${{ env.TELEMETRY_API_URL }}" >> $GITHUB_ENV echo "AIRDROP_WEBSOCKET_CRYPTO_KEY=${{ env.AIRDROP_WEBSOCKET_CRYPTO_KEY }}" >> $GITHUB_ENV echo "TS_FEATURES=release-ci-beta, airdrop-env" >> $GITHUB_ENV # numeric-only and cannot be greater than 65535 for msi target diff --git a/package-lock.json b/package-lock.json index 0d5a94600..8b945af52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "tari-universe", - "version": "0.8.38", + "version": "0.8.41", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tari-universe", - "version": "0.8.38", + "version": "0.8.41", "dependencies": { "@floating-ui/react": "^0.26.28", "@lottiefiles/dotlottie-react": "^0.10.1", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 2b455044e..adcb4d524 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -112,6 +112,7 @@ sys-locale = "0.3.1" [features] airdrop-env = [] +telemetry-env = [] airdrop-local = [] custom-protocol = [ "tauri/custom-protocol", diff --git a/src-tauri/src/app_in_memory_config.rs b/src-tauri/src/app_in_memory_config.rs index 9a1072d83..2fab200f2 100644 --- a/src-tauri/src/app_in_memory_config.rs +++ b/src-tauri/src/app_in_memory_config.rs @@ -39,6 +39,9 @@ const AIRDROP_TWITTER_AUTH_URL: &str = std::env!( "AIRDROP_TWITTER_AUTH_URL", "AIRDROP_TWITTER_AUTH_URL env var not defined" ); +#[cfg(feature = "telemetry-env")] +const TELEMETRY_API_URL: &str = + std::env!("TELEMETRY_API_URL", "TELEMETRY_API_URL env var not defined"); #[derive(Debug, Serialize, Deserialize, Clone)] pub struct AppInMemoryConfig { @@ -46,6 +49,7 @@ pub struct AppInMemoryConfig { pub airdrop_api_url: String, pub airdrop_twitter_auth_url: String, pub airdrop_access_token: Option, + pub telemetry_api_url: String, } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -73,6 +77,7 @@ impl Default for AppInMemoryConfig { airdrop_api_url: "https://ut.tari.com".into(), airdrop_twitter_auth_url: "https://airdrop.tari.com/auth".into(), airdrop_access_token: None, + telemetry_api_url: "https://ut.tari.com/push".into(), } } } @@ -118,6 +123,7 @@ impl AppInMemoryConfig { airdrop_api_url: AIRDROP_API_BASE_URL.into(), airdrop_twitter_auth_url: AIRDROP_TWITTER_AUTH_URL.into(), airdrop_access_token: None, + telemetry_api_url: TELEMETRY_API_URL.into(), }; #[cfg(feature = "airdrop-local")] @@ -126,9 +132,14 @@ impl AppInMemoryConfig { airdrop_api_url: "http://localhost:3004".into(), airdrop_twitter_auth_url: "http://localhost:3004/auth/twitter".into(), airdrop_access_token: None, + telemetry_api_url: "http://localhost:3004".into(), }; - #[cfg(not(any(feature = "airdrop-local", feature = "airdrop-env")))] + #[cfg(not(any( + feature = "airdrop-local", + feature = "airdrop-env", + feature = "telemetry-env" + )))] AppInMemoryConfig::default() } } diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 586bf330f..3a96c9627 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -47,6 +47,7 @@ use log::{debug, error, info, warn}; use monero_address_creator::Seed as MoneroSeed; use regex::Regex; use serde::{Deserialize, Serialize}; +use serde_json::Value; use std::fmt::Debug; use std::fs::{read_dir, remove_dir_all, remove_file, File}; use std::sync::atomic::Ordering; @@ -988,6 +989,23 @@ pub async fn set_allow_telemetry( Ok(()) } +#[tauri::command] +pub async fn send_data_telemetry_service( + state: tauri::State<'_, UniverseAppState>, + event_name: String, + data: Value, +) -> Result<(), String> { + state + .telemetry_service + .read() + .await + .send(event_name, data) + .await + .inspect_err(|e| error!(target: LOG_TARGET, "error at send_data_telemetry_service {:?}", e)) + .map_err(|e| e.to_string())?; + Ok(()) +} + #[tauri::command] pub async fn set_application_language( state: tauri::State<'_, UniverseAppState>, diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 1f2cda715..a26f27e21 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -29,9 +29,11 @@ use hardware::hardware_status_monitor::HardwareStatusMonitor; use log::{debug, error, info, warn}; use node_adapter::BaseNodeStatus; use p2pool::models::Connections; +use serde_json::json; use std::fs::{remove_dir_all, remove_file}; use std::path::Path; use tauri_plugin_cli::CliExt; +use telemetry_service::TelemetryService; use tokio::sync::watch::{self}; use updates_manager::UpdatesManager; use wallet_adapter::WalletBalance; @@ -113,6 +115,7 @@ mod process_watcher; mod progress_tracker; mod setup_status_event; mod telemetry_manager; +mod telemetry_service; mod tests; mod tor_adapter; mod tor_manager; @@ -236,6 +239,16 @@ async fn setup_inner( let last_binaries_update_timestamp = state.config.read().await.last_binaries_update_timestamp(); let now = SystemTime::now(); + let _unused = state + .gpu_miner + .write() + .await + .detect(config_dir.clone()) + .await + .inspect_err(|e| error!(target: LOG_TARGET, "Could not detect gpu miner: {:?}", e)); + + HardwareStatusMonitor::current().initialize().await?; + state .telemetry_manager .write() @@ -243,6 +256,26 @@ async fn setup_inner( .initialize(state.airdrop_access_token.clone(), window.clone()) .await?; + let mut telemetry_id = state + .telemetry_manager + .read() + .await + .get_unique_string() + .await; + if telemetry_id.is_empty() { + telemetry_id = "unknown_miner_tari_universe".to_string(); + } + + let app_version = app.package_info().version.clone(); + state + .telemetry_service + .write() + .await + .init(app_version.to_string(), telemetry_id.clone()) + .await?; + let telemetry_service = state.telemetry_service.clone(); + let telemetry_service = &telemetry_service.read().await; + let mut binary_resolver = BinaryResolver::current().write().await; let should_check_for_update = now .duration_since(last_binaries_update_timestamp) @@ -250,6 +283,15 @@ async fn setup_inner( > Duration::from_secs(60 * 60 * 6); if use_tor && !cfg!(target_os = "macos") { + telemetry_service + .send( + "checking-latest-version-tor".to_string(), + json!({ + "service": "tor_manager", + "percentage": 0, + }), + ) + .await?; progress.set_max(5).await; progress .update("checking-latest-version-tor".to_string(), None, 0) @@ -265,6 +307,17 @@ async fn setup_inner( sleep(Duration::from_secs(1)); } + drop( + telemetry_service + .send( + "checking-latest-version-node".to_string(), + json!({ + "service": "node_manager", + "percentage": 5, + }), + ) + .await, + ); progress.set_max(10).await; progress .update("checking-latest-version-node".to_string(), None, 0) @@ -279,6 +332,17 @@ async fn setup_inner( .await?; sleep(Duration::from_secs(1)); + drop( + telemetry_service + .send( + "checking-latest-version-mmproxy".to_string(), + json!({ + "service": "mmproxy", + "percentage": 10, + }), + ) + .await, + ); progress.set_max(15).await; progress .update("checking-latest-version-mmproxy".to_string(), None, 0) @@ -293,6 +357,17 @@ async fn setup_inner( .await?; sleep(Duration::from_secs(1)); + drop( + telemetry_service + .send( + "checking-latest-version-wallet".to_string(), + json!({ + "service": "wallet", + "percentage": 15, + }), + ) + .await, + ); progress.set_max(20).await; progress .update("checking-latest-version-wallet".to_string(), None, 0) @@ -307,6 +382,17 @@ async fn setup_inner( .await?; sleep(Duration::from_secs(1)); + drop( + telemetry_service + .send( + "checking-latest-version-gpuminer".to_string(), + json!({ + "service": "gpuminer", + "percentage":20, + }), + ) + .await, + ); progress.set_max(25).await; progress .update("checking-latest-version-gpuminer".to_string(), None, 0) @@ -321,6 +407,17 @@ async fn setup_inner( .await?; sleep(Duration::from_secs(1)); + drop( + telemetry_service + .send( + "checking-latest-version-xmrig".to_string(), + json!({ + "service": "xmrig", + "percentage":25, + }), + ) + .await, + ); progress.set_max(30).await; progress .update("checking-latest-version-xmrig".to_string(), None, 0) @@ -335,6 +432,17 @@ async fn setup_inner( .await?; sleep(Duration::from_secs(1)); + drop( + telemetry_service + .send( + "checking-latest-version-sha-p2pool".to_string(), + json!({ + "service": "sha_p2pool", + "percentage":30, + }), + ) + .await, + ); progress.set_max(35).await; progress .update("checking-latest-version-sha-p2pool".to_string(), None, 0) @@ -361,16 +469,6 @@ async fn setup_inner( //drop binary resolver to release the lock drop(binary_resolver); - let _unused = state - .gpu_miner - .write() - .await - .detect(config_dir.clone()) - .await - .inspect_err(|e| error!(target: LOG_TARGET, "Could not detect gpu miner: {:?}", e)); - - HardwareStatusMonitor::current().initialize().await?; - let mut tor_control_port = None; if use_tor && !cfg!(target_os = "macos") { state @@ -423,6 +521,17 @@ async fn setup_inner( } info!(target: LOG_TARGET, "Node has started and is ready"); + drop( + telemetry_service + .send( + "waiting-for-wallet".to_string(), + json!({ + "service": "wallet", + "percentage":35, + }), + ) + .await, + ); progress.set_max(40).await; progress .update("waiting-for-wallet".to_string(), None, 0) @@ -443,9 +552,31 @@ async fn setup_inner( .update("waiting-for-node".to_string(), None, 0) .await; progress.set_max(75).await; + drop( + telemetry_service + .send( + "preparing-for-initial-sync".to_string(), + json!({ + "service": "initial_sync", + "percentage":45, + }), + ) + .await, + ); state.node_manager.wait_synced(progress.clone()).await?; if state.config.read().await.p2pool_enabled() { + drop( + telemetry_service + .send( + "starting-p2pool".to_string(), + json!({ + "service": "starting_p2pool", + "percentage":75, + }), + ) + .await, + ); progress.set_max(85).await; progress .update("starting-p2pool".to_string(), None, 0) @@ -469,6 +600,17 @@ async fn setup_inner( .await?; } + drop( + telemetry_service + .send( + "starting-mmproxy".to_string(), + json!({ + "service": "starting_mmproxy", + "percentage":85, + }), + ) + .await, + ); progress.set_max(100).await; progress .update("starting-mmproxy".to_string(), None, 0) @@ -476,16 +618,6 @@ async fn setup_inner( let base_node_grpc_port = state.node_manager.get_grpc_port().await?; - let mut telemetry_id = state - .telemetry_manager - .read() - .await - .get_unique_string() - .await; - if telemetry_id.is_empty() { - telemetry_id = "unknown_miner_tari_universe".to_string(); - } - let config = state.config.read().await; let p2pool_port = state.p2pool_manager.grpc_port().await; mm_proxy_manager @@ -505,6 +637,17 @@ async fn setup_inner( .await?; mm_proxy_manager.wait_ready().await?; *state.is_setup_finished.write().await = true; + drop( + telemetry_service + .send( + "setup-finished".to_string(), + json!({ + "service": "setup_finished", + "percentage":100, + }), + ) + .await, + ); drop( app.clone() .emit( @@ -594,6 +737,7 @@ struct UniverseAppState { node_manager: NodeManager, wallet_manager: WalletManager, telemetry_manager: Arc>, + telemetry_service: Arc>, feedback: Arc>, airdrop_access_token: Arc>>, p2pool_manager: P2poolManager, @@ -668,7 +812,11 @@ fn main() { gpu_status_rx.clone(), base_node_watch_rx.clone(), ); - + let telemetry_service = TelemetryService::new( + app_config_raw.anon_id().to_string(), + app_config.clone(), + app_in_memory_config.clone(), + ); let updates_manager = UpdatesManager::new(app_config.clone(), shutdown.to_signal()); let feedback = Feedback::new(app_in_memory_config.clone(), app_config.clone()); @@ -696,6 +844,7 @@ fn main() { wallet_manager, p2pool_manager, telemetry_manager: Arc::new(RwLock::new(telemetry_manager)), + telemetry_service: Arc::new(RwLock::new(telemetry_service)), feedback: Arc::new(RwLock::new(feedback)), airdrop_access_token: Arc::new(RwLock::new(None)), tor_manager: TorManager::new(), @@ -902,6 +1051,7 @@ fn main() { commands::send_feedback, commands::set_airdrop_access_token, commands::set_allow_telemetry, + commands::send_data_telemetry_service, commands::set_application_language, commands::set_auto_update, commands::set_cpu_mining_enabled, diff --git a/src-tauri/src/process_utils.rs b/src-tauri/src/process_utils.rs index 48b94159d..1be861473 100644 --- a/src-tauri/src/process_utils.rs +++ b/src-tauri/src/process_utils.rs @@ -20,7 +20,7 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use std::path::Path; +use std::{future::Future, path::Path, pin::Pin, time::Duration}; pub fn launch_child_process( file_path: &Path, @@ -84,3 +84,38 @@ pub fn launch_child_process( // Ok(output.stdout.as_slice().to_vec()) // } // } + +pub async fn retry_with_backoff( + mut f: T, + increment_in_secs: u64, + max_retries: u64, + operation_name: &str, +) -> anyhow::Result +where + T: FnMut() -> Pin> + Send>>, + E: std::error::Error, +{ + let range_size = increment_in_secs * max_retries + 1; + + for i in (0..range_size).step_by(usize::try_from(increment_in_secs)?) { + tokio::time::sleep(Duration::from_secs(i)).await; + + let result = f().await; + match result { + Ok(res) => return Ok(res), + Err(e) => { + if i == range_size - 1 { + return Err(anyhow::anyhow!( + "Max retries reached, {} failed. Last error: {:?}", + operation_name, + e + )); + } + } + } + } + Err(anyhow::anyhow!( + "Max retries reached, {} failed without capturing error", + operation_name + )) +} diff --git a/src-tauri/src/telemetry_manager.rs b/src-tauri/src/telemetry_manager.rs index 0639fd919..8ba22aa07 100644 --- a/src-tauri/src/telemetry_manager.rs +++ b/src-tauri/src/telemetry_manager.rs @@ -25,6 +25,7 @@ use crate::gpu_miner_adapter::GpuMinerStatus; use crate::hardware::hardware_status_monitor::HardwareStatusMonitor; use crate::node_adapter::BaseNodeStatus; use crate::p2pool_manager::{self, P2poolManager}; +use crate::process_utils::retry_with_backoff; use crate::{ app_config::{AppConfig, MiningMode}, cpu_miner::CpuMiner, @@ -41,9 +42,7 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use sha2::Digest; use std::collections::HashMap; -use std::future::Future; use std::ops::Div; -use std::pin::Pin; use std::{sync::Arc, thread::sleep, time::Duration}; use tari_common::configuration::Network; use tari_utilities::encoding::MBase58; @@ -676,40 +675,3 @@ async fn send_telemetry_data( } Ok(None) } - -async fn retry_with_backoff( - mut f: T, - increment_in_secs: u64, - max_retries: u64, - operation_name: &str, -) -> anyhow::Result -where - T: FnMut() -> Pin> + Send>>, - E: std::error::Error, -{ - let range_size = increment_in_secs * max_retries + 1; - - for i in (0..range_size).step_by(usize::try_from(increment_in_secs)?) { - tokio::time::sleep(Duration::from_secs(i)).await; - - let result = f().await; - match result { - Ok(res) => return Ok(res), - Err(e) => { - if i == range_size - 1 { - return Err(anyhow::anyhow!( - "Max retries reached, {} failed. Last error: {:?}", - operation_name, - e - )); - } else { - warn!(target: LOG_TARGET, "Retrying {} due to failure: {:?}", operation_name, e); - } - } - } - } - Err(anyhow::anyhow!( - "Max retries reached, {} failed without capturing error", - operation_name - )) -} diff --git a/src-tauri/src/telemetry_service.rs b/src-tauri/src/telemetry_service.rs new file mode 100644 index 000000000..702959457 --- /dev/null +++ b/src-tauri/src/telemetry_service.rs @@ -0,0 +1,250 @@ +// Copyright 2024. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use log::{debug, error, warn}; +use serde::Serialize; +use serde_json::Value; +use std::{sync::Arc, time::SystemTime}; +use tokio::sync::{ + mpsc::{self, Sender}, + RwLock, +}; +use tokio_util::sync::CancellationToken; + +use crate::{ + app_config::AppConfig, + app_in_memory_config::AppInMemoryConfig, + hardware::hardware_status_monitor::HardwareStatusMonitor, + process_utils::retry_with_backoff, + utils::platform_utils::{CurrentOperatingSystem, PlatformUtils}, +}; + +const LOG_TARGET: &str = "tari::universe::telemetry_service"; + +#[derive(Debug, Serialize, Clone)] +pub struct TelemetryData { + event_name: String, + event_value: Value, +} + +#[derive(Debug, Serialize)] +pub struct FullTelemetryData { + event_name: String, + event_value: Value, + created_at: SystemTime, + user_id: String, + app_id: String, + version: String, + os: String, + cpu_name: String, + gpu_name: String, +} + +#[derive(Debug, thiserror::Error)] +pub enum TelemetryServiceError { + #[error("Other error: {0}")] + Other(#[from] anyhow::Error), + + #[error("Reqwest error: {0}")] + ReqwestError(#[from] reqwest::Error), +} + +pub struct TelemetryService { + app_id: String, + version: String, + tx_channel: Option>, + cancellation_token: CancellationToken, + config: Arc>, + in_memory_config: Arc>, +} + +impl TelemetryService { + pub fn new( + app_id: String, + config: Arc>, + in_memory_config: Arc>, + ) -> Self { + let cancellation_token = CancellationToken::new(); + TelemetryService { + app_id, + version: "0.0.0".to_string(), + tx_channel: None, + cancellation_token, + config, + in_memory_config, + } + } + pub async fn init( + &mut self, + app_version: String, + user: String, + ) -> Result<(), TelemetryServiceError> { + let hardware = HardwareStatusMonitor::current(); + let cpu_name = hardware.get_cpu_devices().await?; + let cpu_name = match cpu_name.first() { + Some(cpu) => cpu.public_properties.name.clone(), + None => "Unknown".to_string(), + }; + let gpu_name = hardware.get_gpu_devices().await?; + let gpu_name = match gpu_name.first() { + Some(gpu) => gpu.public_properties.name.clone(), + None => "Unknown".to_string(), + }; + let os = PlatformUtils::detect_current_os(); + + self.version = app_version; + let cancellation_token = self.cancellation_token.clone(); + let config_cloned = self.config.clone(); + let in_memory_config_cloned = self.in_memory_config.clone(); + let telemetry_api_url = in_memory_config_cloned + .read() + .await + .telemetry_api_url + .clone(); + let app_id = self.app_id.clone(); + let version = self.version.clone(); + let (tx, mut rx) = mpsc::channel(128); + self.tx_channel = Some(tx); + tokio::spawn(async move { + let system_info = SystemInfo { + app_id, + version, + user_id: user, + cpu_name, + gpu_name, + os, + }; + tokio::select! { + _ = async { + debug!(target: LOG_TARGET, "TelemetryService::init has been started"); + while let Some(telemetry_data) = rx.recv().await { + let telemetry_collection_enabled = config_cloned.read().await.allow_telemetry(); + if telemetry_collection_enabled { + drop(retry_with_backoff( + || { + Box::pin(send_telemetry_data( + telemetry_data.clone(), + telemetry_api_url.clone(), + system_info.clone(), + )) + }, + 3, + 2, + "send_telemetry_data", + ) + .await); + } + } + } => {}, + _ = cancellation_token.cancelled() => { + debug!(target: LOG_TARGET,"TelemetryService::init has been cancelled"); + } + } + }); + Ok(()) + } + + pub async fn send( + &self, + event_name: String, + event_value: Value, + ) -> Result<(), TelemetryServiceError> { + let data = TelemetryData { + event_name, + event_value, + }; + if let Some(tx) = &self.tx_channel { + if (tx.send(data).await).is_err() { + warn!(target: LOG_TARGET,"TelemetryService::send_telemetry_data Telemetry data sending failed"); + return Err(TelemetryServiceError::Other(anyhow::anyhow!( + "Telemetry data sending failed" + ))); + } + Ok(()) + } else { + warn!(target: LOG_TARGET,"TelemetryService::send_telemetry_data Telemetry data sending failed - Service is not initialized"); + Err(TelemetryServiceError::Other(anyhow::anyhow!( + "Telemetry data sending failed - Service is not initialized" + ))) + } + } +} + +#[derive(Clone)] +struct SystemInfo { + app_id: String, + version: String, + user_id: String, + os: CurrentOperatingSystem, + cpu_name: String, + gpu_name: String, +} + +async fn send_telemetry_data( + data: TelemetryData, + api_url: String, + system_info: SystemInfo, +) -> Result<(), TelemetryServiceError> { + let request = reqwest::Client::new(); + + let full_data = FullTelemetryData { + event_name: data.event_name, + event_value: data.event_value, + created_at: SystemTime::now(), + user_id: system_info.user_id, + app_id: system_info.app_id, + version: system_info.version, + os: system_info.os.to_string(), + cpu_name: system_info.cpu_name, + gpu_name: system_info.gpu_name, + }; + let request_builder = request + .post(api_url) + .header( + "User-Agent".to_string(), + format!("tari-universe/{}", full_data.version.clone()), + ) + .json(&full_data); + + let response = request_builder.send().await?; + + if response.status() == 429 { + warn!(target: LOG_TARGET,"TelemetryService::send_telemetry_data Telemetry data rate limited by http {:?}", response.status()); + return Ok(()); + } + + if response.status() != 200 { + let status = response.status(); + let response = response.text().await?; + let response_as_json: Result = serde_json::from_str(&response); + return Err(anyhow::anyhow!( + "Telemetry data sending error. Status {:?} response text: {:?}", + status.to_string(), + response_as_json.unwrap_or(response.into()), + ) + .into()); + } + + debug!(target: LOG_TARGET,"TelemetryService::send_telemetry_data Telemetry data sent"); + + Ok(()) +} diff --git a/src-tauri/src/utils/platform_utils.rs b/src-tauri/src/utils/platform_utils.rs index 58352c5bf..6c5da82ac 100644 --- a/src-tauri/src/utils/platform_utils.rs +++ b/src-tauri/src/utils/platform_utils.rs @@ -20,12 +20,25 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +use std::fmt::Display; + +#[derive(Clone)] pub enum CurrentOperatingSystem { Windows, Linux, MacOS, } +impl Display for CurrentOperatingSystem { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CurrentOperatingSystem::Windows => write!(f, "Windows"), + CurrentOperatingSystem::Linux => write!(f, "Linux"), + CurrentOperatingSystem::MacOS => write!(f, "MacOS"), + } + } +} + pub struct PlatformUtils {} impl PlatformUtils { pub fn detect_current_os() -> CurrentOperatingSystem { diff --git a/src/types/invoke.ts b/src/types/invoke.ts index 4f3041cb4..0ddaffcb6 100644 --- a/src/types/invoke.ts +++ b/src/types/invoke.ts @@ -35,6 +35,7 @@ declare module '@tauri-apps/api/core' { function invoke(param: 'start_mining'): Promise; function invoke(param: 'stop_mining'): Promise; function invoke(param: 'set_allow_telemetry', payload: { allow_telemetry: boolean }): Promise; + function invoke(param: 'send_data_telemetry_service', payload: { eventName: string; data: object }): Promise; function invoke(param: 'set_user_inactivity_timeout', payload: { timeout: number }): Promise; function invoke(param: 'update_applications'): Promise; function invoke(