Skip to content

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

Closed
dmarticus wants to merge 2 commits intomasterfrom
posthog-code/ruby-evaluate-flags-api
Closed

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

Conversation

@dmarticus
Copy link
Copy Markdown
Contributor

Problem

Today every flag check on the Ruby SDK fires its own /flags request, and capture(send_feature_flags: true) silently fires another. Flag values can drift between the branching call and the captured event, and send_feature_flags bloats event properties on high-volume events. The /flags v4 response carries rich metadata (id, version, reason, request_id) that is parsed into FeatureFlag objects today and then thrown away on the $feature_flag_called event — silently degrading experiment exposure tracking.

Changes

Phase 1 of the Server SDK Feature Flag Evaluations RFC, mirroring posthog-js#3476 and posthog-python#539:

snapshot = posthog.evaluate_flags("user-1", flag_keys: %w[checkout-redesign])
if snapshot.is_enabled("checkout-redesign")
  # ...
end
posthog.capture(distinct_id: "user-1", event: "checkout_started", flags: snapshot)

A single /flags request powers both branching and event enrichment. is_enabled and get_flag fire \$feature_flag_called events (deduped through the existing per-distinct_id cache) with full metadata (\$feature_flag_id, \$feature_flag_version, \$feature_flag_reason, \$feature_flag_request_id). get_flag_payload does not record access or fire an event.

Two layers of scoping

  • Network-level (flag_keys: option on evaluate_flags): scopes the /flags request itself via flag_keys_to_evaluate.
  • Event-level (only_accessed / only([keys])): narrows which flags get attached to a captured event without re-fetching. only_accessed falls back to all flags with a warning if invoked before any access; only warns on missing keys. Filtered clones do not back-propagate access to the parent.

Local evaluation

Transparent: locally-resolved flags are tagged with locally_evaluated: true, reason \"Evaluated locally\", and \$feature_flag_definitions_loaded_at (added to the poller in this PR) on emitted events.

Internals

The legacy single-flag path and the new snapshot share a new _capture_feature_flag_called_if_needed(distinct_id:, key:, response:, properties:, groups:, disable_geoip:) helper that owns dedup-key construction, the per-distinct_id sent-flags cache, and the \$feature_flag_called capture call. Both paths dedupe identically.

A new feature_flags_log_warnings: client option (default true) silences filter-helper warnings.

Pre-flight answers

  1. Existing dedup helper: was inlined in Client#get_feature_flag. Phase 1 extracts _capture_feature_flag_called_if_needed. Cache: @distinct_id_has_sent_flag_calls (SizeLimitedHash, max 50k).
  2. Rich /flags response handler: FeatureFlagsPoller#get_flags already returns {flags: {key => FeatureFlag}, requestId, evaluatedAt, …}. Reused as-is.
  3. Local evaluation: yes, polls /api/feature_flag/local_evaluation. This PR adds flag_definitions_loaded_at to the poller.
  4. Capture options: single hash attrs with symbol keys. :flags slots in alongside :send_feature_flags and takes precedence.
  5. Module path: lib/posthog/feature_flag_evaluations.rb defining PostHog::FeatureFlagEvaluations.

Phase 2 (separate follow-up minor)

These methods will be flagged with deprecation warnings in a follow-up PR:

  • is_feature_enabled
  • get_feature_flag
  • get_feature_flag_payload
  • capture(send_feature_flags:)

This PR adds no runtime deprecation warnings.

Tests

17 new examples in spec/posthog/feature_flag_evaluations_spec.rb covering: remote-evaluation snapshot shape, single-call request, dedup, full metadata on events, \$feature_flag_error: \"flag_missing\" for unknown flags, get_flag_payload not firing, flag_keys round-trip, empty-distinct_id safety, all four filter behaviors, capture integration without extra /flags call, precedence over send_feature_flags, and local-evaluation tagging including \$feature_flag_definitions_loaded_at.

Full suite: 176 examples, 1 pre-existing transport-spec failure unrelated to this PR. Rubocop clean.


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.

The legacy single-flag dedup + capture is extracted into
`_capture_feature_flag_called_if_needed` and shared between the existing
`get_feature_flag` path and the snapshot's access-recording.

Existing `is_feature_enabled`, `get_feature_flag`,
`get_feature_flag_payload`, and `capture(send_feature_flags:)` continue
to work unchanged; deprecation is a follow-up minor.

Generated-By: PostHog Code
Task-Id: fe67a5bd-7d33-4568-9148-39d181660a5a
The keyword arg `flag_keys:` triggered Ruby 2.7's hash-to-kwargs
conversion on the existing `get_flags(distinct_id, groups,
person_properties, group_properties)` callsite, eating the trailing
`group_properties` hash as kwargs and raising
`ArgumentError: unknown keyword: :company` for any non-empty value.
Positional default avoids the conversion entirely and keeps the
existing internal calls source-compatible across Ruby 2.7 → 3.x.

Generated-By: PostHog Code
Task-Id: fe67a5bd-7d33-4568-9148-39d181660a5a
@dmarticus dmarticus closed this Apr 27, 2026
@dmarticus dmarticus deleted the posthog-code/ruby-evaluate-flags-api branch April 27, 2026 20:23
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