Skip to content

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

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

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

Conversation

@dmarticus
Copy link
Copy Markdown
Contributor

@dmarticus dmarticus commented Apr 27, 2026

Summary

  • Adds Client#evaluate_flags(distinct_id, …) returning a FeatureFlagEvaluations snapshot — one /flags round-trip powers both branching and event enrichment.
  • Snapshot exposes is_enabled / get_flag / get_flag_payload for branching, plus only_accessed / only([keys]) to narrow what gets attached to a captured event.
  • Adds flags: option on capture and capture_exception — when present, attaches $feature/<key> and $active_feature_flags from the snapshot without any extra /flags call.
  • flag_keys: on evaluate_flags scopes the underlying /flags request itself (sent as flag_keys_to_evaluate).
  • Deprecates is_feature_enabled, get_feature_flag, get_feature_flag_result, get_feature_flag_payload, and capture(send_feature_flags:) — they keep working but now emit a DeprecationWarning pointing at evaluate_flags(). Removal is planned for the next major.
  • Adds a feature_flags_log_warnings: client option (default true) to silence the snapshot's filter-helper warnings.

References

RFC: https://github.com/PostHog/requests-for-comments-internal/pull/1020 · mirrors posthog-python#539 and posthog-js#3476.

Design decisions

  • is_enabled returns false for unknown flags, get_flag returns nil — matches the legacy single-flag methods so existing branching code is structurally interchangeable.
  • get_flag_payload deliberately does not record access or fire $feature_flag_called — payload-only reads shouldn't count as an exposure.
  • only_accessed() returns an empty snapshot when nothing has been accessed (it honors its name) — pre-access flags first if you want a populated result.
  • Filtered snapshot clones get a copy of the parent's accessed set, so calls on a clone don't back-propagate exposures into the parent.
  • A small Host struct (two lambdas: capture_flag_called_event_if_needed, log_warning) is passed to the snapshot instead of a back-reference to the full Client — keeps the snapshot decoupled and testable.
  • Locally-evaluated flags are tagged with locally_evaluated: true, reason "Evaluated locally", and $feature_flag_definitions_loaded_at on emitted events, matching the existing single-flag local path.
  • Response-level errors (errorsWhileComputingFlags, quotaLimited) and per-flag errors (flag_missing) are combined into a comma-joined $feature_flag_error so each event keeps the granular error code(s).
  • Passing both flags: and send_feature_flags: to capture uses the snapshot and warns — the snapshot guarantees the event carries the same values your code branched on.
  • Deprecated methods bypass each other internally (is_feature_enabled and get_feature_flag call a private _get_feature_flag_result directly) so a single user-level call emits exactly one warning, not a cascade.

Phase 2 follow-ups

  • Update docs at https://posthog.com/docs/libraries/ruby and the feature-flags pages so the wizard / new-app instrumentation points at evaluate_flags().
  • Consider stashing the active flags snapshot on a request-scoped context so auto-captured exceptions inherit the same flag context as manual captures.

All tests + lint pass on Ruby 3.2 / 3.3 / 3.4 (380 examples).


Created with PostHog Code

Add Client#evaluate_flags(distinct_id, ...) returning a
FeatureFlagEvaluations snapshot, and a flags: option on capture so
a single /flags call can power both flag branching and event
enrichment per request.

The snapshot exposes is_enabled, get_flag, get_flag_payload, plus
only_accessed / only([keys]) filter helpers. flag_keys: scopes the
underlying /flags request itself. is_enabled and get_flag fire
$feature_flag_called events with full metadata (id, version, reason,
request_id), deduped through the existing per-distinct_id cache.
get_flag_payload does not record access or fire an event.

The dedup + capture in get_feature_flag_result is extracted into
_capture_feature_flag_called_if_needed and shared between the existing
path and the snapshot's access-recording.

Existing is_feature_enabled, get_feature_flag, get_feature_flag_result,
get_feature_flag_payload, and capture(send_feature_flags:) continue
to work unchanged.

Generated-By: PostHog Code
Task-Id: fe67a5bd-7d33-4568-9148-39d181660a5a
@dmarticus dmarticus marked this pull request as ready for review April 27, 2026 22:11
@dmarticus dmarticus requested a review from a team as a code owner April 27, 2026 22:11
Mirrors the changes that landed on the Python PR after review:

- only_accessed returns an empty snapshot when nothing has been
  accessed (drops the warn-and-fall-back-to-all-flags behavior — that
  was a misguided safety net that surprised callers in the
  early-pre-access pattern).
- capture(flags:, send_feature_flags:) now warns when both are passed,
  uses the snapshot, and ignores send_feature_flags. The snapshot
  guarantees the event carries the values branched on; the precedence
  was previously implicit.
- $feature_flag_called events now carry response-level errors
  (errors_while_computing_flags, quota_limited) combined with per-flag
  errors (flag_missing) as a comma-joined $feature_flag_error, matching
  the granularity of the legacy single-flag path.
- capture_exception now accepts a flags: kwarg and forwards it to the
  inner capture() so $exception events can carry the same flag context
  as other events.
- Phase 2 deprecation warnings ship alongside Phase 1: is_feature_enabled,
  get_feature_flag, get_feature_flag_result, get_feature_flag_payload,
  and capture(send_feature_flags:) emit Kernel.warn(..., category:
  :deprecated) pointing at evaluate_flags(). Public methods bypass
  each other internally (via a new private _get_feature_flag_result)
  so a single user-level call emits exactly one warning.

Generated-By: PostHog Code
Task-Id: fe67a5bd-7d33-4568-9148-39d181660a5a
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.

1 participant