mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
feat(telemetry): grab env and distinct_id from Sentry session (#9801)
At present, our primary indicator as to whether telemetry is active is whether we have a Sentry session. For our analytics events however, we currently require passing in the Firezone ID and API url again. This makes it difficult to send analytics events from areas of the code that don't have this information available. To still allow for that, we integrate the `analytics` module more tightly with the Sentry session. This allows us to drop two parameters from the `$identify` event and also means we now respect the `NO_TELEMETRY` setting for these events except for `new_session`. This event is sent regardless because it allows us to track, how many on-prem installations of Firezone are out there.
This commit is contained in:
@@ -264,12 +264,7 @@ impl WrappedSession {
|
||||
runtime.block_on(telemetry.start(&api_url, RELEASE, APPLE_DSN, device_id.clone()));
|
||||
Telemetry::set_account_slug(account_slug.clone());
|
||||
|
||||
analytics::identify(
|
||||
device_id.clone(),
|
||||
api_url.to_string(),
|
||||
RELEASE.to_owned(),
|
||||
Some(account_slug),
|
||||
);
|
||||
analytics::identify(RELEASE.to_owned(), Some(account_slug));
|
||||
|
||||
init_logging(log_dir.into(), log_filter)?;
|
||||
install_rustls_crypto_provider();
|
||||
|
||||
@@ -237,12 +237,7 @@ fn connect(
|
||||
runtime.block_on(telemetry.start(&api_url, RELEASE, platform::DSN, device_id.clone()));
|
||||
Telemetry::set_account_slug(account_slug.clone());
|
||||
|
||||
analytics::identify(
|
||||
device_id.clone(),
|
||||
api_url.to_string(),
|
||||
RELEASE.to_owned(),
|
||||
Some(account_slug),
|
||||
);
|
||||
analytics::identify(RELEASE.to_owned(), Some(account_slug));
|
||||
|
||||
init_logging(&PathBuf::from(log_dir), log_filter)?;
|
||||
install_rustls_crypto_provider();
|
||||
|
||||
@@ -52,7 +52,6 @@ pub struct Eventloop {
|
||||
tunnel: GatewayTunnel,
|
||||
portal: PhoenixChannel<(), IngressMessages, (), PublicKeyParam>,
|
||||
tun_device_manager: Arc<Mutex<TunDeviceManager>>,
|
||||
firezone_id: String,
|
||||
|
||||
resolve_tasks:
|
||||
futures_bounded::FuturesTupleSet<Result<Vec<IpAddr>, Arc<anyhow::Error>>, ResolveTrigger>,
|
||||
@@ -68,14 +67,12 @@ impl Eventloop {
|
||||
tunnel: GatewayTunnel,
|
||||
mut portal: PhoenixChannel<(), IngressMessages, (), PublicKeyParam>,
|
||||
tun_device_manager: TunDeviceManager,
|
||||
firezone_id: String,
|
||||
) -> Self {
|
||||
portal.connect(PublicKeyParam(tunnel.public_key().to_bytes()));
|
||||
|
||||
Self {
|
||||
tunnel,
|
||||
portal,
|
||||
firezone_id,
|
||||
tun_device_manager: Arc::new(Mutex::new(tun_device_manager)),
|
||||
resolve_tasks: futures_bounded::FuturesTupleSet::new(DNS_RESOLUTION_TIMEOUT, 1000),
|
||||
set_interface_tasks: futures_bounded::FuturesSet::new(Duration::from_secs(5), 10),
|
||||
@@ -385,12 +382,7 @@ impl Eventloop {
|
||||
if let Some(account_slug) = init.account_slug {
|
||||
Telemetry::set_account_slug(account_slug.clone());
|
||||
|
||||
analytics::identify(
|
||||
self.firezone_id.clone(),
|
||||
self.portal.url(),
|
||||
RELEASE.to_owned(),
|
||||
Some(account_slug),
|
||||
)
|
||||
analytics::identify(RELEASE.to_owned(), Some(account_slug))
|
||||
}
|
||||
|
||||
self.tunnel.state_mut().update_relays(
|
||||
|
||||
@@ -134,13 +134,8 @@ async fn try_main(cli: Cli, telemetry: &mut Telemetry) -> Result<()> {
|
||||
opentelemetry::global::set_meter_provider(provider);
|
||||
}
|
||||
|
||||
let login = LoginUrl::gateway(
|
||||
cli.api_url,
|
||||
&cli.token,
|
||||
firezone_id.clone(),
|
||||
cli.firezone_name,
|
||||
)
|
||||
.context("Failed to construct URL for logging into portal")?;
|
||||
let login = LoginUrl::gateway(cli.api_url, &cli.token, firezone_id, cli.firezone_name)
|
||||
.context("Failed to construct URL for logging into portal")?;
|
||||
|
||||
let resolv_conf = resolv_conf::Config::parse(
|
||||
std::fs::read_to_string("/etc/resolv.conf").context("Failed to read /etc/resolv.conf")?,
|
||||
@@ -184,7 +179,7 @@ async fn try_main(cli: Cli, telemetry: &mut Telemetry) -> Result<()> {
|
||||
}
|
||||
|
||||
let eventloop = future::poll_fn({
|
||||
let mut eventloop = Eventloop::new(tunnel, portal, tun_device_manager, firezone_id);
|
||||
let mut eventloop = Eventloop::new(tunnel, portal, tun_device_manager);
|
||||
|
||||
move |cx| eventloop.poll(cx)
|
||||
});
|
||||
|
||||
@@ -587,18 +587,13 @@ impl<'a> Handler<'a> {
|
||||
firezone_telemetry::GUI_DSN,
|
||||
self.device_id.id.clone(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
.await;
|
||||
|
||||
if let Some(account_slug) = account_slug {
|
||||
Telemetry::set_account_slug(account_slug.clone());
|
||||
if let Some(account_slug) = account_slug {
|
||||
Telemetry::set_account_slug(account_slug.clone());
|
||||
|
||||
analytics::identify(
|
||||
self.device_id.id.clone(),
|
||||
environment,
|
||||
release,
|
||||
Some(account_slug),
|
||||
);
|
||||
analytics::identify(release, Some(account_slug));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,13 +179,6 @@ fn main() -> Result<()> {
|
||||
None => device_id::get_or_create().context("Could not get `firezone_id` from CLI, could not read it from disk, could not generate it and save it to disk")?.id,
|
||||
};
|
||||
|
||||
analytics::identify(
|
||||
firezone_id.clone(),
|
||||
cli.api_url.to_string(),
|
||||
RELEASE.to_owned(),
|
||||
None,
|
||||
);
|
||||
|
||||
let mut telemetry = Telemetry::default();
|
||||
if cli.is_telemetry_allowed() {
|
||||
rt.block_on(telemetry.start(
|
||||
@@ -194,6 +187,8 @@ fn main() -> Result<()> {
|
||||
firezone_telemetry::HEADLESS_DSN,
|
||||
firezone_id.clone(),
|
||||
));
|
||||
|
||||
analytics::identify(RELEASE.to_owned(), None);
|
||||
}
|
||||
|
||||
tracing::info!(arch = std::env::consts::ARCH, version = VERSION);
|
||||
|
||||
@@ -4,8 +4,11 @@ use anyhow::{Context as _, Result, bail};
|
||||
use serde::Serialize;
|
||||
use sha2::Digest as _;
|
||||
|
||||
use crate::{Env, posthog::RUNTIME};
|
||||
use crate::{ApiUrl, Env, Telemetry, posthog::RUNTIME};
|
||||
|
||||
/// Records a `new_session` event for a particular user and API url.
|
||||
///
|
||||
/// This purposely does not use the existing telemetry session because we also want to capture sessions from self-hosted users.
|
||||
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))
|
||||
@@ -17,8 +20,10 @@ pub fn new_session(maybe_legacy_id: String, api_url: String) {
|
||||
if let Err(e) = capture(
|
||||
"new_session",
|
||||
distinct_id,
|
||||
api_url.clone(),
|
||||
NewSessionProperties { api_url },
|
||||
ApiUrl::new(&api_url),
|
||||
NewSessionProperties {
|
||||
api_url: api_url.clone(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -27,30 +32,23 @@ pub fn new_session(maybe_legacy_id: String, api_url: String) {
|
||||
});
|
||||
}
|
||||
|
||||
/// Associate several properties with a particular "distinct_id" in PostHog.
|
||||
pub fn identify(
|
||||
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()
|
||||
/// Associate several properties with the current telemetry user.
|
||||
pub fn identify(release: String, account_slug: Option<String>) {
|
||||
let Some(env) = Telemetry::current_env() else {
|
||||
tracing::debug!("Cannot send $identify: Unknown env");
|
||||
return;
|
||||
};
|
||||
let Some(distinct_id) = Telemetry::current_user() else {
|
||||
tracing::debug!("Cannot send $identify: Unknown user");
|
||||
return;
|
||||
};
|
||||
|
||||
RUNTIME.spawn({
|
||||
let distinct_id = distinct_id.clone();
|
||||
let api_url = api_url.clone();
|
||||
|
||||
async move {
|
||||
if let Err(e) = capture(
|
||||
"$identify",
|
||||
distinct_id,
|
||||
api_url,
|
||||
env,
|
||||
IdentifyProperties {
|
||||
set: PersonProperties {
|
||||
release,
|
||||
@@ -65,39 +63,20 @@ 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 is_legacy_id {
|
||||
RUNTIME.spawn(async move {
|
||||
if let Err(e) = capture(
|
||||
"$create_alias",
|
||||
distinct_id.clone(),
|
||||
api_url,
|
||||
CreateAliasProperties {
|
||||
alias: maybe_legacy_id,
|
||||
distinct_id,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::debug!("Failed to log `$create_alias` event: {e:#}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn capture<P>(
|
||||
event: impl Into<String>,
|
||||
distinct_id: String,
|
||||
api_url: String,
|
||||
env: impl Into<Env>,
|
||||
properties: P,
|
||||
) -> Result<()>
|
||||
where
|
||||
P: Serialize,
|
||||
{
|
||||
let event = event.into();
|
||||
let env = env.into();
|
||||
|
||||
let env = Env::from_api_url(&api_url);
|
||||
let Some(api_key) = crate::posthog::api_key_for_env(env) else {
|
||||
tracing::debug!(%event, %env, "Not sending event because we don't have an API key");
|
||||
|
||||
@@ -111,7 +90,7 @@ where
|
||||
.json(&CaptureRequest {
|
||||
api_key: api_key.to_string(),
|
||||
distinct_id,
|
||||
event,
|
||||
event: event.clone(),
|
||||
properties,
|
||||
})
|
||||
.send()
|
||||
@@ -126,6 +105,8 @@ where
|
||||
bail!("Failed to capture event; status={status}, body={body}")
|
||||
}
|
||||
|
||||
tracing::debug!(%event);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -156,9 +137,3 @@ struct PersonProperties {
|
||||
#[serde(rename = "$os")]
|
||||
os: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct CreateAliasProperties {
|
||||
distinct_id: String,
|
||||
alias: String,
|
||||
}
|
||||
|
||||
25
rust/telemetry/src/api_url.rs
Normal file
25
rust/telemetry/src/api_url.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) struct ApiUrl<'a>(&'a str);
|
||||
|
||||
impl ApiUrl<'static> {
|
||||
pub(crate) const PROD: Self = ApiUrl("wss://api.firezone.dev");
|
||||
pub(crate) const STAGING: Self = ApiUrl("wss://api.firez.one");
|
||||
pub(crate) const DOCKER_COMPOSE: Self = ApiUrl("ws://api:8081");
|
||||
pub(crate) const LOCALHOST: Self = ApiUrl("ws://localhost:8081");
|
||||
}
|
||||
|
||||
impl<'a> ApiUrl<'a> {
|
||||
pub(crate) fn new(url: &'a str) -> Self {
|
||||
Self(url.trim_end_matches("/"))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn trailing_slash_is_trimmed() {
|
||||
assert_eq!(ApiUrl::new("wss://api.firezone.dev/"), ApiUrl::PROD)
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
use std::{borrow::Cow, collections::BTreeMap, fmt, str::FromStr, sync::Arc, time::Duration};
|
||||
|
||||
use anyhow::{Result, bail};
|
||||
use api_url::ApiUrl;
|
||||
use sentry::{
|
||||
BeforeCallback, User,
|
||||
protocol::{Event, Log, LogAttribute, SessionStatus},
|
||||
@@ -13,6 +14,7 @@ pub mod analytics;
|
||||
pub mod feature_flags;
|
||||
pub mod otel;
|
||||
|
||||
mod api_url;
|
||||
mod posthog;
|
||||
|
||||
pub struct Dsn(&'static str);
|
||||
@@ -53,17 +55,18 @@ pub(crate) enum Env {
|
||||
OnPrem,
|
||||
}
|
||||
|
||||
impl Env {
|
||||
pub(crate) fn from_api_url(api_url: &str) -> Self {
|
||||
match api_url.trim_end_matches('/') {
|
||||
"wss://api.firezone.dev" => Self::Production,
|
||||
"wss://api.firez.one" => Self::Staging,
|
||||
"ws://api:8081" => Self::DockerCompose,
|
||||
"ws://localhost:8081" => Self::DockerCompose,
|
||||
impl From<ApiUrl<'_>> for Env {
|
||||
fn from(value: ApiUrl) -> Self {
|
||||
match value {
|
||||
ApiUrl::PROD => Self::Production,
|
||||
ApiUrl::STAGING => Self::Staging,
|
||||
ApiUrl::DOCKER_COMPOSE | ApiUrl::LOCALHOST => Self::DockerCompose,
|
||||
_ => Self::OnPrem,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Env {
|
||||
pub(crate) fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Env::Production => "production",
|
||||
@@ -116,7 +119,7 @@ impl Telemetry {
|
||||
pub async fn start(&mut self, api_url: &str, release: &str, dsn: Dsn, firezone_id: String) {
|
||||
// Can't use URLs as `environment` directly, because Sentry doesn't allow slashes in environments.
|
||||
// <https://docs.sentry.io/platforms/rust/configuration/environments/>
|
||||
let environment = Env::from_api_url(api_url);
|
||||
let environment = Env::from(ApiUrl::new(api_url));
|
||||
|
||||
if self
|
||||
.inner
|
||||
@@ -213,6 +216,18 @@ impl Telemetry {
|
||||
user.other.insert("account_slug".to_owned(), slug.into());
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn current_env() -> Option<Env> {
|
||||
let client = sentry::Hub::main().client()?;
|
||||
let env = client.options().environment.as_deref()?;
|
||||
let env = Env::from_str(env).ok()?;
|
||||
|
||||
Some(env)
|
||||
}
|
||||
|
||||
pub(crate) fn current_user() -> Option<String> {
|
||||
sentry::Hub::main().configure_scope(|s| s.user()?.id.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the [`User`] scope based on the contents of `firezone_id`.
|
||||
|
||||
Reference in New Issue
Block a user