posthog-rs 0.12.0

The official Rust client for Posthog (https://posthog.com/).
Documentation

PostHog Rust

Crates.io Documentation

The official Rust SDK for PostHog. See the PostHog docs for more information.

Features

  • Event capture - Send events to PostHog for product analytics
  • Feature flags - Evaluate feature flags with local or remote evaluation
  • Error tracking - Capture Rust errors with stack traces
  • A/B testing - Support for multivariate flags and experiments
  • Group analytics - Track events and flags for B2B use cases
  • Async and sync clients - Choose based on your runtime

Quickstart

Add posthog-rs to your Cargo.toml.

[dependencies]
posthog-rs = "$version"
let client = posthog_rs::client(env!("POSTHOG_API_KEY"));

// Capture events
let mut event = posthog_rs::Event::new("test", "1234");
event.insert_prop("key1", "value1").unwrap();
event.insert_prop("key2", vec!["a", "b"]).unwrap();

client.capture(event).unwrap();

// Check feature flags
let is_enabled = client.is_feature_enabled(
    "new-feature".to_string(),
    "user-123".to_string(),
    None,
    None,
    None,
).unwrap();

if is_enabled {
    println!("Feature is enabled!");
}

Error Tracking

Capture Rust errors manually with stack traces and send them to PostHog Error Tracking. Error tracking ships enabled by default (the error-tracking feature); opt out with default-features = false.

use posthog_rs::{client, CaptureExceptionOptions};

let client = client("your-api-key").await;
let error = std::io::Error::new(std::io::ErrorKind::Other, "checkout failed");

// Associate the exception with a person and attach optional context.
client.capture_exception_with(
    &error,
    CaptureExceptionOptions::new()
        .distinct_id("user-123")
        .property("route", "/checkout").unwrap()
        .fingerprint("checkout-error"),
).await.unwrap();

// Bare capture is personless.
client.capture_exception(&error).await.unwrap();

CaptureExceptionOptions carries everything optional: distinct_id to associate a person, custom properties, groups, a custom fingerprint, and a severity level (defaults to "error").

The stacktrace is captured at the call site (a bubbled-up Err carries no stack of its own); the error's type, message, and source() chain are always sent regardless. Configure stacktrace capture and in-app frame classification through ErrorTrackingOptionsBuilder on ClientOptions::error_tracking — in-app patterns match both file paths and function symbols, so a crate prefix like "other_crate::" marks that crate's frames as library code:

use posthog_rs::{ClientOptionsBuilder, ErrorTrackingOptionsBuilder};

let options = ClientOptionsBuilder::default()
    .api_key("your-api-key".to_string())
    .error_tracking(
        ErrorTrackingOptionsBuilder::default()
            .in_app_exclude_paths(vec!["other_crate::".to_string()])
            .build()
            .unwrap(),
    )
    .build()
    .unwrap();

Captured properties

Property Contents
$exception_list One entry per error in the source() chain: type, message, and mechanism, with the captured stacktrace attached to the first entry.
$exception_level "error" unless set via CaptureExceptionOptions::level.
$exception_fingerprint Only present when set via CaptureExceptionOptions::fingerprint; overrides server-side issue grouping.

Feature Flags

The SDK now supports PostHog feature flags, allowing you to control feature rollout and run A/B tests.

Basic Usage

use posthog_rs::{client, ClientOptions, FlagValue};
use std::collections::HashMap;
use serde_json::json;

let client = client(ClientOptions::from("your-api-key"));

// Check if a feature is enabled
let is_enabled = client.is_feature_enabled(
    "feature-key".to_string(),
    "user-id".to_string(),
    None, None, None
).unwrap();

// Get feature flag value (boolean or variant)
match client.get_feature_flag(
    "feature-key".to_string(),
    "user-id".to_string(),
    None, None, None
).unwrap() {
    Some(FlagValue::Boolean(enabled)) => println!("Flag is: {}", enabled),
    Some(FlagValue::String(variant)) => println!("Variant: {}", variant),
    None => println!("Flag is disabled"),
}

With Properties

// Include person properties for flag evaluation
let mut person_props = HashMap::new();
person_props.insert("plan".to_string(), json!("enterprise"));
person_props.insert("country".to_string(), json!("US"));

let flag = client.get_feature_flag(
    "premium-feature".to_string(),
    "user-id".to_string(),
    None,
    Some(person_props),
    None
).unwrap();

With Groups (B2B)

// For B2B apps with group-based flags
let mut groups = HashMap::new();
groups.insert("company".to_string(), "company-123".to_string());

let mut group_props = HashMap::new();
let mut company_props = HashMap::new();
company_props.insert("size".to_string(), json!(500));
group_props.insert("company".to_string(), company_props);

let flag = client.get_feature_flag(
    "b2b-feature".to_string(),
    "user-id".to_string(),
    Some(groups),
    None,
    Some(group_props)
).unwrap();

Get All Flags

// Get all feature flags for a user
let (feature_flags, _payloads) = client.get_feature_flags(
    "user-id".to_string(),
    None, None, None
).unwrap();

for (key, value) in feature_flags {
    println!("Flag {}: {:?}", key, value);
}

Feature Flag Payloads

// Get additional data associated with a feature flag
let payload = client.get_feature_flag_payload(
    "onboarding-flow".to_string(),
    "user-id".to_string()
).unwrap();

if let Some(data) = payload {
    println!("Payload: {}", data);
}

Error Handling

In production code, handle errors properly instead of using .unwrap():

use posthog_rs::{client, Error, FlagValue};

let client = client("your-api-key");

// Pattern 1: Match on the error type
match client.get_feature_flag("my-flag", "user-123", None, None, None) {
    Ok(Some(FlagValue::Boolean(true))) => {
        // Flag is enabled
        enable_feature();
    }
    Ok(Some(FlagValue::String(variant))) => {
        // Multivariate flag - use the variant
        use_variant(&variant);
    }
    Ok(Some(FlagValue::Boolean(false))) | Ok(None) => {
        // Flag is disabled or doesn't exist
        use_default_behavior();
    }
    Err(Error::Connection(e)) => {
        // Network error - maybe use cached value or default
        eprintln!("Failed to fetch flag: {}", e);
        use_default_behavior();
    }
    Err(Error::InconclusiveMatch(reason)) => {
        // Local evaluation couldn't determine flag value
        // (missing properties, unknown cohort, etc.)
        eprintln!("Inconclusive evaluation: {}", reason);
        use_default_behavior();
    }
    Err(e) => {
        // Other errors
        eprintln!("Unexpected error: {}", e);
        use_default_behavior();
    }
}

// Pattern 2: Use unwrap_or for simple defaults
let is_enabled = client
    .is_feature_enabled("my-flag", "user-123", None, None, None)
    .unwrap_or(false);  // Default to disabled on error

// Pattern 3: Propagate errors with ?
fn check_feature(client: &posthog_rs::Client, user_id: &str) -> Result<bool, Error> {
    let flag = client.is_feature_enabled("premium-feature", user_id, None, None, None)?;
    Ok(flag)
}

Observability

The SDK uses tracing for structured logging. To see logs, add a tracing subscriber to your application:

use tracing_subscriber::{fmt, EnvFilter};

// Initialize tracing (e.g., in main.rs)
tracing_subscriber::fmt()
    .with_env_filter(EnvFilter::from_default_env())
    .init();

Then set the RUST_LOG environment variable to control log levels:

# See all posthog logs
RUST_LOG=posthog_rs=debug cargo run

# See only warnings and errors
RUST_LOG=posthog_rs=warn cargo run

# See trace-level logs for flag evaluation
RUST_LOG=posthog_rs=trace cargo run

Log levels:

  • error: Connection failures, HTTP errors
  • warn: Configuration issues, failed flag fetches
  • info: Client initialization, poller start/stop
  • debug: Flag evaluation results, API fallback decisions
  • trace: Detailed cache updates, individual flag lookups

For release instructions, see RELEASING.md.

Contributing

See CONTRIBUTING.md for local development and verification commands.

Acknowledgements

Thanks to @christos-h for building the initial version of this project.