diff --git a/api/public-api.txt b/api/public-api.txt index 5ad25c66..b80542a3 100644 --- a/api/public-api.txt +++ b/api/public-api.txt @@ -156,16 +156,17 @@ pub fn posthog_rs::ClientOptionsBuilder::local_evaluation_only(&mut self, bool) pub fn posthog_rs::ClientOptionsBuilder::max_batch_size(&mut self, usize) -> &mut Self pub fn posthog_rs::ClientOptionsBuilder::max_capture_attempts(&mut self, u32) -> &mut Self pub fn posthog_rs::ClientOptionsBuilder::max_queue_size(&mut self, usize) -> &mut Self -pub fn posthog_rs::ClientOptionsBuilder::personal_api_key>(&mut self, VALUE) -> &mut Self pub fn posthog_rs::ClientOptionsBuilder::poll_interval_seconds(&mut self, u64) -> &mut Self pub fn posthog_rs::ClientOptionsBuilder::request_timeout_seconds(&mut self, u64) -> &mut Self pub fn posthog_rs::ClientOptionsBuilder::retry_initial_backoff_ms(&mut self, u64) -> &mut Self pub fn posthog_rs::ClientOptionsBuilder::retry_max_backoff_ms(&mut self, u64) -> &mut Self +pub fn posthog_rs::ClientOptionsBuilder::secret_key>(&mut self, VALUE) -> &mut Self pub fn posthog_rs::ClientOptionsBuilder::shutdown_timeout_ms(&mut self, u64) -> &mut Self impl posthog_rs::ClientOptionsBuilder pub fn posthog_rs::ClientOptionsBuilder::before_send(&mut self, F) -> &mut Self where F: core::ops::function::FnMut(posthog_rs::Event) -> core::option::Option + core::marker::Send + 'static pub fn posthog_rs::ClientOptionsBuilder::build(&self) -> core::result::Result pub fn posthog_rs::ClientOptionsBuilder::on_error(&mut self, F) -> &mut Self where F: core::ops::function::Fn(&posthog_rs::PostHogError<'_>) + core::marker::Send + core::marker::Sync + 'static +pub fn posthog_rs::ClientOptionsBuilder::personal_api_key>(&mut self, VALUE) -> &mut Self impl core::default::Default for posthog_rs::ClientOptionsBuilder pub fn posthog_rs::ClientOptionsBuilder::default() -> Self pub struct posthog_rs::Cohort diff --git a/examples/advanced_config.rs b/examples/advanced_config.rs index 3e4ad30d..7158713b 100644 --- a/examples/advanced_config.rs +++ b/examples/advanced_config.rs @@ -41,7 +41,7 @@ async fn main() -> Result<(), Box> { println!("5. High-performance with local evaluation:"); let performance_config = ClientOptionsBuilder::default() .api_key("phc_project_key".to_string()) - .personal_api_key("phx_personal_key") // Required for local eval + .secret_key("phx_personal_key") // Required for local eval .enable_local_evaluation(true) // Cache flags locally .poll_interval_seconds(30) // Update cache every 30s .feature_flags_request_timeout_seconds(3) diff --git a/examples/local_evaluation.rs b/examples/local_evaluation.rs index 1508f06b..6d3eb7ca 100644 --- a/examples/local_evaluation.rs +++ b/examples/local_evaluation.rs @@ -53,7 +53,7 @@ async fn main() { let local_client = { let options = ClientOptionsBuilder::default() .api_key(api_key.clone()) - .personal_api_key(personal_key) + .secret_key(personal_key) .enable_local_evaluation(true) .poll_interval_seconds(30) // Poll for updates every 30 seconds .build() diff --git a/src/client/async_client.rs b/src/client/async_client.rs index 87129c68..e6483e4b 100644 --- a/src/client/async_client.rs +++ b/src/client/async_client.rs @@ -130,32 +130,33 @@ pub async fn client>(options: C) -> Client { .build() .unwrap(); // Unwrap here is as safe as `HttpClient::new` - let (local_evaluator, flag_poller) = if options.enable_local_evaluation - && !options.is_disabled() - { - if let Some(ref personal_key) = options.personal_api_key { - let cache = FlagCache::new(); - - let config = LocalEvaluationConfig { - personal_api_key: personal_key.clone(), - project_api_key: options.api_key.clone(), - api_host: options.endpoints().api_host(), - poll_interval: Duration::from_secs(options.poll_interval_seconds), - request_timeout: Duration::from_secs(options.request_timeout_seconds), - }; - - let mut poller = AsyncFlagPoller::new(config, cache.clone()); - poller.set_on_error(options.on_error.clone()); - poller.start().await; - - (Some(LocalEvaluator::new(cache)), Some(poller)) + let (local_evaluator, flag_poller) = + if options.enable_local_evaluation && !options.is_disabled() { + if let Some(ref secret_key) = options.secret_key { + let cache = FlagCache::new(); + + let config = LocalEvaluationConfig { + personal_api_key: secret_key.clone(), + project_api_key: options.api_key.clone(), + api_host: options.endpoints().api_host(), + poll_interval: Duration::from_secs(options.poll_interval_seconds), + request_timeout: Duration::from_secs(options.request_timeout_seconds), + }; + + let mut poller = AsyncFlagPoller::new(config, cache.clone()); + poller.set_on_error(options.on_error.clone()); + poller.start().await; + + (Some(LocalEvaluator::new(cache)), Some(poller)) + } else { + warn!( + "Local evaluation enabled but secret_key not set, falling back to API evaluation" + ); + (None, None) + } } else { - warn!("Local evaluation enabled but personal_api_key not set, falling back to API evaluation"); (None, None) - } - } else { - (None, None) - }; + }; let transport = if options.is_disabled() { None diff --git a/src/client/blocking.rs b/src/client/blocking.rs index ded2cd16..82af10bb 100644 --- a/src/client/blocking.rs +++ b/src/client/blocking.rs @@ -134,32 +134,33 @@ pub fn client>(options: C) -> Client { .build() .unwrap(); // Unwrap here is as safe as `HttpClient::new` - let (local_evaluator, flag_poller) = if options.enable_local_evaluation - && !options.is_disabled() - { - if let Some(ref personal_key) = options.personal_api_key { - let cache = FlagCache::new(); - - let config = LocalEvaluationConfig { - personal_api_key: personal_key.clone(), - project_api_key: options.api_key.clone(), - api_host: options.endpoints().api_host(), - poll_interval: Duration::from_secs(options.poll_interval_seconds), - request_timeout: Duration::from_secs(options.request_timeout_seconds), - }; - - let mut poller = FlagPoller::new(config, cache.clone()); - poller.set_on_error(options.on_error.clone()); - poller.start(); - - (Some(LocalEvaluator::new(cache)), Some(poller)) + let (local_evaluator, flag_poller) = + if options.enable_local_evaluation && !options.is_disabled() { + if let Some(ref secret_key) = options.secret_key { + let cache = FlagCache::new(); + + let config = LocalEvaluationConfig { + personal_api_key: secret_key.clone(), + project_api_key: options.api_key.clone(), + api_host: options.endpoints().api_host(), + poll_interval: Duration::from_secs(options.poll_interval_seconds), + request_timeout: Duration::from_secs(options.request_timeout_seconds), + }; + + let mut poller = FlagPoller::new(config, cache.clone()); + poller.set_on_error(options.on_error.clone()); + poller.start(); + + (Some(LocalEvaluator::new(cache)), Some(poller)) + } else { + warn!( + "Local evaluation enabled but secret_key not set, falling back to API evaluation" + ); + (None, None) + } } else { - warn!("Local evaluation enabled but personal_api_key not set, falling back to API evaluation"); (None, None) - } - } else { - (None, None) - }; + }; let transport = if options.is_disabled() { None diff --git a/src/client/mod.rs b/src/client/mod.rs index 732aea0b..6db8b39b 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -135,10 +135,12 @@ pub struct ClientOptions { #[builder(default = "30")] request_timeout_seconds: u64, - /// Personal API key for fetching flag definitions. Required when - /// `enable_local_evaluation` is `true`. + /// Secret key used for local feature flag evaluation and remote config. + /// + /// Accepts either a Personal API Key (`phx_...`) or a Project Secret API + /// Key (`phs_...`). Required when `enable_local_evaluation` is `true`. #[builder(setter(into, strip_option), default)] - personal_api_key: Option, + secret_key: Option, /// Enable local evaluation of feature flags using a background definitions /// poller. @@ -321,8 +323,8 @@ impl ClientOptions { } None => DEFAULT_HOST.to_string(), }); - self.personal_api_key = self.personal_api_key.and_then(|personal_api_key| { - let normalized = personal_api_key.trim().to_string(); + self.secret_key = self.secret_key.and_then(|secret_key| { + let normalized = secret_key.trim().to_string(); if normalized.is_empty() { None } else { @@ -398,6 +400,18 @@ impl ClientOptionsBuilder { pub fn build(&self) -> Result { Ok(self.build_unchecked()?.sanitize()) } + + /// Deprecated alias for [`secret_key`](Self::secret_key). + /// + /// Kept for backwards compatibility; forwards to `secret_key`. The last + /// builder call wins if both are set. + #[deprecated( + note = "use `secret_key` instead; it accepts a Personal API Key or a Project Secret API Key" + )] + pub fn personal_api_key>(&mut self, value: VALUE) -> &mut Self { + self.secret_key = Some(Some(value.into())); + self + } } impl From<&str> for ClientOptions { @@ -431,16 +445,49 @@ mod tests { let options = ClientOptionsBuilder::default() .api_key(" \n test-api-key\t ".to_string()) .host(" \nhttps://eu.posthog.com/\t ") - .personal_api_key(" \n\t ") + .secret_key(" \n\t ") .build() .unwrap(); assert_eq!(options.api_key, "test-api-key"); assert_eq!(options.host.as_deref(), Some("https://eu.posthog.com/")); - assert_eq!(options.personal_api_key, None); + assert_eq!(options.secret_key, None); assert_eq!(options.endpoints().api_host(), EU_INGESTION_ENDPOINT); } + #[test] + #[allow(deprecated)] + fn personal_api_key_forwards_to_secret_key_last_call_wins() { + let resolve = |calls: &[(&str, &str)]| { + let mut builder = ClientOptionsBuilder::default(); + builder.api_key("test-api-key".to_string()); + for (which, val) in calls { + match *which { + "secret" => builder.secret_key(*val), + _ => builder.personal_api_key(*val), + }; + } + builder.build().unwrap().secret_key + }; + + assert_eq!( + resolve(&[("secret", "phs_secret")]).as_deref(), + Some("phs_secret") + ); + assert_eq!( + resolve(&[("personal", "phx_personal")]).as_deref(), + Some("phx_personal") + ); + assert_eq!( + resolve(&[("personal", "phx_personal"), ("secret", "phs_secret")]).as_deref(), + Some("phs_secret") + ); + assert_eq!( + resolve(&[("secret", "phs_secret"), ("personal", "phx_personal")]).as_deref(), + Some("phx_personal") + ); + } + #[test] fn defaults_blank_host_after_trimming_whitespace() { let options = ClientOptionsBuilder::default() diff --git a/tests/test_local_evaluation.rs b/tests/test_local_evaluation.rs index e3e28d00..d93bb7c7 100644 --- a/tests/test_local_evaluation.rs +++ b/tests/test_local_evaluation.rs @@ -209,7 +209,7 @@ async fn test_local_evaluation_with_mock_server() { let options = ClientOptionsBuilder::default() .host(server.base_url()) .api_key("test_project_key".to_string()) - .personal_api_key("test_personal_key".to_string()) + .secret_key("test_personal_key".to_string()) .enable_local_evaluation(true) .poll_interval_seconds(60) .build()