Skip to content
Open
Show file tree
Hide file tree
Changes from 68 commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
0e0735e
initial commit
dmarticus Sep 18, 2025
c2f2045
initial commit
dmarticus Sep 18, 2025
0b35172
adding config options
dmarticus Sep 18, 2025
daa1a6b
clean up the examples
dmarticus Sep 18, 2025
85a3397
use the config
dmarticus Sep 18, 2025
d134568
clean uo
dmarticus Sep 18, 2025
5f1ba8e
clean up example
dmarticus Sep 18, 2025
c64d749
clean up
dmarticus Sep 18, 2025
cb879d3
fix tests
dmarticus Sep 18, 2025
b58eb9a
fmt you dawg
dmarticus Sep 18, 2025
99c49e1
clippy you dawg
dmarticus Sep 18, 2025
0037463
fmt you dawg
dmarticus Sep 18, 2025
6ce06b8
codeql error/warning docs
iamnamananand996 Oct 29, 2025
541fd41
feat(flags): add support for `$feature_flag_called`
iamnamananand996 Oct 30, 2025
670ed45
feat(flags): add e2e tests
iamnamananand996 Oct 30, 2025
ecbee59
chore(rust): refactor error handling
iamnamananand996 Nov 2, 2025
9f8bb35
chore: config system
iamnamananand996 Nov 3, 2025
8ee53f7
chore: not required tests
iamnamananand996 Nov 3, 2025
2e7c4e6
chore: simplified comments
iamnamananand996 Nov 3, 2025
169d0ff
chore: clean up
iamnamananand996 Nov 3, 2025
e1c287f
chore: clean up and tests
iamnamananand996 Nov 3, 2025
c8d9f16
chore: make is url params generic
iamnamananand996 Nov 3, 2025
7070d53
chore: before after example
iamnamananand996 Nov 4, 2025
496efd5
chore: sync/async disable
iamnamananand996 Nov 4, 2025
be0b8c9
chore: update examples
iamnamananand996 Nov 4, 2025
0534cee
readme update
iamnamananand996 Nov 4, 2025
339ef1f
local_evaluation
iamnamananand996 Nov 4, 2025
922a05f
chore: Error Classification Example
iamnamananand996 Nov 4, 2025
fe95928
updated CHANGELOG and README
aditya-arolkar-swe Nov 5, 2025
af552bc
updated changelog
aditya-arolkar-swe Nov 5, 2025
dbda0f8
updated readme and changelog
aditya-arolkar-swe Nov 5, 2025
53ff674
updated changelog and readme
aditya-arolkar-swe Nov 5, 2025
bfcdea8
updated semver version
aditya-arolkar-swe Nov 5, 2025
00d6a6a
minor
aditya-arolkar-swe Nov 5, 2025
bb80f1a
updated changelog to reflect dependencies
aditya-arolkar-swe Nov 5, 2025
8ebc638
minor formatting
aditya-arolkar-swe Nov 5, 2025
4a913ab
minor
aditya-arolkar-swe Nov 5, 2025
2633ff0
example update
iamnamananand996 Nov 5, 2025
f6bba70
Update README.md
fern-support Nov 12, 2025
9f2711a
trigger codeql
iamnamananand996 Nov 12, 2025
5e666be
fix: clippy checks
iamnamananand996 Nov 12, 2025
eb766bc
trigger codeql
iamnamananand996 Nov 12, 2025
cfe44a0
trigger codeql
iamnamananand996 Nov 12, 2025
a57c383
trigger codeql
iamnamananand996 Nov 12, 2025
2f27b65
trigger codeql
iamnamananand996 Nov 12, 2025
b4ede78
clippy check
iamnamananand996 Nov 12, 2025
390a3aa
chore: clippy checks
iamnamananand996 Nov 13, 2025
9bce1d6
chore: clippy checks
iamnamananand996 Nov 13, 2025
4c95c8e
clippy check
iamnamananand996 Nov 13, 2025
6bfbf75
chore: remove Feature Flag Events Example
iamnamananand996 Nov 13, 2025
b311797
chore: use `AtomicBool` over `RwLocks` forced &mut for lock-free bool…
iamnamananand996 Nov 13, 2025
007db74
chore: only support `V2` for feature flags
iamnamananand996 Nov 13, 2025
f65c5be
resolve warnings
iamnamananand996 Nov 13, 2025
8af4d6e
chore: reduce exported `structs` for public API, move units tests to …
iamnamananand996 Nov 13, 2025
c98d7ab
Merge pull request #1 from fern-api/namananand/fer-7476-posthog-sdk-i…
aditya-arolkar-swe Nov 13, 2025
5460491
Merge branch 'main' into namananand/fer-7545-posthog-sdk-config-system
aditya-arolkar-swe Nov 13, 2025
7ac7c64
allow deprecated error
iamnamananand996 Nov 13, 2025
ddb9c27
Merge pull request #2 from fern-api/namananand/fer-7545-posthog-sdk-c…
aditya-arolkar-swe Nov 13, 2025
9fda9c6
chore: avoid nasty match unwrapping
iamnamananand996 Nov 13, 2025
a936fd9
chore: refactor `FeatureFlagsResponse `
iamnamananand996 Nov 14, 2025
baa0bf5
cargo fmt
iamnamananand996 Nov 14, 2025
c047599
fix: disable codeql error
iamnamananand996 Nov 14, 2025
8a39ee0
chore: inline disable
iamnamananand996 Nov 14, 2025
a13e59a
chore: inline disable
iamnamananand996 Nov 14, 2025
32a8f15
Merge branch 'main' of https://github.com/fern-api/posthog-rs-fern in…
iamnamananand996 Nov 14, 2025
eae0a19
chore: align config, error and enpoints with new changes
iamnamananand996 Nov 14, 2025
b4db66f
trigger codeql
iamnamananand996 Nov 14, 2025
a16e32d
Merge pull request #3 from fern-api/feat/feature-flags
aditya-arolkar-swe Nov 14, 2025
ae2aa84
chore: backward compatibility for `ClientOptionsBuilderError` type
iamnamananand996 Nov 18, 2025
3091216
Merge pull request #6 from fern-api/backward-compatibity-error-export
iamnamananand996 Nov 20, 2025
bf8b06d
chore: make config accessibility more strict
iamnamananand996 Nov 20, 2025
6c46616
Merge branch 'main' of https://github.com/fern-api/posthog-rs-fern in…
iamnamananand996 Nov 20, 2025
89738ff
clippy fix
iamnamananand996 Nov 20, 2025
7cdcc2b
chore: update error with new classification
iamnamananand996 Nov 20, 2025
523b78c
remove underscore
iamnamananand996 Nov 20, 2025
b158972
chore: fallback the api silently
iamnamananand996 Nov 20, 2025
5e4df17
remove `_`
iamnamananand996 Nov 20, 2025
5bb7c23
add check for enable_local_evaluation to true
iamnamananand996 Nov 20, 2025
71c605a
chore: make `capture_feature_flag_called` return result
iamnamananand996 Nov 20, 2025
4acf50a
Merge pull request #7 from fern-api/feedback-fix
aditya-arolkar-swe Nov 21, 2025
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
41 changes: 41 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,44 @@
## 0.6.0 - 2025-11-05

