diff --git a/Cargo.toml b/Cargo.toml index d7056c6..c9e696c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ uuid = { version = "1.13.2", features = ["serde", "v7"] } [dev-dependencies] dotenv = "0.15.0" ctor = "0.1.26" +tokio = { version = "1.47.1", features = ["rt", "macros"] } [features] default = ["async-client"] diff --git a/README.md b/README.md index 3fd7966..d8189d9 100644 --- a/README.md +++ b/README.md @@ -22,3 +22,21 @@ event.insert_prop("key2", vec!["a", "b"]).unwrap(); client.capture(event).unwrap(); ``` + +## Disabled Client + +The client can be initialized without an API key, which creates a disabled client. This is useful for development environments or when you need to conditionally disable event tracking (e.g., based on user privacy settings). + +```rust +// Create a disabled client (no API key). +let client = posthog_rs::client(posthog_rs::ClientOptions::default()); + +// Events can be captured but won't be sent to PostHog. +let event = posthog_rs::Event::new("test", "1234"); +client.capture(event).unwrap(); // Returns Ok(()) without sending anything. + +// Check if client is disabled. +if client.is_disabled() { + println!("Client is disabled - events will not be sent"); +} +``` diff --git a/src/client/async_client.rs b/src/client/async_client.rs index 56139fb..c1ee404 100644 --- a/src/client/async_client.rs +++ b/src/client/async_client.rs @@ -23,9 +23,20 @@ pub async fn client>(options: C) -> Client { } impl Client { + /// Returns true if this client is disabled (has no API key). + pub fn is_disabled(&self) -> bool { + self.options.api_key.is_none() + } + /// Capture the provided event, sending it to PostHog. + /// If the client is disabled (no API key), this method returns Ok(()) without sending anything. pub async fn capture(&self, event: Event) -> Result<(), Error> { - let inner_event = InnerEvent::new(event, self.options.api_key.clone()); + if self.is_disabled() { + return Ok(()); + } + + let api_key = self.options.api_key.as_ref().unwrap(); + let inner_event = InnerEvent::new(event, api_key.clone()); let payload = serde_json::to_string(&inner_event).map_err(|e| Error::Serialization(e.to_string()))?; @@ -43,10 +54,16 @@ impl Client { /// Capture a collection of events with a single request. This function may be /// more performant than capturing a list of events individually. + /// If the client is disabled (no API key), this method returns Ok(()) without sending anything. pub async fn capture_batch(&self, events: Vec) -> Result<(), Error> { + if self.is_disabled() { + return Ok(()); + } + + let api_key = self.options.api_key.as_ref().unwrap(); let events: Vec<_> = events .into_iter() - .map(|event| InnerEvent::new(event, self.options.api_key.clone())) + .map(|event| InnerEvent::new(event, api_key.clone())) .collect(); let payload = @@ -63,3 +80,53 @@ impl Client { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ClientOptionsBuilder, Event}; + + #[tokio::test] + async fn test_client_without_api_key_is_disabled() { + let options = ClientOptions::default(); + + let client = client(options).await; + + assert!(client.is_disabled()); + } + + #[tokio::test] + async fn test_client_with_api_key_is_enabled() { + let options = ClientOptionsBuilder::default() + .api_key(Some("test_key".to_string())) + .build() + .unwrap(); + + let client = client(options).await; + + assert!(!client.is_disabled()); + } + + #[tokio::test] + async fn test_disabled_client_capture_returns_ok() { + let client = client(ClientOptions::default()).await; + let event = Event::new("test_event", "user_123"); + + let result = client.capture(event).await; + + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_disabled_client_capture_batch_returns_ok() { + let client = client(ClientOptions::default()).await; + let events = vec![ + Event::new("test_event1", "user_123"), + Event::new("test_event2", "user_456"), + ]; + + let result = client.capture_batch(events).await; + + assert!(result.is_ok()); + } +} diff --git a/src/client/blocking.rs b/src/client/blocking.rs index 0f9af91..4740c48 100644 --- a/src/client/blocking.rs +++ b/src/client/blocking.rs @@ -23,9 +23,20 @@ pub fn client>(options: C) -> Client { } impl Client { + /// Returns true if this client is disabled (has no API key). + pub fn is_disabled(&self) -> bool { + self.options.api_key.is_none() + } + /// Capture the provided event, sending it to PostHog. + /// If the client is disabled (no API key), this method returns Ok(()) without sending anything. pub fn capture(&self, event: Event) -> Result<(), Error> { - let inner_event = InnerEvent::new(event, self.options.api_key.clone()); + if self.is_disabled() { + return Ok(()); + } + + let api_key = self.options.api_key.as_ref().unwrap(); + let inner_event = InnerEvent::new(event, api_key.clone()); let payload = serde_json::to_string(&inner_event).map_err(|e| Error::Serialization(e.to_string()))?; @@ -42,10 +53,16 @@ impl Client { /// Capture a collection of events with a single request. This function may be /// more performant than capturing a list of events individually. + /// If the client is disabled (no API key), this method returns Ok(()) without sending anything. pub fn capture_batch(&self, events: Vec) -> Result<(), Error> { + if self.is_disabled() { + return Ok(()); + } + + let api_key = self.options.api_key.as_ref().unwrap(); let events: Vec<_> = events .into_iter() - .map(|event| InnerEvent::new(event, self.options.api_key.clone())) + .map(|event| InnerEvent::new(event, api_key.clone())) .collect(); let payload = @@ -61,3 +78,53 @@ impl Client { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ClientOptionsBuilder, Event}; + + #[test] + fn test_client_without_api_key_is_disabled() { + let options = ClientOptions::default(); + + let client = client(options); + + assert!(client.is_disabled()); + } + + #[test] + fn test_client_with_api_key_is_enabled() { + let options = ClientOptionsBuilder::default() + .api_key(Some("test_key".to_string())) + .build() + .unwrap(); + + let client = client(options); + + assert!(!client.is_disabled()); + } + + #[test] + fn test_disabled_client_capture_returns_ok() { + let client = client(ClientOptions::default()); + let event = Event::new("test_event", "user_123"); + + let result = client.capture(event); + + assert!(result.is_ok()); + } + + #[test] + fn test_disabled_client_capture_batch_returns_ok() { + let client = client(ClientOptions::default()); + let events = vec![ + Event::new("test_event1", "user_123"), + Event::new("test_event2", "user_456"), + ]; + + let result = client.capture_batch(events); + + assert!(result.is_ok()); + } +} diff --git a/src/client/mod.rs b/src/client/mod.rs index a60d772..dd8aabd 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -19,16 +19,25 @@ pub use async_client::Client; pub struct ClientOptions { #[builder(default = "API_ENDPOINT.to_string()")] api_endpoint: String, - api_key: String, + #[builder(default = "None")] + api_key: Option, #[builder(default = "30")] request_timeout_seconds: u64, } +impl Default for ClientOptions { + fn default() -> Self { + ClientOptionsBuilder::default() + .build() + .expect("Default ClientOptions should always build successfully") + } +} + impl From<&str> for ClientOptions { fn from(api_key: &str) -> Self { ClientOptionsBuilder::default() - .api_key(api_key.to_string()) + .api_key(Some(api_key.to_string())) .build() .expect("We always set the API key, so this is infallible") }