Skip to content

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

Merged
dmarticus merged 1 commit intomainfrom
posthog-code/evaluate-flags-api
May 1, 2026
Merged

feat: add EvaluateFlags() API for single-call flag evaluation#191
dmarticus merged 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. When local evaluation resolves some flags and the remote request fails, the snapshot still carries the locally-resolved flags alongside the error.
  • Snapshot exposes IsEnabled / GetFlag / GetFlagPayload / OnlyAccessed / Only / Keys. Access methods fire deduped $feature_flag_called with full v4 metadata, the per-flag $feature/<key> value, and the parsed $feature_flag_payload. 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 (with a runtime warning if both are set), and caller-supplied Properties override the auto-generated values on conflict.
  • 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.
  • Partial snapshot on remote failure: if /flags errors after local evaluation succeeded for some flags, we return both the snapshot and the error so callers don't lose the locally-resolved work.
  • Capture.Flags takes precedence over SendFeatureFlags with a runtime warning when both are set, and user Properties override generated $feature/<key> values — letting callers manually overwrite a flag's tagged value on a single event without rebuilding the 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.
  • Filter warnings route through Config.Logger rather than a dedicated config switch; users who want them silenced can pass a quiet Logger.
  • 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-05-01 20:02:17 UTC
Duration: 105964ms

⚠️ 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 610ms
Format Validation.Event Has Uuid 608ms
Format Validation.Event Has Lib Properties 608ms
Format Validation.Distinct Id Is String 607ms
Format Validation.Token Is Present 608ms
Format Validation.Custom Properties Preserved 607ms
Format Validation.Event Has Timestamp 607ms
Retry Behavior.Retries On 503 5609ms
Retry Behavior.Does Not Retry On 400 2609ms
Retry Behavior.Does Not Retry On 401 2611ms
Retry Behavior.Respects Retry After Header 5613ms
Retry Behavior.Implements Backoff 15624ms
Retry Behavior.Retries On 500 5613ms
Retry Behavior.Retries On 502 5613ms
Retry Behavior.Retries On 504 5613ms
Retry Behavior.Max Retries Respected 15614ms
Deduplication.Generates Unique Uuids 616ms
Deduplication.Preserves Uuid On Retry 5614ms
Deduplication.Preserves Uuid And Timestamp On Retry 10617ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry 5615ms
Deduplication.No Duplicate Events In Batch 611ms
Deduplication.Different Events Have Different Uuids 627ms
Compression.Sends Gzip When Enabled 608ms
Batch Format.Uses Proper Batch Structure 608ms
Batch Format.Flush With No Events Sends Nothing 606ms
Batch Format.Multiple Events Batched Together 612ms
Error Handling.Does Not Retry On 403 2610ms
Error Handling.Does Not Retry On 413 2610ms
Error Handling.Retries On 408 5613ms

Feature_Flags Tests

⚠️ 0/1 tests passed, 1 failed

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

Failures

request_payload.request_with_person_properties_device_id

Field 'token' not found in /flags request body at path 'token'. Available keys: ['api_key', 'distinct_id', 'groups', 'person_properties', 'group_properties', 'geoip_disable']

@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 2 times, most recently from c6d9751 to 4b71cbd Compare April 29, 2026 21:24
Comment thread config.go Outdated
// unexpected ways — for example, calling OnlyAccessed before any flag has
// been accessed, or passing unknown keys to Only. The zero value (nil)
// means warnings are emitted; set to Ptr(false) to silence them.
FeatureFlagsLogWarnings *bool
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +236 to +239
properties := NewProperties().
Set("$feature_flag", key).
Set("$feature_flag_response", response).
Set("locally_evaluated", found && flag.LocallyEvaluated)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other SDKs were setting $feature/<key> as well

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+ $feature_flag_payload

Comment thread posthog.go Outdated
Comment on lines +801 to +803
if err != nil {
return nil, err
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread posthog.go Outdated
if m.Properties == nil {
m.Properties = NewProperties()
}
m.Properties.Merge(m.Flags.eventProperties())
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the Python SDK merged generated flag properties first, letting user properties override conflicts

@dmarticus dmarticus force-pushed the posthog-code/evaluate-flags-api branch from 4b71cbd to a93d4be Compare April 30, 2026 21:16
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 a93d4be to 192f744 Compare May 1, 2026 19:59
@dmarticus dmarticus merged commit 2370beb into main May 1, 2026
14 checks passed
@dmarticus dmarticus deleted the posthog-code/evaluate-flags-api branch May 1, 2026 20:03
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