From e08439696923638a01253c543f50254d986951e5 Mon Sep 17 00:00:00 2001 From: Diego Prats Date: Tue, 12 Nov 2024 17:03:59 -0800 Subject: [PATCH] feat: use firebase instead of mixpanel in for event tracking (#32) Co-authored-by: Diego Prats Co-authored-by: Collin Jackson --- clients/cli/src/analytics.rs | 39 ++++++++++++++--- clients/cli/src/config.rs | 84 ++++++++++++++++++++++++++++++------ 2 files changed, 104 insertions(+), 19 deletions(-) diff --git a/clients/cli/src/analytics.rs b/clients/cli/src/analytics.rs index effd2ee..681a81c 100644 --- a/clients/cli/src/analytics.rs +++ b/clients/cli/src/analytics.rs @@ -1,4 +1,4 @@ -use crate::config::analytics_token; +use crate::config::{analytics_id, analytics_api_key}; use chrono::Datelike; use chrono::Timelike; use reqwest::header::{ACCEPT, CONTENT_TYPE}; @@ -16,14 +16,27 @@ pub fn track( ) { println!("{}", description); - let token = analytics_token(ws_addr_string); - if token.is_empty() { + let firebase_app_id = analytics_id(ws_addr_string); + let firebase_api_key = analytics_api_key(ws_addr_string); + if firebase_app_id.is_empty() { return; } let local_now = chrono::offset::Local::now(); + + // For tracking events, we use the Firebase Measurement Protocol + // Firebase is mostly designed for mobile and web apps, but for our use case of a CLI, + // we can use the Measurement Protocol to track events by POST to a URL. + // The only thing that may be unexpected is that the URL we use includes a firebase key + + // Firebase format for properties for Measurement protocol: + // https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?client_type=firebase#payload + // https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?client_type=firebase#payload_query_parameters let mut properties = json!({ - "token": token, "time": SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis(), + // app_instance_id is the standard key Firebase uses this key to track the same user across sessions + // It is a bit redundant, but I wanted to keep the recommended format Firebase uses to minimize surprises + // I still left the distinct_id key as well for backwards compatibility + "app_instance_id": event_properties["prover_id"], "distinct_id": event_properties["prover_id"], "prover_type": "volunteer", "client_type": "cli", @@ -36,14 +49,26 @@ pub fn track( for (k, v) in event_properties.as_object().unwrap() { properties[k] = v.clone(); } + + // Firebase format for events let body = json!({ - "event": event_name, - "properties": properties + "app_instance_id": event_properties["prover_id"], + "events": [{ + "name": event_name, + "params": properties + }], }); + tokio::spawn(async move { let client = reqwest::Client::new(); + let _ = client - .post("https://api.mixpanel.com/track?ip=1") + // URL is the Google Analytics endpoint for Firebase: https://stackoverflow.com/questions/50355752/firebase-analytics-from-remote-rest-api + .post(format!( + "https://www.google-analytics.com/mp/collect?firebase_app_id={}&api_secret={}", + firebase_app_id, + firebase_api_key + )) .body(format!("[{}]", body)) .header(ACCEPT, "text/plain") .header(CONTENT_TYPE, "application/json") diff --git a/clients/cli/src/config.rs b/clients/cli/src/config.rs index 50a7f5f..074365f 100644 --- a/clients/cli/src/config.rs +++ b/clients/cli/src/config.rs @@ -1,18 +1,78 @@ +// Debug version of analytics_id #[cfg(debug_assertions)] -pub fn analytics_token(_ws_addr_string: &str) -> String { +pub fn analytics_id(_ws_addr_string: &str) -> String { // Use one of the tokens in the release version if debugging analytics "".into() } -#[cfg(not(debug_assertions))] -pub fn analytics_token(ws_addr_string: &str) -> String { - if ws_addr_string.starts_with("wss://dev.orchestrator.nexus.xyz:443/") { - return "".into(); // TODO: Firebase Analytics tid - } else if ws_addr_string.starts_with("wss://staging.orchestrator.nexus.xyz:443/") { - return "".into(); // TODO: Firebase Analytics tid - } else if ws_addr_string.starts_with("wss://beta.orchestrator.nexus.xyz:443/") { - return "".into(); // TODO: Firebase Analytics tid - } else { - return "".into(); - }; +// Debug version of analytics_api_key +#[cfg(debug_assertions)] +pub fn analytics_api_key(_ws_addr_string: &str) -> String { + // Use one of the tokens in the release version if debugging analytics + "".into() } + +// The following enum is used to determine the environment from the web socket string +#[derive(Debug)] +#[cfg(not(debug_assertions))] +enum Environment { + Dev, + Staging, + Beta, + Unknown, +} + +// The web socket addresses for the different environments +#[cfg(not(debug_assertions))] +mod web_socket_urls { + pub const DEV: &str = "wss://dev.orchestrator.nexus.xyz:443/"; + pub const STAGING: &str = "wss://staging.orchestrator.nexus.xyz:443/"; + pub const BETA: &str = "wss://beta.orchestrator.nexus.xyz:443/"; +} + +// the firebase APP IDS by environment +#[cfg(not(debug_assertions))] +mod firebase { + pub const DEV_APP_ID: &str = "1:954530464230:web:f0a14de14ef7bcdaa99627"; + pub const STAGING_APP_ID: &str = "1:222794630996:web:1758d64a85eba687eaaac1"; + pub const BETA_APP_ID: &str = "1:279395003658:web:04ee2c524474d683d75ef3"; + + // Analytics keys for the different environments + // These are keys that allow the measurement protocol to write to the analytics database + // They are not sensitive. Worst case, if a malicious actor obtains the secret, they could potentially send false or misleading data to your GA4 property + pub const DEV_API_SECRET: &str = "8ySxiKrtT8a76zClqqO8IQ"; + pub const STAGING_API_SECRET: &str = "OI7H53soRMSDWfJf1ittHQ"; + pub const BETA_API_SECRET: &str = "gxxzKAQLSl-uYI0eKbIi_Q"; +} + +// Release versions (existing code) +#[cfg(not(debug_assertions))] +pub fn analytics_id(ws_addr_string: &str) -> String { + + // Determine the environment from the web socket string (ws_addr_string) + let env = match ws_addr_string { + web_socket_urls::DEV => Environment::Dev, + web_socket_urls::STAGING => Environment::Staging, + web_socket_urls::BETA => Environment::Beta, + _ => Environment::Unknown, + }; + + // Return the appropriate Firebase App ID based on the environment + match env { + Environment::Dev => firebase::DEV_APP_ID.to_string(), + Environment::Staging => firebase::STAGING_APP_ID.to_string(), + Environment::Beta => firebase::BETA_APP_ID.to_string(), + Environment::Unknown => String::new(), + } +} + +#[cfg(not(debug_assertions))] +pub fn analytics_api_key(ws_addr_string: &str) -> String { + match ws_addr_string { + web_socket_urls::DEV => firebase::DEV_API_SECRET.to_string(), + web_socket_urls::STAGING => firebase::STAGING_API_SECRET.to_string(), + web_socket_urls::BETA => firebase::BETA_API_SECRET.to_string(), + _ => String::new(), + } +} +