feat: add evaluate_flags() API for single-call flag evaluation#137
Open
feat: add evaluate_flags() API for single-call flag evaluation#137
Conversation
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
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
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
Client#evaluate_flags(distinct_id, …)returning aFeatureFlagEvaluationssnapshot — one/flagsround-trip powers both branching and event enrichment.is_enabled/get_flag/get_flag_payloadfor branching, plusonly_accessed/only([keys])to narrow what gets attached to a captured event.flags:option oncaptureandcapture_exception— when present, attaches$feature/<key>and$active_feature_flagsfrom the snapshot without any extra/flagscall.flag_keys:onevaluate_flagsscopes the underlying/flagsrequest itself (sent asflag_keys_to_evaluate).is_feature_enabled,get_feature_flag,get_feature_flag_result,get_feature_flag_payload, andcapture(send_feature_flags:)— they keep working but now emit aDeprecationWarningpointing atevaluate_flags(). Removal is planned for the next major.feature_flags_log_warnings:client option (defaulttrue) 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_enabledreturnsfalsefor unknown flags,get_flagreturnsnil— matches the legacy single-flag methods so existing branching code is structurally interchangeable.get_flag_payloaddeliberately 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.accessedset, so calls on a clone don't back-propagate exposures into the parent.Hoststruct (two lambdas:capture_flag_called_event_if_needed,log_warning) is passed to the snapshot instead of a back-reference to the fullClient— keeps the snapshot decoupled and testable.locally_evaluated: true, reason"Evaluated locally", and$feature_flag_definitions_loaded_aton emitted events, matching the existing single-flag local path.errorsWhileComputingFlags,quotaLimited) and per-flag errors (flag_missing) are combined into a comma-joined$feature_flag_errorso each event keeps the granular error code(s).flags:andsend_feature_flags:tocaptureuses the snapshot and warns — the snapshot guarantees the event carries the same values your code branched on.is_feature_enabledandget_feature_flagcall a private_get_feature_flag_resultdirectly) so a single user-level call emits exactly one warning, not a cascade.Phase 2 follow-ups
evaluate_flags().flagssnapshot 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