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.
This commit is contained in:
Thomas Eizinger
2025-07-04 17:55:44 +01:00
committed by GitHub
parent 8b001b3e8b
commit a6796fe8b2
3 changed files with 50 additions and 31 deletions

View File

@@ -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<String>,
) {
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,
},
)

View File

@@ -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<FeatureFlagsResponse> {
async fn decide(maybe_legacy_id: String, api_key: String) -> Result<FeatureFlagsResponse> {
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()?

View File

@@ -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()
}
}