### Added

- Feature flags support (boolean, multivariate, payloads)
- Local evaluation for 100-1000x faster flag evaluation with background polling
- Automatic `$feature_flag_called` event tracking with deduplication
- Property-based targeting and group (B2B) support
- New methods: `is_feature_enabled()`, `get_feature_flag()`, `get_feature_flags()`, `get_feature_flag_payload()`

#### New Dependencies:

- Added `sha1` for flag matching algorithms
- Added `regex` for property matching in feature flags
- Added `tokio` (optional) for async local evaluation with background polling
- Added `json` and `gzip` features to `reqwest` for flag payloads and compression
- Dev dependencies: `httpmock` for testing, `futures` for async tests

## 0.5.0 - 2025-11-05

### Minor Changes

Configuration system now accepts base URLs instead of full endpoint URLs
- Provide just the hostname (e.g., `https://eu.posthog.com`)
- SDK automatically appends `/i/v0/e/` for single events and `/batch/` for batch events
- Old format with full URLs still works - paths are automatically stripped and normalized
- Enables simultaneous use of both single-event and batch endpoints
## 0.4.0 - 2025-11-05

### Minor Changes

- Refactored error handling to use organized error types (`TransportError`, `ValidationError`, `InitializationError`) with structured data (timeouts, status codes, batch sizes) that can be pattern matched
- Existing errors will continue to work with deprecation warnings.

- New helper methods:
- `is_retryable()` - identifies transient errors (timeouts, 5xx, 429)
- `is_client_error()` - identifies 4xx errors

#### New Dependencies:
- Added `thiserror` to reduce writing manual error handling boilerplate

## 0.2.6 - 2025-01-08


Expand Down
14 changes: 12 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,29 @@ rust-version = "1.78.0"
reqwest = { version = "0.11.3", default-features = false, features = [
"rustls-tls",
"blocking",
"json",
"gzip",
] }
serde = { version = "1.0.125", features = ["derive"] }
chrono = { version = "0.4.19", features = ["serde"] }
serde_json = "1.0.64"
semver = "1.0.24"
derive_builder = "0.20.2"
uuid = { version = "1.13.2", features = ["serde", "v7"] }
sha1 = "0.10"
regex = "1.10"
tokio = { version = "1", features = ["rt", "sync", "time", "macros"], optional = true }
url = "2.5"
thiserror = "2.0"

[dev-dependencies]
dotenv = "0.15.0"
ctor = "0.1.26"
tokio = { version = "1", features = ["full"] }
httpmock = "0.7"
serde_json = "1.0"
futures = "0.3"

[features]
default = ["async-client"]
e2e-test = []
async-client = []
async-client = ["tokio"]
239 changes: 235 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# PostHog Rust

