fix(telemetry): introduce staging and prod PostHog projects (#8647)

As per PostHog's recommendation [0], we now use different projects to
manage the feature-flags. This allows us to turn feature flags in
staging or production on / off without affecting the other.

[0]: https://posthog.com/tutorials/multiple-environments
This commit is contained in:
Thomas Eizinger
2025-04-04 01:56:28 +00:00
committed by GitHub
parent ebb71e0f54
commit 3ce3c03291
2 changed files with 29 additions and 27 deletions

View File

@@ -1,11 +1,12 @@
use std::{borrow::Cow, sync::LazyLock, time::Duration};
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: &str = "phc_uXXl56plyvIBHj81WwXBLtdPElIRbm7keRTdUCmk8ll";
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);
static RUNTIME: LazyLock<Runtime> = LazyLock::new(init_runtime);
@@ -23,9 +24,15 @@ pub fn drop_llmnr_nxdomain_responses() -> bool {
FEATURE_FLAGS.read().drop_llmnr_nxdomain_responses
}
pub(crate) fn reevaluate(user_id: String, env: Option<Cow<'static, str>>) {
pub(crate) fn reevaluate(user_id: String, env: &str) {
let api_key = match env {
crate::env::PRODUCTION => POSTHOG_API_KEY_PROD,
crate::env::STAGING => POSTHOG_API_KEY_STAGING,
_ => return,
};
RUNTIME.spawn(async move {
let flags = decide(user_id, env)
let flags = decide(user_id, api_key.to_owned())
.await
.inspect_err(|e| tracing::debug!("Failed to evaluate feature flags: {e:#}"))
.unwrap_or_default();
@@ -58,31 +65,31 @@ fn init_runtime() -> Runtime {
continue;
};
let Some(env) = client.options().environment.as_ref() else {
continue; // Nothing to do if we don't have an environment set.
};
let Some(user_id) = sentry::Hub::main()
.configure_scope(|scope| scope.user().and_then(|u| u.id.clone()))
else {
continue; // Nothing to do if we don't have a user-id set.
};
reevaluate(user_id, client.options().environment.to_owned());
reevaluate(user_id, env);
}
});
runtime
}
async fn decide(
distinct_id: String,
environment: Option<Cow<'static, str>>,
) -> Result<FeatureFlags> {
async fn decide(distinct_id: String, api_key: String) -> Result<FeatureFlags> {
let response = reqwest::ClientBuilder::new()
.connection_verbose(true)
.build()?
.post("https://us.i.posthog.com/decide?v=3")
.json(&DecideRequest {
api_key: POSTHOG_API_KEY.to_string(),
api_key,
distinct_id,
groups: Groups { environment },
})
.send()
.await
@@ -108,13 +115,6 @@ async fn decide(
struct DecideRequest {
api_key: String,
distinct_id: String,
groups: Groups,
}
#[derive(Debug, Serialize)]
struct Groups {
#[serde(skip_serializing_if = "Option::is_none")]
environment: Option<Cow<'static, str>>,
}
#[derive(Debug, Deserialize)]

View File

@@ -1,6 +1,6 @@
#![cfg_attr(test, allow(clippy::unwrap_used))]
use std::{sync::Arc, time::Duration};
use std::{borrow::Cow, sync::Arc, time::Duration};
use env::ON_PREM;
use sentry::protocol::SessionStatus;
@@ -37,11 +37,9 @@ pub const TESTING: Dsn = Dsn(
);
mod env {
use std::borrow::Cow;
pub const PRODUCTION: Cow<'static, str> = Cow::Borrowed("production");
pub const STAGING: Cow<'static, str> = Cow::Borrowed("staging");
pub const ON_PREM: Cow<'static, str> = Cow::Borrowed("on-prem");
pub const PRODUCTION: &str = "production";
pub const STAGING: &str = "staging";
pub const ON_PREM: &str = "on-prem";
}
#[derive(Default)]
@@ -74,7 +72,7 @@ impl Telemetry {
.inner
.as_ref()
.and_then(|i| i.options().environment.as_ref())
.is_some_and(|env| env == &environment)
.is_some_and(|env| env == environment)
{
tracing::debug!(%environment, "Telemetry already initialised");
@@ -101,7 +99,7 @@ impl Telemetry {
let inner = sentry::init((
dsn.0,
sentry::ClientOptions {
environment: Some(environment),
environment: Some(Cow::Borrowed(environment)),
// We can't get the release number ourselves because we don't know if we're embedded in a GUI Client or a Headless Client.
release: Some(release.to_owned().into()),
traces_sampler: Some(Arc::new(|tx| {
@@ -173,7 +171,11 @@ impl Telemetry {
return;
};
feature_flags::reevaluate(id, client.options().environment.to_owned());
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);
}
}