Skip to content

feat: add evaluate_flags() API for single-call flag evaluation#105

Open
dmarticus wants to merge 5 commits intomainfrom
posthog-code/evaluate-flags-api
Open

feat: add evaluate_flags() API for single-call flag evaluation#105
dmarticus wants to merge 5 commits intomainfrom
posthog-code/evaluate-flags-api

Conversation

@dmarticus
Copy link
Copy Markdown
Contributor

@dmarticus dmarticus commented Apr 27, 2026

Summary

Adds evaluate_flags(distinct_id, options) — one /flags?v=2 round-trip returns a FeatureFlagEvaluations snapshot. From the snapshot:

  • is_enabled(key) / get_flag(key) read the cached value and fire a deduplicated $feature_flag_called event 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() and only([keys]) produce filtered clones for narrower capture attribution.
  • Event::with_flags(&snapshot) attaches $feature/<key> and $active_feature_flags to a captured event with no second /flags call.

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 of evaluate_flags() — they keep working but emit a compile warning at every call site:

  • Client::get_feature_flag
  • Client::is_feature_enabled
  • Client::get_feature_flag_payload

Design decisions

  • Capture wiring: exposed as event.with_flags(&snapshot) rather than a new capture parameter — keeps capture(event) unchanged and reads naturally in Rust.
  • Empty-distinct_id snapshots short-circuit access events so $feature_flag_called never lands with an empty distinct_id.
  • Filter clones don't back-propagate access to the parent snapshot (each clone gets its own copy of the accessed set).
  • only_accessed() returns empty when nothing was accessed (no fallback-to-all, no warning) — pre-access the flags you want attached.
  • $feature_flag_error is comma-joined combining response-level errors (errors_while_computing_flags, quota_limited) with per-flag errors (flag_missing) so consumers can filter by type.
  • Warning suppression: new feature_flags_log_warnings client option silences the only(...) misuse warnings.

Replaces #104, which was closed when the branch was recreated to clear merge conflicts after rebasing onto main.


Created with PostHog Code

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
Comment thread src/client/async_client.rs Fixed
Comment thread src/client/async_client.rs Fixed
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
Comment thread src/client/async_client.rs Fixed
Comment thread src/client/async_client.rs Fixed
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
@dmarticus dmarticus marked this pull request as ready for review April 27, 2026 22:12
@dmarticus dmarticus requested a review from a team as a code owner April 27, 2026 22:12
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants