From 2b8e8f0066a4cb5eb21720d88f7f8252ff38b40a Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Tue, 1 Sep 2026 11:09:26 +0200 Subject: [PATCH 1/4] docs!: audit and document the v1 public API --- .sampo/changesets/v1-response-stability.md | 5 + README.md | 2 + api/public-api.txt | 16 +-- docs/migration-0.x-to-1.0.md | 135 +++++++++++++++++++++ docs/v1-public-api-audit.md | 82 +++++++++++++ src/capture_event.rs | 2 + src/client/async_client.rs | 2 +- src/client/blocking.rs | 2 +- src/client/capture.rs | 14 +-- src/client/mod.rs | 10 +- src/client/on_error.rs | 10 +- src/client/retry.rs | 8 +- src/feature_flags.rs | 4 + src/local_evaluation.rs | 16 +++ tests/test_local_evaluation.rs | 30 +---- 15 files changed, 283 insertions(+), 55 deletions(-) create mode 100644 .sampo/changesets/v1-response-stability.md create mode 100644 docs/migration-0.x-to-1.0.md create mode 100644 docs/v1-public-api-audit.md diff --git a/.sampo/changesets/v1-response-stability.md b/.sampo/changesets/v1-response-stability.md new file mode 100644 index 00000000..49ce6a6e --- /dev/null +++ b/.sampo/changesets/v1-response-stability.md @@ -0,0 +1,5 @@ +--- +cargo/posthog-rs: major +--- + +Mark server response types as non-exhaustive and add the 0.x to 1.0 migration and public API audit documentation. diff --git a/README.md b/README.md index 9ae531e4..453e4c78 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/api/public-api.txt b/api/public-api.txt index ffa35554..02604d8a 100644 --- a/api/public-api.txt +++ b/api/public-api.txt @@ -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> pub posthog_rs::FeatureFlagsResponse::Legacy::feature_flag_payloads: std::collections::hash::map::HashMap @@ -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 -pub struct posthog_rs::CaptureResponse +#[non_exhaustive] pub struct posthog_rs::CaptureResponse pub posthog_rs::CaptureResponse::results: std::collections::hash::map::HashMap #[non_exhaustive] pub struct posthog_rs::CaptureSummary impl posthog_rs::CaptureSummary @@ -238,7 +238,7 @@ pub fn posthog_rs::Event::remove_prop(&mut self, &str) -> core::option::Option(&mut self, chrono::datetime::DateTime) -> 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 pub posthog_rs::EventResult::result: posthog_rs::EventStatus pub struct posthog_rs::FeatureFlag @@ -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 pub posthog_rs::FlagDetail::reason: core::option::Option pub posthog_rs::FlagDetail::variant: core::option::Option -pub struct posthog_rs::FlagMetadata +#[non_exhaustive] pub struct posthog_rs::FlagMetadata pub posthog_rs::FlagMetadata::description: core::option::Option pub posthog_rs::FlagMetadata::has_experiment: core::option::Option pub posthog_rs::FlagMetadata::id: u64 @@ -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 pub posthog_rs::FlagReason::description: core::option::Option @@ -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 -pub struct posthog_rs::LocalEvaluationResponse +#[non_exhaustive] pub struct posthog_rs::LocalEvaluationResponse pub posthog_rs::LocalEvaluationResponse::cohorts: std::collections::hash::map::HashMap pub posthog_rs::LocalEvaluationResponse::flags: alloc::vec::Vec pub posthog_rs::LocalEvaluationResponse::group_type_mapping: std::collections::hash::map::HashMap pub posthog_rs::LocalEvaluationResponse::minimal_flag_called_events: bool +impl posthog_rs::LocalEvaluationResponse +pub fn posthog_rs::LocalEvaluationResponse::new(alloc::vec::Vec) -> Self pub struct posthog_rs::LocalEvaluator impl posthog_rs::LocalEvaluator pub fn posthog_rs::LocalEvaluator::cache(&self) -> &posthog_rs::FlagCache diff --git a/docs/migration-0.x-to-1.0.md b/docs/migration-0.x-to-1.0.md new file mode 100644 index 00000000..46a189d0 --- /dev/null +++ b/docs/migration-0.x-to-1.0.md @@ -0,0 +1,135 @@ +# 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. The package rename described below lands separately in [PR #183](https://github.com/PostHog/posthog-rs/pull/183). + +## 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 `all_persisted()` under the new semantics. + +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. + +## Planned package rename + +[PR #183](https://github.com/PostHog/posthog-rs/pull/183) plans to publish the implementation as `posthog` and retain `posthog-rs` as a compatibility crate that re-exports it. Once that PR is part of the release branch, new applications should depend on and import `posthog`: + +```toml +posthog = "1" +``` + +```rust +use posthog::{client, Event}; +``` + +Existing applications may stay on the `posthog-rs` compatibility package during the announced compatibility period. The exact retirement timeline is not decided yet in [issue #178](https://github.com/PostHog/posthog-rs/issues/178), so do not remove `posthog-rs` solely based on this guide until the v1 release notes confirm the package plan. + +## TLS configuration + +The proposed `rustls-no-provider` feature from [PR #201](https://github.com/PostHog/posthog-rs/pull/201) is not part of the current `v1` branch. No TLS migration is documented yet. Recheck the final v1 release notes if that work is revived before 1.0. + +## Upgrade checklist + +1. Remove the `capture-v1` Cargo feature. +2. Replace deprecated feature flag calls with `evaluate_flags` snapshots. +3. Replace `personal_api_key` configuration with `secret_key`. +4. Rename `V1ErrorResponse` and remove any use of `Endpoint::Batch`. +5. Review immediate-capture persistence checks and configured compression. +6. Run both your default async build and any `default-features = false` blocking build. +7. Apply the package rename only after PR #183 is included in the release branch. diff --git a/docs/v1-public-api-audit.md b/docs/v1-public-api-audit.md new file mode 100644 index 00000000..8fc91b8f --- /dev/null +++ b/docs/v1-public-api-audit.md @@ -0,0 +1,82 @@ +# V1 public API audit + +This audit records the intended stability boundary for the generated `api/public-api.txt` snapshot on the `v1` branch. The snapshot is generated by `scripts/check-public-api.sh`; it is not edited by hand. + +## Audit scope + +The review covered every exported item in the snapshot under the default public-facing feature set and grouped it by how applications use it: + +- client entry points and lifecycle APIs; +- capture input, response, error, compression, and hook types; +- remote and local feature flag APIs, definitions, and responses; +- error tracking configuration and context types; +- global client functions, constants, and endpoint helpers. + +Test-harness-only APIs remain excluded by `scripts/check-public-api.sh`. + +## Stability decisions + +### Extensible enums and response types + +Enums that can gain server or SDK outcomes remain non-exhaustive: `Error`, `Endpoint`, `EventStatus`, and `PostHogError`. + +The audit also makes these server response types non-exhaustive before 1.0: + +- `FeatureFlagsResponse`; +- `CaptureResponse` and `EventResult`; +- `LocalEvaluationResponse`; +- `FlagDetail`, `FlagReason`, and `FlagMetadata`. + +Applications can continue to deserialize and read their public fields. They must use a wildcard when matching `FeatureFlagsResponse` and cannot construct the response structs with literals outside the crate. This lets the SDK represent additional backend fields and response variants in a minor v1 release. + +`CaptureErrorResponse`, `CaptureSummary`, and the failure structs were already non-exhaustive. + +### Closed value types + +`CaptureCompression` remains exhaustive because its four variants are the complete set accepted by the V1 capture endpoint. `FlagValue` remains exhaustive because the public flag value contract is boolean or string. + +### Options and configuration + +`EvaluateFlagsOptions` is non-exhaustive and implements `Default`, so new request options can be added compatibly. + +`ClientOptions`, `ErrorTrackingOptions`, and `CaptureExceptionOptions` keep private fields and are created through builders or fluent methods. Their fields can evolve without exposing struct-literal construction. + +`LocalEvaluationConfig` remains directly constructible because it is also the low-level configuration for `FlagPoller` and `LocalEvaluator`. Any future field addition needs either a new constructor/builder transition or a major release. That tradeoff is intentional for v1 rather than removing current low-level construction. + +### Feature flag definition models + +`FeatureFlag`, `FeatureFlagFilters`, `FeatureFlagCondition`, `Property`, `MultivariateFilter`, `MultivariateVariant`, `Cohort`, and `CohortDefinition` remain constructible data models. They are inputs to the public local evaluation API as well as deserialized server definitions. Preserving literal construction is intentional for tests, offline definitions, and custom definition stores. Adding required fields to these models is therefore outside the v1 compatibility promise unless defaults or constructors preserve source compatibility. + +### Opaque runtime handles + +`Client`, pollers, evaluators, caches, hooks, snapshots, and endpoint managers expose behavior through methods while keeping runtime state private. No additional stability attributes are needed. + +## Removed or renamed before v1 + +The audited snapshot confirms the intended breaking cleanup already present on `v1`: + +- deprecated single-flag methods and `get_feature_flags` are absent; +- `ClientOptionsBuilder::personal_api_key` is absent; +- `LocalEvaluationConfig::secret_key` replaces `personal_api_key`; +- `EvaluateFlagsOptions` is non-exhaustive; +- `Endpoint::Batch` is absent and `Endpoint` is non-exhaustive; +- `CaptureErrorResponse` replaces `V1ErrorResponse`. + +## Remaining release gates + +This snapshot is the intended API for the current `posthog-rs` implementation crate. It must be regenerated and reviewed once the package rename and compatibility shim in [PR #183](https://github.com/PostHog/posthog-rs/pull/183) land, because the canonical crate/module name will change to `posthog`. + +The TLS proposal in [PR #201](https://github.com/PostHog/posthog-rs/pull/201) is closed and not present on `v1`. If revived, its Cargo feature surface needs a separate review even though Cargo features do not appear in this Rust item snapshot. + +## Verification + +Run all of the following from the release candidate: + +```bash +scripts/check-public-api.sh +cargo test --workspace +cargo test --no-default-features +cargo check --examples +``` + +A clean public API check proves the generated snapshot matches the source. It does not replace the explicit review above or the required post-#183 review. diff --git a/src/capture_event.rs b/src/capture_event.rs index b0ca2b26..b368047b 100644 --- a/src/capture_event.rs +++ b/src/capture_event.rs @@ -172,6 +172,7 @@ pub enum EventStatus { } #[derive(Debug, Clone, Serialize, Deserialize)] +#[non_exhaustive] pub struct EventResult { pub result: EventStatus, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -179,6 +180,7 @@ pub struct EventResult { } #[derive(Debug, Clone, Serialize, Deserialize)] +#[non_exhaustive] pub struct CaptureResponse { pub results: HashMap, } diff --git a/src/client/async_client.rs b/src/client/async_client.rs index e7d21205..8b7c9c14 100644 --- a/src/client/async_client.rs +++ b/src/client/async_client.rs @@ -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( diff --git a/src/client/blocking.rs b/src/client/blocking.rs index 991babe4..e0694d8d 100644 --- a/src/client/blocking.rs +++ b/src/client/blocking.rs @@ -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( diff --git a/src/client/capture.rs b/src/client/capture.rs index 319161c7..cb38aad1 100644 --- a/src/client/capture.rs +++ b/src/client/capture.rs @@ -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}; @@ -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::{ @@ -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 { @@ -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 @@ -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)) } @@ -300,7 +300,7 @@ pub(crate) fn after_response( request_id = %request_id, attempt, results = ?result_counts, - "V1 capture batch response" + "Capture batch response" ); } @@ -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 { diff --git a/src/client/mod.rs b/src/client/mod.rs index 59eb0ae1..a1015672 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -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, @@ -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, diff --git a/src/client/on_error.rs b/src/client/on_error.rs index 74c31aed..a1c59bd6 100644 --- a/src/client/on_error.rs +++ b/src/client/on_error.rs @@ -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 @@ -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 diff --git a/src/client/retry.rs b/src/client/retry.rs index d80f6b45..2e9cb862 100644 --- a/src/client/retry.rs +++ b/src/client/retry.rs @@ -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, @@ -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` diff --git a/src/feature_flags.rs b/src/feature_flags.rs index 65b61ce0..9ff172ac 100644 --- a/src/feature_flags.rs +++ b/src/feature_flags.rs @@ -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 { @@ -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, @@ -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, @@ -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, diff --git a/src/local_evaluation.rs b/src/local_evaluation.rs index 4a46868e..7b29ecf4 100644 --- a/src/local_evaluation.rs +++ b/src/local_evaluation.rs @@ -57,6 +57,7 @@ fn report_local_eval_error(hooks: &[OnErrorHook], status: Option, error: &E /// Contains feature flag definitions, group type mappings, and cohort definitions /// that can be cached locally for flag evaluation without server round-trips. #[derive(Debug, Clone, Serialize, Deserialize)] +#[non_exhaustive] pub struct LocalEvaluationResponse { /// List of feature flag definitions pub flags: Vec, @@ -73,6 +74,21 @@ pub struct LocalEvaluationResponse { pub minimal_flag_called_events: bool, } +impl LocalEvaluationResponse { + /// Create a local-evaluation response from feature flag definitions. + /// + /// Group mappings, cohorts, and minimized flag-called events default to + /// empty or disabled and can be assigned through their public fields. + pub fn new(flags: Vec) -> Self { + Self { + flags, + group_type_mapping: HashMap::new(), + cohorts: HashMap::new(), + minimal_flag_called_events: false, + } + } +} + /// A cohort definition for local evaluation. /// /// Cohorts are groups of users defined by property filters, used for diff --git a/tests/test_local_evaluation.rs b/tests/test_local_evaluation.rs index 887d485a..08c7c877 100644 --- a/tests/test_local_evaluation.rs +++ b/tests/test_local_evaluation.rs @@ -39,12 +39,7 @@ fn test_local_evaluation_basic() { }; // Update cache with the flag - let response = LocalEvaluationResponse { - flags: vec![flag], - group_type_mapping: HashMap::new(), - cohorts: HashMap::new(), - minimal_flag_called_events: false, - }; + let response = LocalEvaluationResponse::new(vec![flag]); cache.update(response); // Test evaluation @@ -302,12 +297,7 @@ fn test_local_evaluation_with_properties() { }; // Update cache - let response = LocalEvaluationResponse { - flags: vec![flag], - group_type_mapping: HashMap::new(), - cohorts: HashMap::new(), - minimal_flag_called_events: false, - }; + let response = LocalEvaluationResponse::new(vec![flag]); cache.update(response); // Test with matching properties @@ -731,12 +721,7 @@ fn test_cache_operations() { }, ]; - let response = LocalEvaluationResponse { - flags: flags.clone(), - group_type_mapping: HashMap::new(), - cohorts: HashMap::new(), - minimal_flag_called_events: false, - }; + let response = LocalEvaluationResponse::new(flags.clone()); cache.update(response); @@ -1274,12 +1259,9 @@ fn cache_with(flag: FeatureFlag) -> FlagCache { let cache = FlagCache::new(); let mut group_type_mapping = HashMap::new(); group_type_mapping.insert("0".to_string(), "company".to_string()); - cache.update(LocalEvaluationResponse { - flags: vec![flag], - group_type_mapping, - cohorts: HashMap::new(), - minimal_flag_called_events: false, - }); + let mut response = LocalEvaluationResponse::new(vec![flag]); + response.group_type_mapping = group_type_mapping; + cache.update(response); cache } From 2075fc93619d6b958620121767b731e3ff5d6cd2 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Tue, 1 Sep 2026 11:22:24 +0200 Subject: [PATCH 2/4] docs: remove the standalone public API audit --- .sampo/changesets/v1-response-stability.md | 2 +- docs/v1-public-api-audit.md | 82 ---------------------- 2 files changed, 1 insertion(+), 83 deletions(-) delete mode 100644 docs/v1-public-api-audit.md diff --git a/.sampo/changesets/v1-response-stability.md b/.sampo/changesets/v1-response-stability.md index 49ce6a6e..ffdc8ed8 100644 --- a/.sampo/changesets/v1-response-stability.md +++ b/.sampo/changesets/v1-response-stability.md @@ -2,4 +2,4 @@ cargo/posthog-rs: major --- -Mark server response types as non-exhaustive and add the 0.x to 1.0 migration and public API audit documentation. +Mark server response types as non-exhaustive and add the 0.x to 1.0 migration guide. diff --git a/docs/v1-public-api-audit.md b/docs/v1-public-api-audit.md deleted file mode 100644 index 8fc91b8f..00000000 --- a/docs/v1-public-api-audit.md +++ /dev/null @@ -1,82 +0,0 @@ -# V1 public API audit - -This audit records the intended stability boundary for the generated `api/public-api.txt` snapshot on the `v1` branch. The snapshot is generated by `scripts/check-public-api.sh`; it is not edited by hand. - -## Audit scope - -The review covered every exported item in the snapshot under the default public-facing feature set and grouped it by how applications use it: - -- client entry points and lifecycle APIs; -- capture input, response, error, compression, and hook types; -- remote and local feature flag APIs, definitions, and responses; -- error tracking configuration and context types; -- global client functions, constants, and endpoint helpers. - -Test-harness-only APIs remain excluded by `scripts/check-public-api.sh`. - -## Stability decisions - -### Extensible enums and response types - -Enums that can gain server or SDK outcomes remain non-exhaustive: `Error`, `Endpoint`, `EventStatus`, and `PostHogError`. - -The audit also makes these server response types non-exhaustive before 1.0: - -- `FeatureFlagsResponse`; -- `CaptureResponse` and `EventResult`; -- `LocalEvaluationResponse`; -- `FlagDetail`, `FlagReason`, and `FlagMetadata`. - -Applications can continue to deserialize and read their public fields. They must use a wildcard when matching `FeatureFlagsResponse` and cannot construct the response structs with literals outside the crate. This lets the SDK represent additional backend fields and response variants in a minor v1 release. - -`CaptureErrorResponse`, `CaptureSummary`, and the failure structs were already non-exhaustive. - -### Closed value types - -`CaptureCompression` remains exhaustive because its four variants are the complete set accepted by the V1 capture endpoint. `FlagValue` remains exhaustive because the public flag value contract is boolean or string. - -### Options and configuration - -`EvaluateFlagsOptions` is non-exhaustive and implements `Default`, so new request options can be added compatibly. - -`ClientOptions`, `ErrorTrackingOptions`, and `CaptureExceptionOptions` keep private fields and are created through builders or fluent methods. Their fields can evolve without exposing struct-literal construction. - -`LocalEvaluationConfig` remains directly constructible because it is also the low-level configuration for `FlagPoller` and `LocalEvaluator`. Any future field addition needs either a new constructor/builder transition or a major release. That tradeoff is intentional for v1 rather than removing current low-level construction. - -### Feature flag definition models - -`FeatureFlag`, `FeatureFlagFilters`, `FeatureFlagCondition`, `Property`, `MultivariateFilter`, `MultivariateVariant`, `Cohort`, and `CohortDefinition` remain constructible data models. They are inputs to the public local evaluation API as well as deserialized server definitions. Preserving literal construction is intentional for tests, offline definitions, and custom definition stores. Adding required fields to these models is therefore outside the v1 compatibility promise unless defaults or constructors preserve source compatibility. - -### Opaque runtime handles - -`Client`, pollers, evaluators, caches, hooks, snapshots, and endpoint managers expose behavior through methods while keeping runtime state private. No additional stability attributes are needed. - -## Removed or renamed before v1 - -The audited snapshot confirms the intended breaking cleanup already present on `v1`: - -- deprecated single-flag methods and `get_feature_flags` are absent; -- `ClientOptionsBuilder::personal_api_key` is absent; -- `LocalEvaluationConfig::secret_key` replaces `personal_api_key`; -- `EvaluateFlagsOptions` is non-exhaustive; -- `Endpoint::Batch` is absent and `Endpoint` is non-exhaustive; -- `CaptureErrorResponse` replaces `V1ErrorResponse`. - -## Remaining release gates - -This snapshot is the intended API for the current `posthog-rs` implementation crate. It must be regenerated and reviewed once the package rename and compatibility shim in [PR #183](https://github.com/PostHog/posthog-rs/pull/183) land, because the canonical crate/module name will change to `posthog`. - -The TLS proposal in [PR #201](https://github.com/PostHog/posthog-rs/pull/201) is closed and not present on `v1`. If revived, its Cargo feature surface needs a separate review even though Cargo features do not appear in this Rust item snapshot. - -## Verification - -Run all of the following from the release candidate: - -```bash -scripts/check-public-api.sh -cargo test --workspace -cargo test --no-default-features -cargo check --examples -``` - -A clean public API check proves the generated snapshot matches the source. It does not replace the explicit review above or the required post-#183 review. From 353e8580e400ed64ae130095633024f6da67a942 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Tue, 1 Sep 2026 11:23:07 +0200 Subject: [PATCH 3/4] docs: narrow the v1 migration guide --- .sampo/changesets/v1-response-stability.md | 5 ---- docs/migration-0.x-to-1.0.md | 30 +--------------------- 2 files changed, 1 insertion(+), 34 deletions(-) delete mode 100644 .sampo/changesets/v1-response-stability.md diff --git a/.sampo/changesets/v1-response-stability.md b/.sampo/changesets/v1-response-stability.md deleted file mode 100644 index ffdc8ed8..00000000 --- a/.sampo/changesets/v1-response-stability.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -cargo/posthog-rs: major ---- - -Mark server response types as non-exhaustive and add the 0.x to 1.0 migration guide. diff --git a/docs/migration-0.x-to-1.0.md b/docs/migration-0.x-to-1.0.md index 46a189d0..f48ebdba 100644 --- a/docs/migration-0.x-to-1.0.md +++ b/docs/migration-0.x-to-1.0.md @@ -1,6 +1,6 @@ # 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. The package rename described below lands separately in [PR #183](https://github.com/PostHog/posthog-rs/pull/183). +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 @@ -105,31 +105,3 @@ let options = posthog_rs::ClientOptionsBuilder::default() ``` `secret_key` accepts either a project secret key (`phs_...`) or a personal API key (`phx_...`). Do not send this key as an event property. - -## Planned package rename - -[PR #183](https://github.com/PostHog/posthog-rs/pull/183) plans to publish the implementation as `posthog` and retain `posthog-rs` as a compatibility crate that re-exports it. Once that PR is part of the release branch, new applications should depend on and import `posthog`: - -```toml -posthog = "1" -``` - -```rust -use posthog::{client, Event}; -``` - -Existing applications may stay on the `posthog-rs` compatibility package during the announced compatibility period. The exact retirement timeline is not decided yet in [issue #178](https://github.com/PostHog/posthog-rs/issues/178), so do not remove `posthog-rs` solely based on this guide until the v1 release notes confirm the package plan. - -## TLS configuration - -The proposed `rustls-no-provider` feature from [PR #201](https://github.com/PostHog/posthog-rs/pull/201) is not part of the current `v1` branch. No TLS migration is documented yet. Recheck the final v1 release notes if that work is revived before 1.0. - -## Upgrade checklist - -1. Remove the `capture-v1` Cargo feature. -2. Replace deprecated feature flag calls with `evaluate_flags` snapshots. -3. Replace `personal_api_key` configuration with `secret_key`. -4. Rename `V1ErrorResponse` and remove any use of `Endpoint::Batch`. -5. Review immediate-capture persistence checks and configured compression. -6. Run both your default async build and any `default-features = false` blocking build. -7. Apply the package rename only after PR #183 is included in the release branch. From 5263925bcc428bd00f39fe066f6b5090f619ad84 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Tue, 1 Sep 2026 11:31:51 +0200 Subject: [PATCH 4/4] docs: clarify immediate capture durability checks --- docs/migration-0.x-to-1.0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/migration-0.x-to-1.0.md b/docs/migration-0.x-to-1.0.md index f48ebdba..1134ad2b 100644 --- a/docs/migration-0.x-to-1.0.md +++ b/docs/migration-0.x-to-1.0.md @@ -40,7 +40,7 @@ All event-producing SDK paths now use the same capture endpoint, including error 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 `all_persisted()` under the new semantics. +`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.