Skip to content
Merged
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

The official Rust SDK for [PostHog](https://posthog.com). See the main [PostHog docs](https://posthog.com/docs) for more information.

Upgrading from a 0.x release? Read the [1.0 migration guide](docs/migration-0.x-to-1.0.md).

SDK usage examples and code snippets live in the official documentation so they stay up to date.

## Documentation
Expand Down
16 changes: 9 additions & 7 deletions api/public-api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ pub posthog_rs::EventStatus::Ok
pub posthog_rs::EventStatus::Retry
pub posthog_rs::EventStatus::Unknown
pub posthog_rs::EventStatus::Warning
pub enum posthog_rs::FeatureFlagsResponse
#[non_exhaustive] pub enum posthog_rs::FeatureFlagsResponse
pub posthog_rs::FeatureFlagsResponse::Legacy
pub posthog_rs::FeatureFlagsResponse::Legacy::errors: core::option::Option<alloc::vec::Vec<alloc::string::String>>
pub posthog_rs::FeatureFlagsResponse::Legacy::feature_flag_payloads: std::collections::hash::map::HashMap<alloc::string::String, serde_json::value::Value>
Expand Down Expand Up @@ -118,7 +118,7 @@ pub fn posthog_rs::CaptureFailure<'a>::event_results(&self) -> &std::collections
pub fn posthog_rs::CaptureFailure<'a>::historical_migration(&self) -> bool
pub fn posthog_rs::CaptureFailure<'a>::request_id(&self) -> core::option::Option<&uuid::Uuid>
pub fn posthog_rs::CaptureFailure<'a>::status(&self) -> core::option::Option<u16>
pub struct posthog_rs::CaptureResponse
#[non_exhaustive] pub struct posthog_rs::CaptureResponse
pub posthog_rs::CaptureResponse::results: std::collections::hash::map::HashMap<uuid::Uuid, posthog_rs::EventResult>
#[non_exhaustive] pub struct posthog_rs::CaptureSummary
impl posthog_rs::CaptureSummary
Expand Down Expand Up @@ -238,7 +238,7 @@ pub fn posthog_rs::Event::remove_prop(&mut self, &str) -> core::option::Option<s
pub fn posthog_rs::Event::set_timestamp<Tz>(&mut self, chrono::datetime::DateTime<Tz>) -> core::result::Result<(), posthog_rs::Error> where Tz: chrono::offset::TimeZone
pub fn posthog_rs::Event::set_uuid(&mut self, uuid::Uuid)
pub fn posthog_rs::Event::with_flags(&mut self, &posthog_rs::FeatureFlagEvaluations) -> &mut Self
pub struct posthog_rs::EventResult
#[non_exhaustive] pub struct posthog_rs::EventResult
pub posthog_rs::EventResult::details: core::option::Option<alloc::string::String>
pub posthog_rs::EventResult::result: posthog_rs::EventStatus
pub struct posthog_rs::FeatureFlag
Expand Down Expand Up @@ -282,13 +282,13 @@ pub fn posthog_rs::FlagCache::new() -> Self
pub fn posthog_rs::FlagCache::update(&self, posthog_rs::LocalEvaluationResponse)
impl core::default::Default for posthog_rs::FlagCache
pub fn posthog_rs::FlagCache::default() -> Self
pub struct posthog_rs::FlagDetail
#[non_exhaustive] pub struct posthog_rs::FlagDetail
pub posthog_rs::FlagDetail::enabled: bool
pub posthog_rs::FlagDetail::key: alloc::string::String
pub posthog_rs::FlagDetail::metadata: core::option::Option<posthog_rs::FlagMetadata>
pub posthog_rs::FlagDetail::reason: core::option::Option<posthog_rs::FlagReason>
pub posthog_rs::FlagDetail::variant: core::option::Option<alloc::string::String>
pub struct posthog_rs::FlagMetadata
#[non_exhaustive] pub struct posthog_rs::FlagMetadata
pub posthog_rs::FlagMetadata::description: core::option::Option<alloc::string::String>
pub posthog_rs::FlagMetadata::has_experiment: core::option::Option<bool>
pub posthog_rs::FlagMetadata::id: u64
Expand All @@ -302,7 +302,7 @@ pub fn posthog_rs::FlagPoller::start(&mut self)
pub fn posthog_rs::FlagPoller::stop(&mut self)
impl core::ops::drop::Drop for posthog_rs::FlagPoller
pub fn posthog_rs::FlagPoller::drop(&mut self)
pub struct posthog_rs::FlagReason
#[non_exhaustive] pub struct posthog_rs::FlagReason
pub posthog_rs::FlagReason::code: alloc::string::String
pub posthog_rs::FlagReason::condition_index: core::option::Option<usize>
pub posthog_rs::FlagReason::description: core::option::Option<alloc::string::String>
Expand Down Expand Up @@ -330,11 +330,13 @@ pub posthog_rs::LocalEvaluationConfig::secret_key: alloc::string::String
impl<'a> posthog_rs::LocalEvaluationFailure<'a>
pub fn posthog_rs::LocalEvaluationFailure<'a>::error(&self) -> &posthog_rs::Error
pub fn posthog_rs::LocalEvaluationFailure<'a>::status(&self) -> core::option::Option<u16>
pub struct posthog_rs::LocalEvaluationResponse
#[non_exhaustive] pub struct posthog_rs::LocalEvaluationResponse
pub posthog_rs::LocalEvaluationResponse::cohorts: std::collections::hash::map::HashMap<alloc::string::String, posthog_rs::Cohort>
pub posthog_rs::LocalEvaluationResponse::flags: alloc::vec::Vec<posthog_rs::FeatureFlag>
pub posthog_rs::LocalEvaluationResponse::group_type_mapping: std::collections::hash::map::HashMap<alloc::string::String, alloc::string::String>
pub posthog_rs::LocalEvaluationResponse::minimal_flag_called_events: bool
impl posthog_rs::LocalEvaluationResponse
pub fn posthog_rs::LocalEvaluationResponse::new(alloc::vec::Vec<posthog_rs::FeatureFlag>) -> Self
pub struct posthog_rs::LocalEvaluator
impl posthog_rs::LocalEvaluator
pub fn posthog_rs::LocalEvaluator::cache(&self) -> &posthog_rs::FlagCache
Expand Down
107 changes: 107 additions & 0 deletions docs/migration-0.x-to-1.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Migrating from posthog-rs 0.x to 1.0

Version 1.0 makes the V1 analytics endpoint the SDK's only capture path and removes APIs that were already deprecated in 0.x. This guide describes the changes currently staged on the `v1` branch.

## Cargo features

Remove `capture-v1` from your dependency features. Capture no longer needs a feature flag.

```toml
# 0.x while opting into V1 capture
posthog-rs = { version = "0.25", features = ["capture-v1"] }

# 1.0
posthog-rs = "1"
```

The default features remain the async client and error tracking. Use `default-features = false` for the blocking client.

```toml
# Async client with error tracking
posthog-rs = "1"

# Blocking client without error tracking
posthog-rs = { version = "1", default-features = false }
```

The V0 capture implementation and its `/i/v0/e/` and batch plumbing have been removed. `Endpoint::Batch` no longer exists, and `Endpoint::Capture` now resolves to `/i/v1/analytics/events`.

## Capture behavior

The regular `capture` and `capture_batch` methods remain fire-and-forget. They enqueue events for the background worker, which batches and retries delivery. `capture_immediate` and `capture_batch_immediate` still bypass the queue and return a `CaptureSummary` after reaching a terminal result.

All event-producing SDK paths now use the same capture endpoint, including error tracking, `$feature_flag_called` events, historical migration, `before_send`, and terminal failures reported through `on_error`.

### Compression

`CaptureCompression::Gzip`, `Deflate`, `Br`, and `Zstd` now all apply their corresponding `Content-Encoding`. In older default V0 builds, only gzip was supported and selecting another variant could send an uncompressed body. Check any proxy or WAF in front of PostHog before enabling Brotli or Zstandard.

### Retry and persistence results

The V1 endpoint returns a result for each event. The SDK retries transient request failures and only the events with retryable results from a partial response.

`CaptureSummary::not_persisted()` and `CaptureSummary::all_persisted()` now use those per-event results. In the V0 path they reported a successful `2xx` as fully persisted without per-event confirmation. Applications that advance durable state after `capture_immediate` should check both that `submitted()` equals the number of intended events and that `all_persisted()` is true, because disabled clients and fully `before_send`-filtered batches submit no events but still report `all_persisted()` as true.

HTTP 429 is not a retryable V1 capture status. The V1 service uses HTTP 402 for billing limits and per-event `drop` or `warning` results in successful responses. `Retry-After` is still honored for retryable failures and retry results.

### Renamed capture response type

Rename `V1ErrorResponse` to `CaptureErrorResponse`:

```rust
// 0.x with capture-v1
let response: Option<&posthog_rs::V1ErrorResponse> = failure.error_response();

// 1.0
let response: Option<&posthog_rs::CaptureErrorResponse> = failure.error_response();
```

## Feature flags

The deprecated single-flag methods have been removed:

- `is_feature_enabled`
- `get_feature_flag`
- `get_feature_flag_payload`
- `get_feature_flags`

Call `evaluate_flags` once and read from the returned snapshot instead:

```rust
use posthog_rs::EvaluateFlagsOptions;

let flags = client
.evaluate_flags("user-123", EvaluateFlagsOptions::default())
.await?;

if flags.is_enabled("new-checkout") {
// Use the enabled feature.
}

let variant = flags.get_flag("checkout-variant");
let payload = flags.get_flag_payload("checkout-variant");
```

The blocking client uses the same calls without `.await`.

`EvaluateFlagsOptions` is now non-exhaustive. Construct it with `Default` and assign the fields you need instead of using a struct literal:

```rust
let mut options = EvaluateFlagsOptions::default();
options.disable_geoip = Some(true);
options.flag_keys = Some(vec!["new-checkout".to_string()]);
```

## Local evaluation credentials

Use `secret_key` terminology throughout configuration. The builder's deprecated `personal_api_key` alias has been removed, and `LocalEvaluationConfig::personal_api_key` is now `secret_key`.

```rust
let options = posthog_rs::ClientOptionsBuilder::default()
.api_key("phc_project_token")
.secret_key("phs_project_secret")
.enable_local_evaluation(true)
.build()?;
```

`secret_key` accepts either a project secret key (`phs_...`) or a personal API key (`phx_...`). Do not send this key as an event property.
2 changes: 2 additions & 0 deletions src/capture_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,13 +172,15 @@ pub enum EventStatus {
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct EventResult {
pub result: EventStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub details: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CaptureResponse {
pub results: HashMap<Uuid, EventResult>,
}
Expand Down
2 changes: 1 addition & 1 deletion src/client/async_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,7 @@ impl Client {
self.send_immediate(events, historical_migration).await
}

/// Inline V1 capture: prepare once via the shared sans-IO helpers, then loop
/// Inline capture: prepare once via the shared sans-I/O helpers, then loop
/// send/classify, awaiting `tokio::time::sleep` between retries. The setup and
/// classification are shared with the blocking client; only this loop differs.
async fn send_immediate(
Expand Down
2 changes: 1 addition & 1 deletion src/client/blocking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,7 @@ impl Client {
self.send_immediate(events, historical_migration)
}

/// Inline V1 capture: prepare once via the shared sans-IO helpers, then loop
/// Inline capture: prepare once via the shared sans-I/O helpers, then loop
/// send/classify, sleeping on the calling thread between retries. The setup and
/// classification are shared with the async client; only this loop differs.
fn send_immediate(
Expand Down
14 changes: 7 additions & 7 deletions src/client/capture.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! Shared, runtime-agnostic helpers for the V1 capture pipeline.
//! Shared, runtime-agnostic helpers for the capture pipeline.
//! Each client keeps only the I/O; this module owns everything else.

use std::{collections::HashMap, time::Duration};
Expand All @@ -9,7 +9,7 @@ use tracing::debug;
use uuid::Uuid;

use super::retry::{backoff_duration, is_retryable_status};
// Re-exported so the V1 capture loops in the client modules can reach them as
// Re-exported so the capture loops in the client modules can reach them as
// `capture::parse_retry_after` / `capture::Step`.
pub(crate) use super::retry::{parse_retry_after, Step};
use super::{
Expand Down Expand Up @@ -141,7 +141,7 @@ pub(crate) fn maybe_compress(
// Inline (immediate) capture preparation
// ---------------------------------------------------------------------------

/// Everything an inline immediate V1 capture needs after event preparation:
/// Everything an inline immediate capture needs after event preparation:
/// built once, then reused across retry attempts. The async and blocking
/// clients share this (it is I/O-free) and keep only the send loop.
pub(crate) struct Prepared {
Expand All @@ -153,7 +153,7 @@ pub(crate) struct Prepared {
pub(crate) submitted: usize,
}

/// Prepare an inline immediate V1 capture: apply client defaults + `before_send`,
/// Prepare an inline immediate capture: apply client defaults + `before_send`,
/// then build the wire events and the per-request identity (request id,
/// `created_at`, URL). Returns `None` when nothing survives filtering (an empty
/// or fully `before_send`-dropped batch), so the caller returns a default
Expand Down Expand Up @@ -270,7 +270,7 @@ pub(crate) fn after_transport_error(
request_id = %request_id,
attempt,
error = %err_msg,
"V1 capture request failed, will retry"
"Capture request failed, will retry"
);
Step::Backoff(backoff_duration(opts, attempt, None))
}
Expand Down Expand Up @@ -300,7 +300,7 @@ pub(crate) fn after_response(
request_id = %request_id,
attempt,
results = ?result_counts,
"V1 capture batch response"
"Capture batch response"
);
}

Expand Down Expand Up @@ -329,7 +329,7 @@ pub(crate) fn after_response(
attempt,
status,
error = %error_desc,
"V1 capture request failed, will retry"
"Capture request failed, will retry"
);

if attempt >= opts.max_capture_attempts {
Expand Down
10 changes: 4 additions & 6 deletions src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,7 @@ pub struct ClientOptions {
pub(crate) shutdown_timeout_ms: u64,

/// Optional request-body compression. When `None` (default), bodies are
/// sent uncompressed. The V0 pipeline supports `Gzip` only; V1 supports all
/// variants.
/// sent uncompressed. The capture endpoint supports all variants.
#[builder(default, setter(strip_option))]
pub(crate) capture_compression: Option<CaptureCompression>,

Expand Down Expand Up @@ -268,10 +267,9 @@ pub struct ClientOptions {

/// Resolved client-level default properties for capture requests.
///
/// Built once from [`ClientOptions`] and threaded through all event-producing
/// paths (V0 capture, V0 flag-called host, V1 capture) so each default is
/// applied in exactly one place with caller-wins (`entry().or_insert`)
/// semantics.
/// Built once from [`ClientOptions`] and threaded through every event-producing
/// path so each default is applied in exactly one place with caller-wins
/// (`entry().or_insert`) semantics.
#[derive(Debug, Clone, Copy)]
pub(crate) struct CaptureDefaults {
pub(crate) disable_geoip: bool,
Expand Down
10 changes: 5 additions & 5 deletions src/client/on_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,14 +127,14 @@ impl<'a> CaptureFailure<'a> {
self.historical_migration
}

/// The V1 capture `posthog-request-id` of the final attempt, when one was
/// sent. `None` for a serialization failure (no request reached the wire)
/// and on the v0 pipeline (which has no request id).
/// The capture `posthog-request-id` of the final attempt, when one was
/// sent. `None` for a serialization failure where no request reached the
/// wire.
pub fn request_id(&self) -> Option<&Uuid> {
self.request_id
}

/// Per-event server verdicts for the batch (V1 capture pipeline only).
/// Per-event server verdicts for the capture batch.
///
/// Maps event UUID to its [`EventResult`]. Includes **all** verdicts the
/// batch collected — persisted (`ok`/`warning`) as well as lost
Expand All @@ -148,7 +148,7 @@ impl<'a> CaptureFailure<'a> {
self.results
}

/// The structured error body returned by the V1 capture backend on a
/// The structured error body returned by the capture backend on a
/// non-`2xx` response (`error`, `error_description`, `error_uri`), when the
/// body parsed as one. `None` for a transport error, a `2xx`, or an
/// unrecognizable body — the raw body remains available via
Expand Down
8 changes: 4 additions & 4 deletions src/client/retry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ use reqwest::header::HeaderMap;
use super::ClientOptions;
use crate::error::Error;

/// Outcome of one capture attempt, computed without any I/O so both the async
/// and blocking clients (and both V0 and V1) can share the decision logic and
/// keep only the transport-specific loop.
/// Outcome of one capture attempt, computed without any I/O so the async and
/// blocking clients can share the decision logic and keep only the
/// transport-specific loop.
#[derive(Debug)]
pub(crate) enum Step {
Done,
Expand Down Expand Up @@ -402,7 +402,7 @@ mod tests {
);
}

// -- v0 sans-IO decisions ------------------------------------------------
// -- Feature flag sans-I/O decisions -------------------------------------

/// The schedule the call sites actually produce. Guards the `attempt + 1`
/// off-by-one: the first retry must wait exactly `retry_initial_backoff_ms`
Expand Down
4 changes: 4 additions & 0 deletions src/feature_flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ pub struct MultivariateVariant {
/// legacy format (simple flag values and payloads).
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum FeatureFlagsResponse {
/// v2 API format from `/flags/?v=2` endpoint
V2 {
Expand Down Expand Up @@ -411,6 +412,7 @@ impl FeatureFlagsResponse {
/// Returned by the `/flags/?v=2` endpoint with extended information about why a
/// flag evaluated to a particular value.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct FlagDetail {
/// The feature flag key
pub key: String,
Expand All @@ -428,6 +430,7 @@ pub struct FlagDetail {

/// Explains why a feature flag evaluated to a particular value.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct FlagReason {
/// Reason code (e.g., "condition_match", "out_of_rollout_bound")
pub code: String,
Expand All @@ -441,6 +444,7 @@ pub struct FlagReason {

/// Metadata about a feature flag from the PostHog server.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct FlagMetadata {
/// Unique identifier for this flag
pub id: u64,
Expand Down
Loading