diff --git a/rust/android-client-ffi/src/lib.rs b/rust/android-client-ffi/src/lib.rs index 008fe8747..1d47fa42f 100644 --- a/rust/android-client-ffi/src/lib.rs +++ b/rust/android-client-ffi/src/lib.rs @@ -12,7 +12,7 @@ use client_shared::{DisconnectError, Session, V4RouteList, V6RouteList}; use connlib_model::ResourceView; use dns_types::DomainName; use firezone_logging::{err_with_src, sentry_layer}; -use firezone_telemetry::{ANDROID_DSN, Telemetry}; +use firezone_telemetry::{ANDROID_DSN, Telemetry, analytics}; use ip_network::{Ipv4Network, Ipv6Network}; use jni::{ JNIEnv, JavaVM, @@ -345,6 +345,8 @@ fn connect( telemetry.start(&api_url, RELEASE, ANDROID_DSN); Telemetry::set_firezone_id(device_id.clone()); + analytics::identify(device_id.clone(), api_url.to_string(), RELEASE.to_owned()); + init_logging(&PathBuf::from(log_dir), log_filter)?; install_rustls_crypto_provider(); @@ -356,7 +358,7 @@ fn connect( let url = LoginUrl::client( api_url.as_str(), &secret, - device_id, + device_id.clone(), Some(device_name), device_info, )?; @@ -389,6 +391,8 @@ fn connect( runtime.handle().clone(), ); + analytics::new_session(device_id, api_url.to_string()); + runtime.spawn(async move { while let Some(event) = event_stream.next().await { match event { diff --git a/rust/apple-client-ffi/src/lib.rs b/rust/apple-client-ffi/src/lib.rs index 6ff2e5662..3538bad17 100644 --- a/rust/apple-client-ffi/src/lib.rs +++ b/rust/apple-client-ffi/src/lib.rs @@ -15,6 +15,7 @@ use firezone_logging::err_with_src; use firezone_logging::sentry_layer; use firezone_telemetry::APPLE_DSN; use firezone_telemetry::Telemetry; +use firezone_telemetry::analytics; use ip_network::{Ipv4Network, Ipv6Network}; use phoenix_channel::LoginUrl; use phoenix_channel::PhoenixChannel; @@ -258,6 +259,8 @@ impl WrappedSession { Telemetry::set_firezone_id(device_id.clone()); Telemetry::set_account_slug(account_slug); + analytics::identify(device_id.clone(), api_url.to_string(), RELEASE.to_owned()); + init_logging(log_dir.into(), log_filter)?; install_rustls_crypto_provider(); @@ -268,7 +271,7 @@ impl WrappedSession { let url = LoginUrl::client( api_url.as_str(), &secret, - device_id, + device_id.clone(), device_name_override, device_info, )?; @@ -300,6 +303,8 @@ impl WrappedSession { ); session.set_tun(Box::new(Tun::new()?)); + analytics::new_session(device_id, api_url.to_string()); + runtime.spawn(async move { let callback_handler = CallbackHandler { inner: callback_handler, diff --git a/rust/gui-client/src-tauri/src/gui.rs b/rust/gui-client/src-tauri/src/gui.rs index 95b8a2276..5aa81eec9 100644 --- a/rust/gui-client/src-tauri/src/gui.rs +++ b/rust/gui-client/src-tauri/src/gui.rs @@ -16,7 +16,7 @@ use crate::{ }; use anyhow::{Context, Result, bail}; use firezone_logging::err_with_src; -use firezone_telemetry::Telemetry; +use firezone_telemetry::{Telemetry, analytics}; use futures::SinkExt as _; use std::time::Duration; use tauri::{Emitter, Manager}; @@ -226,12 +226,13 @@ pub fn run( .inspect_err(|e| tracing::debug!("Failed to load MDM settings {e:#}")) .unwrap_or_default(); + let api_url = mdm_settings + .api_url + .as_ref() + .unwrap_or(&advanced_settings.api_url); + telemetry.start( - mdm_settings - .api_url - .as_ref() - .unwrap_or(&advanced_settings.api_url) - .as_str(), + api_url.as_str(), crate::RELEASE, firezone_telemetry::GUI_DSN, ); @@ -239,7 +240,9 @@ pub fn run( // Get the device ID before starting Tokio, so that all the worker threads will inherit the correct scope. // Technically this means we can fail to get the device ID on a newly-installed system, since the Tunnel service may not have fully started up when the GUI process reaches this point, but in practice it's unlikely. if let Ok(id) = firezone_bin_shared::device_id::get() { - Telemetry::set_firezone_id(id.id); + Telemetry::set_firezone_id(id.id.clone()); + + analytics::identify(id.id, api_url.to_string(), crate::RELEASE.to_owned()); } // Needed for the deep link server diff --git a/rust/gui-client/src-tauri/src/service.rs b/rust/gui-client/src-tauri/src/service.rs index ff310f721..49f314ee6 100644 --- a/rust/gui-client/src-tauri/src/service.rs +++ b/rust/gui-client/src-tauri/src/service.rs @@ -14,7 +14,7 @@ use firezone_bin_shared::{ signals, }; use firezone_logging::{FilterReloadHandle, err_with_src, telemetry_span}; -use firezone_telemetry::Telemetry; +use firezone_telemetry::{Telemetry, analytics}; use futures::{ Future as _, SinkExt as _, Stream as _, future::poll_fn, @@ -456,7 +456,7 @@ impl<'a> Handler<'a> { let url = LoginUrl::client( Url::parse(api_url).context("Failed to parse URL")?, &token, - device_id.id, + device_id.id.clone(), None, DeviceInfo { device_serial: device_info::serial(), @@ -490,6 +490,9 @@ impl<'a> Handler<'a> { portal, tokio::runtime::Handle::current(), ); + + analytics::new_session(device_id.id, api_url.to_string()); + // Call `set_dns` before `set_tun` so that the tunnel starts up with a valid list of resolvers. tracing::debug!(?dns, "Calling `set_dns`..."); connlib.set_dns(dns); diff --git a/rust/headless-client/src/main.rs b/rust/headless-client/src/main.rs index 6f0a8185c..2d20ed0d0 100644 --- a/rust/headless-client/src/main.rs +++ b/rust/headless-client/src/main.rs @@ -12,8 +12,7 @@ use firezone_bin_shared::{ signals, }; use firezone_logging::telemetry_span; -use firezone_telemetry::Telemetry; -use firezone_telemetry::otel; +use firezone_telemetry::{Telemetry, analytics, otel}; use opentelemetry_sdk::metrics::{PeriodicReader, SdkMeterProvider}; use phoenix_channel::PhoenixChannel; use phoenix_channel::get_user_agent; @@ -129,6 +128,7 @@ enum Cmd { } const VERSION: &str = env!("CARGO_PKG_VERSION"); +const RELEASE: &str = concat!("headless-client@", env!("CARGO_PKG_VERSION")); fn main() -> Result<()> { rustls::crypto::ring::default_provider() @@ -174,7 +174,7 @@ fn main() -> Result<()> { if cli.is_telemetry_allowed() { telemetry.start( cli.api_url.as_ref(), - &format!("headless-client@{VERSION}"), + RELEASE, firezone_telemetry::HEADLESS_DSN, ); } @@ -201,8 +201,14 @@ fn main() -> Result<()> { }; Telemetry::set_firezone_id(firezone_id.clone()); + analytics::identify( + firezone_id.clone(), + cli.api_url.to_string(), + RELEASE.to_owned(), + ); + let url = LoginUrl::client( - cli.api_url, + cli.api_url.clone(), &token, firezone_id.clone(), cli.firezone_name, @@ -230,7 +236,7 @@ fn main() -> Result<()> { .with_resource(otel::default_resource_with([ otel::attr::service_name!(), otel::attr::service_version!(), - otel::attr::service_instance_id(firezone_id), + otel::attr::service_instance_id(firezone_id.clone()), ])) .build(); @@ -263,6 +269,8 @@ fn main() -> Result<()> { rt.handle().clone(), ); + analytics::new_session(firezone_id.clone(), cli.api_url.to_string()); + let mut terminate = signals::Terminate::new()?; let mut hangup = signals::Hangup::new()?; diff --git a/rust/telemetry/src/analytics.rs b/rust/telemetry/src/analytics.rs new file mode 100644 index 000000000..d4c87851e --- /dev/null +++ b/rust/telemetry/src/analytics.rs @@ -0,0 +1,103 @@ +use anyhow::{Context as _, Result, bail}; +use serde::Serialize; + +use crate::{Env, posthog::RUNTIME}; + +pub fn new_session(distinct_id: String, api_url: String) { + RUNTIME.spawn(async move { + if let Err(e) = capture( + "new_session", + distinct_id, + api_url.clone(), + NewSessionProperties { api_url }, + ) + .await + { + tracing::debug!("Failed to log `new_session` event: {e:#}"); + } + }); +} + +/// Associate several properties with a particular "distinct_id" in PostHog. +pub fn identify(distinct_id: String, api_url: String, release: String) { + RUNTIME.spawn(async move { + if let Err(e) = capture( + "$identify", + distinct_id, + api_url, + IdentifyProperties { + set: PersonProperties { + release, + os: std::env::consts::OS.to_owned(), + }, + }, + ) + .await + { + tracing::debug!("Failed to log `$identify` event: {e:#}"); + } + }); +} + +async fn capture
(
+ event: impl Into {
+ event: String,
+ distinct_id: String,
+ api_key: String,
+ properties: P,
+}
+
+#[derive(serde::Serialize)]
+struct NewSessionProperties {
+ api_url: String,
+}
+
+#[derive(serde::Serialize)]
+struct IdentifyProperties {
+ #[serde(rename = "$set")]
+ set: PersonProperties,
+}
+
+#[derive(serde::Serialize)]
+struct PersonProperties {
+ release: String,
+ #[serde(rename = "$os")]
+ os: String,
+}
diff --git a/rust/telemetry/src/feature_flags.rs b/rust/telemetry/src/feature_flags.rs
index 3643a951c..1aaeeb6e8 100644
--- a/rust/telemetry/src/feature_flags.rs
+++ b/rust/telemetry/src/feature_flags.rs
@@ -3,13 +3,13 @@ use std::{sync::LazyLock, time::Duration};
use anyhow::{Context as _, Result, bail};
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
-use tokio::runtime::Runtime;
-const POSTHOG_API_KEY_PROD: &str = "phc_uXXl56plyvIBHj81WwXBLtdPElIRbm7keRTdUCmk8ll";
-const POSTHOG_API_KEY_STAGING: &str = "phc_tHOVtq183RpfKmzadJb4bxNpLM5jzeeb1Gu8YSH3nsK";
-const RE_EVAL_DURATION: Duration = Duration::from_secs(5 * 60);
+use crate::{
+ Env,
+ posthog::{POSTHOG_API_KEY_PROD, POSTHOG_API_KEY_STAGING, RUNTIME},
+};
-static RUNTIME: LazyLock