Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
```
71 changes: 69 additions & 2 deletions src/client/async_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,20 @@ pub async fn client<C: Into<ClientOptions>>(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()))?;
Expand All @@ -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<Event>) -> 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 =
Expand All @@ -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());
}
}
71 changes: 69 additions & 2 deletions src/client/blocking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,20 @@ pub fn client<C: Into<ClientOptions>>(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()))?;
Expand All @@ -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<Event>) -> 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 =
Expand All @@ -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());
}
}
13 changes: 11 additions & 2 deletions src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,

#[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")
}
Expand Down