feat: add EvaluateFlagsAsync() API for single-call flag evaluation#186
Closed
feat: add EvaluateFlagsAsync() API for single-call flag evaluation#186
Conversation
Adds a new EvaluateFlagsAsync(distinctId, options) method on the client that returns a FeatureFlagEvaluations snapshot. The snapshot powers IsEnabled / GetFlag / GetFlagPayload calls, fires $feature_flag_called lazily (deduped against the existing per-distinct-id cache), and can be forwarded to a new Capture(..., flags: snapshot) overload to attach $feature/<key> and $active_feature_flags to events without a second /flags request. Mirrors PostHog/posthog-js#3476 and PostHog/posthog-python#539. Also fixes a long-standing bug where the legacy single-flag path hard-coded locally_evaluated=false on every $feature_flag_called event. Locally-evaluated flags now correctly carry locally_evaluated=true, $feature_flag_reason="Evaluated locally", and a new $feature_flag_definitions_loaded_at timestamp surfaced via LocalFeatureFlagsLoader. The existing IsFeatureEnabledAsync / GetFeatureFlagAsync / Capture(..., sendFeatureFlags, ...) APIs are unchanged in this PR; a follow-up minor will mark them deprecated in favor of the snapshot API. Generated-By: PostHog Code Task-Id: 494d1c64-1b39-421a-9317-7ccd5992aa40
Contributor
Author
|
Replaced by a fresh branch (rebased on current main) — GitHub's verified-signatures rule blocked the force-push of a rebase that pulled in an older unsigned auto-bump commit from main. New PR coming. |
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
Phase 1 of the cross-SDK "Server SDK Feature Flag Evaluations" RFC, mirroring the recently-added designs in posthog-js#3476 (Node) and posthog-python#539 (Python).
Today, a request handler that wants to branch on a flag and attach
$feature/<key>to its analytics events ends up calling/flagstwice: once viaIsFeatureEnabledAsync/GetFeatureFlagAsync, and again viaCapture(..., sendFeatureFlags: true). This PR introduces a singleEvaluateFlagsAsync(distinctId, options)call that returns aFeatureFlagEvaluationssnapshot. The snapshot is the source of truth for the rest of that request: it powersIsEnabled/GetFlag/GetFlagPayloadcalls, fires\$feature_flag_calledlazily and dedup'd on first access, and can be forwarded to a newCapture(..., flags: snapshot, ...)overload so\$feature/<key>and\$active_feature_flagsflow through without a second/flagsround-trip.What's new
FeatureFlagEvaluations— a public sealed snapshot class withIsEnabled,GetFlag,GetFlagPayload,OnlyAccessed,Only(...), and aKeysaccessor.IPostHogClient.EvaluateFlagsAsync(distinctId, options, ct)— returns a snapshot. Reuses the existingAllFeatureFlagsOptions(itsFlagKeysToEvaluatealready scopes the/flagsrequest body, distinct from the in-memoryOnly(...)filter).IPostHogClient.Capture(..., FeatureFlagEvaluations? flags, ...)+ matchingCaptureExceptionoverload. The existingCapture(..., bool sendFeatureFlags, ...)is unchanged and is not deprecated in this PR.PostHogOptions.FeatureFlagsLogWarnings(defaults totrue) — silences the warnings emitted byOnlyAccessed-empty andOnly(...)with unknown keys.\$feature_flag_calledevent hard-codedlocally_evaluated=falseeven for locally-evaluated flags. Locally-evaluated flags now correctly emitlocally_evaluated=true,\$feature_flag_reason=\"Evaluated locally\", and a new\$feature_flag_definitions_loaded_attimestamp surfaced fromLocalFeatureFlagsLoader. Two existing tests were updated to assert the corrected output.Pre-flight answers
PostHogClient.CaptureFeatureFlagCalledEvent(legacy line 490) wrote into_featureFlagCalledEventCachewith key(distinctId, featureKey, (string)response). This PR splits property-bag construction (BuildFeatureFlagCalledProperties, static) from the cache-aware fire (TryCaptureDedupedFeatureFlagCalledEvent) so the legacy path and the snapshot path share the same dedup cache./flagsresponse handler.FlagsApiResultalready surfacesFlags: Dict<string, FeatureFlagResult>withId,Version, andReason.Description.FeatureFlag.CreateFromFlagsApiupgrades to the internalFeatureFlagWithMetadatawhen those fields are present. The snapshot reads them through that same mechanism.LocalFeatureFlagsLoaderpolls definitions;LocalEvaluatordoes the per-flag work viaEvaluateAllFlags(distinctId, groups, personProperties, warnOnUnknownGroups: false). This PR addsLocalFeatureFlagsLoader.FlagDefinitionsLoadedAt(Unix ms) — set after each successful load, cleared byClear().Captureis positional (distinctId, eventName, properties, groups, sendFeatureFlags, timestamp). Adding a default-valuedflagsparameter would be source-breaking, so this PR adds a separate overload takingFeatureFlagEvaluations? flagsand routes both through a privateCaptureCore. Mirrored onCaptureException.src/PostHog/Features/FeatureFlagEvaluations.cs(next toFeatureFlag.cs), withEvaluatedFlagRecordandIFeatureFlagEvaluationsHostas siblings.Test plan
dotnet build PostHog.slnclean.dotnet test tests/UnitTests/UnitTests.csproj— 18 new tests pass; pre-existing failures unrelated to this PR (CaptureExceptionWithInvalidFilePathInStackFrame,CaptureExceptionCauseIOFailureEmptyContext) are unchanged.dotnet test tests/UnitTests.AspNetCore/UnitTests.AspNetCore.csproj— all 25 pass.Capture_LegacyAndSnapshotPaths_ShareDedupCacheexercises the cross-path cache.locally_evaluated=true+ reason + loaded-at on\$feature_flag_called.Phase 2 (separate PR)
Mark
IsFeatureEnabledAsync,GetFeatureFlagAsync, andCapture(..., sendFeatureFlags, ...)as[Obsolete]with a pointer to the snapshot API. Not included here.Created with PostHog Code