From a6796fe8b26e928ff628929522a076933385fd53 Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Fri, 4 Jul 2025 17:55:44 +0100 Subject: [PATCH] fix(telemetry): always use hex-encoded ID as user ID (#9781) We are currently in the process of transitioning the Firezone Clients away from always hashing the ID before sending it to the portal. This will make lookups and correlation of data between our systems much easier. The way we are performing this migration is that new installations of Firezone will directly generate a 64 char hex-string as the Firezone ID. If the ID looks like a UUID (which is the old format), we still hash it and send it to the portal, otherwise we send it as-is. Presently, the telemetry integration with Sentry and PostHog do the opposite. They always sets the Firezone ID as-is and includes an `external_id` that is the hashed form if it detects that it is a UUID (or in the case of PostHog, create an alias). It is much better to flip this around and always set the hex-string as the user id. That way, we can simply always filter by the `user.id` attribute in Sentry and always refer to the ID that we are seeing in the portal. --- rust/telemetry/src/analytics.rs | 22 ++++++++++--- rust/telemetry/src/feature_flags.rs | 10 +++++- rust/telemetry/src/lib.rs | 49 ++++++++++++++--------------- 3 files changed, 50 insertions(+), 31 deletions(-) diff --git a/rust/telemetry/src/analytics.rs b/rust/telemetry/src/analytics.rs index b159f08e9..3c21e3c5b 100644 --- a/rust/telemetry/src/analytics.rs +++ b/rust/telemetry/src/analytics.rs @@ -6,7 +6,13 @@ use sha2::Digest as _; use crate::{Env, posthog::RUNTIME}; -pub fn new_session(distinct_id: String, api_url: String) { +pub fn new_session(maybe_legacy_id: String, api_url: String) { + let distinct_id = if uuid::Uuid::from_str(&maybe_legacy_id).is_ok() { + hex::encode(sha2::Sha256::digest(&maybe_legacy_id)) + } else { + maybe_legacy_id + }; + RUNTIME.spawn(async move { if let Err(e) = capture( "new_session", @@ -23,11 +29,19 @@ pub fn new_session(distinct_id: String, api_url: String) { /// Associate several properties with a particular "distinct_id" in PostHog. pub fn identify( - distinct_id: String, + maybe_legacy_id: String, api_url: String, release: String, account_slug: Option, ) { + let is_legacy_id = uuid::Uuid::from_str(&maybe_legacy_id).is_ok(); + + let distinct_id = if is_legacy_id { + hex::encode(sha2::Sha256::digest(&maybe_legacy_id)) + } else { + maybe_legacy_id.clone() + }; + RUNTIME.spawn({ let distinct_id = distinct_id.clone(); let api_url = api_url.clone(); @@ -53,14 +67,14 @@ pub fn identify( }); // Create an alias ID for the user so we can also find them under the "external ID" used in the portal. - if uuid::Uuid::from_str(&distinct_id).is_ok() { + if is_legacy_id { RUNTIME.spawn(async move { if let Err(e) = capture( "$create_alias", distinct_id.clone(), api_url, CreateAliasProperties { - alias: hex::encode(sha2::Sha256::digest(&distinct_id)), + alias: maybe_legacy_id, distinct_id, }, ) diff --git a/rust/telemetry/src/feature_flags.rs b/rust/telemetry/src/feature_flags.rs index 6bea0dcf5..1efb75d7f 100644 --- a/rust/telemetry/src/feature_flags.rs +++ b/rust/telemetry/src/feature_flags.rs @@ -1,4 +1,5 @@ use std::{ + str::FromStr as _, sync::{ LazyLock, atomic::{AtomicBool, Ordering}, @@ -8,6 +9,7 @@ use std::{ use anyhow::{Context as _, Result, bail}; use serde::{Deserialize, Serialize}; +use sha2::Digest as _; use crate::{ Env, @@ -88,7 +90,13 @@ pub(crate) async fn reeval_timer() { } } -async fn decide(distinct_id: String, api_key: String) -> Result { +async fn decide(maybe_legacy_id: String, api_key: String) -> Result { + let distinct_id = if uuid::Uuid::from_str(&maybe_legacy_id).is_ok() { + hex::encode(sha2::Sha256::digest(&maybe_legacy_id)) + } else { + maybe_legacy_id + }; + let response = reqwest::ClientBuilder::new() .connection_verbose(true) .build()? diff --git a/rust/telemetry/src/lib.rs b/rust/telemetry/src/lib.rs index 989ac05b1..4907a6e2f 100644 --- a/rust/telemetry/src/lib.rs +++ b/rust/telemetry/src/lib.rs @@ -1,6 +1,6 @@ #![cfg_attr(test, allow(clippy::unwrap_used))] -use std::{borrow::Cow, fmt, str::FromStr, sync::Arc, time::Duration}; +use std::{borrow::Cow, collections::BTreeMap, fmt, str::FromStr, sync::Arc, time::Duration}; use anyhow::{Result, bail}; use sentry::{ @@ -173,10 +173,7 @@ impl Telemetry { scope.set_context("os", ctx); } - scope.set_user(Some(User { - id: Some(firezone_id), - ..User::default() - })); + scope.set_user(Some(compute_user(firezone_id))); }); self.inner.replace(inner); sentry::start_session(); @@ -216,31 +213,31 @@ impl Telemetry { user.other.insert("account_slug".to_owned(), slug.into()); }); } +} - pub fn set_firezone_id(id: String) { - update_user({ - let id = id.clone(); - move |user| { - user.id = Some(id.clone()); +/// Computes the [`User`] scope based on the contents of `firezone_id`. +/// +/// If `firezone_id` looks like a UUID, we hash and hex-encode it. +/// This will align the ID with what we see in the portal. +/// +/// If it is not a UUID, it is already from a newer installation of Firezone +/// where the ID is sent as-is. +/// +/// As a result, this will allow us to always filter the user by the hex-encoded ID. +fn compute_user(firezone_id: String) -> User { + if uuid::Uuid::from_str(&firezone_id).is_ok() { + let encoded_id = hex::encode(sha2::Sha256::digest(firezone_id)); - if uuid::Uuid::from_str(&id).is_ok() { - user.other.insert( - "external_id".to_owned(), - serde_json::Value::String(hex::encode(sha2::Sha256::digest(&id))), - ); - } - } - }); - - let Some(client) = sentry::Hub::main().client() else { - return; + return User { + id: Some(encoded_id.clone()), + other: BTreeMap::from([("uuid".to_owned(), serde_json::Value::String(encoded_id))]), + ..User::default() }; + } - let Some(env) = client.options().environment.as_ref() else { - return; // Nothing to do if we don't have an environment set. - }; - - feature_flags::reevaluate(id, env); + User { + id: Some(firezone_id), + ..User::default() } }