feat: add evaluate_flags() API for single-call flag evaluation#105
Open
feat: add evaluate_flags() API for single-call flag evaluation#105
Conversation
Adds a snapshot-based feature flag API mirroring posthog-python (#539) and posthog-node (#3476). One call to evaluate_flags(distinct_id, options) reaches /flags?v=2 once and returns a FeatureFlagEvaluations cache that: - Resolves is_enabled / get_flag locally with full metadata propagation ($feature_flag_id, $feature_flag_version, $feature_flag_reason, $feature_flag_request_id) on the deduplicated $feature_flag_called event - Treats get_flag_payload as event-free - Offers only_accessed() / only([keys]) filter helpers with warnings on misuse, gated by a new feature_flags_log_warnings client option - Short-circuits empty-distinct_id snapshots so accesses never emit events Also adds Event::with_flags(&snapshot) so a captured event inherits \$feature/<key> and \$active_feature_flags from the snapshot without an extra /flags request. Both blocking and async clients implement the host trait that owns the per-distinct_id dedup cache (cap 50_000, full reset on overflow to match the JS SDK). The existing get_feature_flag / is_feature_enabled methods stay silent — a Phase 2 follow-up will retrofit them onto the same dedup helper. Generated-By: PostHog Code Task-Id: 2b101877-6890-43d1-8dbd-306433cd9d25
CodeQL flags `e` as unused inside `tokio::spawn(async move { ... })` even
though tracing's `%e` shorthand uses Display on it. Switch to `{e}` capture
syntax so the use is unambiguous to the analyzer; same telemetry, slightly
less structured but the field was only consumed by the debug log anyway.
Generated-By: PostHog Code
Task-Id: 2b101877-6890-43d1-8dbd-306433cd9d25
The previous attempt swapped tracing's `%e` shorthand for `{e}` capture
syntax but CodeQL still flagged the variables as unused — its Rust
extractor doesn't track identifiers through the format-string macro
expansion inside `tokio::spawn(async move { ... })`. Explicitly bind the
error to a `String` via `.to_string()` and log that, which gives the
analyzer an unambiguous use.
Generated-By: PostHog Code
Task-Id: 2b101877-6890-43d1-8dbd-306433cd9d25
Mirrors the changes from PostHog/posthog-python#539 (commit 95eb1e9): - only_accessed() returns an empty snapshot when nothing was accessed, rather than falling back to all flags + a warning. The fallback contradicted the method's name and surprised reviewers — pre-access any flags you want attached. - Propagate response-level errors (errors_while_computing_flags, quota_limited) into $feature_flag_called events as a comma-joined $feature_flag_error so each access carries the same granular error codes the single-flag path emits. quota_limited is now parsed from the v2 response. - Drop the unused flag_definitions_loaded_at plumbing (dead code in Phase 1 — replaced by the response-level error propagation). - Clarify the flag_keys docstring on EvaluateFlagsOptions: it scopes the network call, distinct from the in-memory only([keys]) helper. Generated-By: PostHog Code Task-Id: 2b101877-6890-43d1-8dbd-306433cd9d25
Per reviewer feedback on PostHog/posthog-python#539, ship Phase 2 in this PR alongside Phase 1 instead of splitting into a follow-up. The deprecated methods continue to work — they just emit a `#[deprecated]` compile warning pointing at `evaluate_flags()`: - `Client::get_feature_flag` - `Client::is_feature_enabled` - `Client::get_feature_flag_payload` Both blocking and async clients are covered. `is_feature_enabled` allows the deprecation lint internally because it still routes through `get_feature_flag` — that's the implementation detail; user-level call sites still surface exactly one warning each (one per call to a deprecated method). The existing tests and examples that exercise these methods get module-level `#![allow(deprecated)]` with a comment noting the deprecation window. The companion `evaluate_flags()` snapshot path covers all three methods' use cases without an extra `/flags` round-trip per call and emits a deduped `\$feature_flag_called` event with full metadata. Generated-By: PostHog Code Task-Id: 2b101877-6890-43d1-8dbd-306433cd9d25
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
evaluate_flags(distinct_id, options)— one/flags?v=2round-trip returns aFeatureFlagEvaluationssnapshot. From the snapshot:is_enabled(key)/get_flag(key)read the cached value and fire a deduplicated$feature_flag_calledevent with full metadata ($feature_flag_id,$feature_flag_version,$feature_flag_reason,$feature_flag_request_id).get_flag_payload(key)reads the payload without firing an event.only_accessed()andonly([keys])produce filtered clones for narrower capture attribution.Event::with_flags(&snapshot)attaches$feature/<key>and$active_feature_flagsto a captured event with no second/flagscall.Implements RFC #1020 — Server SDK Feature Flag Evaluations API. Mirrors posthog-python#539 and posthog-js#3476.
Deprecations
The legacy single-flag methods are now
#[deprecated]in favor ofevaluate_flags()— they keep working but emit a compile warning at every call site:Client::get_feature_flagClient::is_feature_enabledClient::get_feature_flag_payloadDesign decisions
event.with_flags(&snapshot)rather than a newcaptureparameter — keepscapture(event)unchanged and reads naturally in Rust.$feature_flag_callednever lands with an emptydistinct_id.only_accessed()returns empty when nothing was accessed (no fallback-to-all, no warning) — pre-access the flags you want attached.$feature_flag_erroris comma-joined combining response-level errors (errors_while_computing_flags,quota_limited) with per-flag errors (flag_missing) so consumers can filter by type.feature_flags_log_warningsclient option silences theonly(...)misuse warnings.Created with PostHog Code