Skip to content

feat: add EvaluateFlags() API for single-call flag evaluation#191

Open
dmarticus wants to merge 1 commit intomainfrom
posthog-code/evaluate-flags-api
Open

feat: add EvaluateFlags() API for single-call flag evaluation#191
dmarticus wants to merge 1 commit intomainfrom
posthog-code/evaluate-flags-api

Conversation

@dmarticus
Copy link
Copy Markdown
Contributor

@dmarticus dmarticus commented Apr 27, 2026

Summary

  • New Client.EvaluateFlags(payload) returns a *FeatureFlagEvaluations snapshot built from a single /flags request.
  • Snapshot exposes IsEnabled / GetFlag / GetFlagPayload / OnlyAccessed / Only / Keys. Access methods fire deduped $feature_flag_called with full v4 metadata; GetFlagPayload is silent.
  • New Capture.Flags *FeatureFlagEvaluations attaches $feature/<key> and $active_feature_flags from the snapshot — no second /flags call per event. Takes precedence over SendFeatureFlags and warns if both are set.
  • New Config.FeatureFlagsLogWarnings *bool silences the Only() filter-helper warning.
  • SendFeatureFlags is unchanged (no deprecations in this PR).
snap, _ := client.EvaluateFlags(posthog.EvaluateFlagsPayload{DistinctId: "user-1"})
if snap.IsEnabled("new-checkout") { /* ... */ }
client.Enqueue(posthog.Capture{
    DistinctId: "user-1",
    Event:      "checkout-completed",
    Flags:      snap, // attaches $feature/<key> + $active_feature_flags, no extra /flags call
})

RFC — mirrored from posthog-python#539 and posthog-js#3476, incorporating the latest review feedback from the Python PR.

Design decisions

  • Single LRU cache for both paths: the legacy single-flag path and the snapshot path share the same (distinctId, key, deviceId) dedup cache, so mixing the two APIs in one process can't double-fire $feature_flag_called.
  • OnlyAccessed() honors its name — empty by default: returning all flags as a "safety net" when nothing was accessed contradicted the method's intent; callers who want all flags can just pass the parent snapshot directly.
  • Capture.Flags takes precedence over SendFeatureFlags with a runtime warning when both are set — firing a redundant /flags request would defeat the whole point of supplying a snapshot.
  • Filtered children get a fresh accessed set: OnlyAccessed() / Only() clones don't back-propagate access into the parent, so attaching snap.OnlyAccessed() to a Capture doesn't widen what a later parent.OnlyAccessed() returns.
  • Empty DistinctId returns a silent empty snapshot instead of erroring — matches the Python/Node design and avoids $feature_flag_called events with empty distinct_ids leaking into analytics.
  • Response-level errors propagate per-flag: errorsWhileComputingFlags and quotaLimited on the snapshot are combined comma-joined with per-flag errors (e.g. errors_while_computing_flags,flag_missing) in $feature_flag_error, matching the legacy single-flag path's granularity so dashboards don't lose error categories when callers migrate.
  • flag_keys_to_evaluate wired through the decider interface, so existing single-flag callers pass nil and behavior is unchanged on the wire.

Phase 2 follow-ups (out of scope here)

  • Deprecate Capture.SendFeatureFlags in favor of Capture.Flags.
  • Steer IsFeatureEnabled / GetFeatureFlag / GetFeatureFlagResult callers toward EvaluateFlags for multi-flag use.
  • Plumb a flag_definitions_loaded_at timestamp from the local-eval poller so $feature_flag_definitions_loaded_at can be attached for locally-evaluated flags.
  • Surface TIMEOUT / CONNECTION_ERROR / api_error_NNN exceptions in the snapshot's $feature_flag_error (currently only the response-level error categories are propagated).

Branch was originally based on stale local master; force-pushed after rebasing onto origin/main. All tests + lint pass.


Created with PostHog Code

@dmarticus dmarticus force-pushed the posthog-code/evaluate-flags-api branch from 832df34 to 30b1585 Compare April 27, 2026 20:10
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 27, 2026

posthog-go Compliance Report

Date: 2026-04-29 21:27:15 UTC
Duration: 105915ms

⚠️ Some Tests Failed

29/30 tests passed, 1 failed


Capture Tests

29/29 tests passed

View Details
Test Status Duration
Format Validation.Event Has Required Fields 609ms
Format Validation.Event Has Uuid 607ms
Format Validation.Event Has Lib Properties 608ms
Format Validation.Distinct Id Is String 607ms
Format Validation.Token Is Present 607ms
Format Validation.Custom Properties Preserved 607ms
Format Validation.Event Has Timestamp 608ms
Retry Behavior.Retries On 503 5612ms
Retry Behavior.Does Not Retry On 400 2610ms
Retry Behavior.Does Not Retry On 401 2608ms
Retry Behavior.Respects Retry After Header 5611ms
Retry Behavior.Implements Backoff 15623ms
Retry Behavior.Retries On 500 5614ms
Retry Behavior.Retries On 502 5613ms
Retry Behavior.Retries On 504 5613ms
Retry Behavior.Max Retries Respected 15623ms
Deduplication.Generates Unique Uuids 615ms
Deduplication.Preserves Uuid On Retry 5613ms
Deduplication.Preserves Uuid And Timestamp On Retry 10617ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry 5616ms
Deduplication.No Duplicate Events In Batch 610ms
Deduplication.Different Events Have Different Uuids 622ms
Compression.Sends Gzip When Enabled 607ms
Batch Format.Uses Proper Batch Structure 606ms
Batch Format.Flush With No Events Sends Nothing 606ms
Batch Format.Multiple Events Batched Together 609ms
Error Handling.Does Not Retry On 403 2610ms
Error Handling.Does Not Retry On 413 2609ms
Error Handling.Retries On 408 5612ms

Feature_Flags Tests

⚠️ 0/1 tests passed, 1 failed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id 7ms

Failures

request_payload.request_with_person_properties_device_id

Field 'distinct_id' not found in /flags request body at path 'person_properties.distinct_id'. Available keys: ['$device_id']

@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
@dmarticus dmarticus force-pushed the posthog-code/evaluate-flags-api branch from 30b1585 to c6d9751 Compare April 29, 2026 21:07
Adds a Phase 1 implementation of the Server SDK Feature Flag
Evaluations RFC, mirroring posthog-js#3476 and posthog-python#539.

Client.EvaluateFlags returns a FeatureFlagEvaluations snapshot built
from at most one /flags request. The snapshot powers IsEnabled,
GetFlag, and GetFlagPayload checks, fires deduped $feature_flag_called
events with full v4 metadata (id, version, reason, request_id), and can
be attached to a Capture event via the new Capture.Flags field to
populate $feature/<key> and $active_feature_flags with no extra
network call.

The dedup logic for $feature_flag_called is extracted into
captureFlagCalledIfNeeded so the existing single-flag path and the new
snapshot path share the same per-distinct_id LRU cache and behave
identically.

OnlyAccessed and Only return filtered child snapshots with independent
access tracking, so filtering for a Capture does not back-propagate to
the parent. A new Config.FeatureFlagsLogWarnings option silences their
warnings for callers that prefer quieter helpers.

Capture.SendFeatureFlags is unchanged and not deprecated; Phase 2 will
follow up with deprecations and migration guidance.

Generated-By: PostHog Code
Task-Id: b9d98122-fe61-462a-bfb3-6e1be3a8966a
@dmarticus dmarticus force-pushed the posthog-code/evaluate-flags-api branch from c6d9751 to 4b71cbd Compare April 29, 2026 21:24
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