Please see the main [PostHog docs](https://posthog.com/docs).
Please see the main [PostHog docs](https://posthog.com/docs)

**This crate is under development**

Expand All @@ -13,12 +13,243 @@ Add `posthog-rs` to your `Cargo.toml`.
posthog-rs = "0.3.7"
```

## Basic Usage (US Region)

```rust
use posthog_rs::{Event, ClientOptionsBuilder};

// Simple initialization with API key (defaults to US region)
let client = posthog_rs::client(env!("POSTHOG_API_KEY"));

let mut event = posthog_rs::Event::new("test", "1234");
event.insert_prop("key1", "value1").unwrap();
event.insert_prop("key2", vec!["a", "b"]).unwrap();
// Create and send an event
let mut event = Event::new("user_signed_up", "user_distinct_id");
event.insert_prop("plan", "premium").unwrap();
event.insert_prop("source", "web").unwrap();

client.capture(event).unwrap();
```

## EU Region Configuration

```rust
use posthog_rs::{Event, ClientOptionsBuilder};

// Configure for EU region - just provide the base URL
let options = ClientOptionsBuilder::new()
.api_key("phc_your_api_key")
.api_endpoint("https://eu.posthog.com") // SDK handles /i/v0/e/ and /batch/ automatically
.build()
.unwrap();

let client = posthog_rs::client(options);

// Single event capture
let event = Event::new("user_signed_up", "user_distinct_id");
client.capture(event).unwrap();

// Batch event capture (uses same base URL, different endpoint path)
let events = vec![
Event::new("page_view", "user_1"),
Event::new("button_click", "user_2"),
];
client.capture_batch(events).unwrap();
```

## Backward Compatibility

Old format with full URLs still works - the SDK automatically normalizes them:

```rust
// This still works - path is automatically stripped
let options = ClientOptionsBuilder::new()
.api_key("phc_your_api_key")
.api_endpoint("https://eu.posthog.com/i/v0/e/") // Gets normalized to base URL
.build()
.unwrap();
```

## Feature Flags

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

### Basic Usage

```rust
use posthog_rs::{ClientOptionsBuilder, FlagValue};
use std::collections::HashMap;
use serde_json::json;

let options = ClientOptionsBuilder::default()
.api_key("phc_your_project_key")
.build()
.unwrap();

let client = posthog_rs::client(options);

// 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

```rust
// 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)

```rust
// 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

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

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

### Feature Flag Payloads

```rust
// 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);
}
```

### Local Evaluation (High Performance)

For significantly faster flag evaluation, enable local evaluation to cache flag definitions locally:

```rust
use posthog_rs::ClientOptionsBuilder;

let options = ClientOptionsBuilder::default()
.api_key("phc_your_project_key")
.personal_api_key("phx_your_personal_key") // Required for local evaluation
.enable_local_evaluation(true)
.poll_interval_seconds(30) // Update cache every 30s
.build()
.unwrap();

let client = posthog_rs::client(options);

// Flag evaluations now happen locally (no API calls needed)
let enabled = client.is_feature_enabled(
"new-feature".to_string(),
"user-123".to_string(),
None, None, None
).unwrap();
```

**Performance:** Local evaluation is 100-1000x faster than API evaluation (~119µs vs ~125ms per request).

Get your personal API key at: https://app.posthog.com/me/settings

### Automatic Event Tracking

The SDK automatically captures `$feature_flag_called` events when you evaluate feature flags. These events include:
- Feature flag key and response value
- Deduplication per user + flag + value combination
- Rich metadata (payloads, versions, request IDs)

To disable automatic events globally:
```rust
let options = ClientOptionsBuilder::default()
.api_key("phc_your_key")
.send_feature_flag_events(false)
.build()
.unwrap();
```

## Error Handling

The SDK provides error handling with semantic categories:

```rust
use posthog_rs::{Error, TransportError, ValidationError};

match client.capture(event).await {
Ok(_) => println!("Event sent successfully"),
Err(Error::Transport(TransportError::Timeout(duration))) => {
eprintln!("Request timed out after {:?}", duration);
// Retry logic here
}
Err(Error::Transport(TransportError::HttpError(401, _))) => {
eprintln!("Invalid API key - check your configuration");
}
Err(e) if e.is_retryable() => {
// Automatically handles: timeouts, 5xx errors, 429 rate limits
tokio::time::sleep(Duration::from_secs(2)).await;
client.capture(event).await?;
}
Err(e) => eprintln!("Permanent error: {}", e),
}
```

### Error Categories

- **TransportError**: Network issues (DNS, timeouts, HTTP errors, connection failures)
- **ValidationError**: Data problems (serialization, batch size, invalid timestamps)
- **InitializationError**: Configuration issues (already initialized, not initialized)

### Helper Methods

- `is_retryable()`: Returns `true` for transient errors (timeouts, 5xx, 429)
- `is_client_error()`: Returns `true` for 4xx HTTP errors

See [`examples/error_classification.rs`](examples/error_classification.rs) for comprehensive error handling patterns
Loading