Skip to content

feat: add EvaluateFlagsAsync() API for single-call flag evaluation#186

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

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

Conversation

@dmarticus
Copy link
Copy Markdown
Contributor

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 /flags twice: once via IsFeatureEnabledAsync/GetFeatureFlagAsync, and again via Capture(..., sendFeatureFlags: true). This PR introduces a single EvaluateFlagsAsync(distinctId, options) call that returns a FeatureFlagEvaluations snapshot. The snapshot is the source of truth for the rest of that request: it powers IsEnabled / GetFlag / GetFlagPayload calls, fires \$feature_flag_called lazily and dedup'd on first access, and can be forwarded to a new Capture(..., flags: snapshot, ...) overload so \$feature/<key> and \$active_feature_flags flow through without a second /flags round-trip.

What's new

  • FeatureFlagEvaluations — a public sealed snapshot class with IsEnabled, GetFlag, GetFlagPayload, OnlyAccessed, Only(...), and a Keys accessor.
  • IPostHogClient.EvaluateFlagsAsync(distinctId, options, ct) — returns a snapshot. Reuses the existing AllFeatureFlagsOptions (its FlagKeysToEvaluate already scopes the /flags request body, distinct from the in-memory Only(...) filter).
  • IPostHogClient.Capture(..., FeatureFlagEvaluations? flags, ...) + matching CaptureException overload. The existing Capture(..., bool sendFeatureFlags, ...) is unchanged and is not deprecated in this PR.
  • PostHogOptions.FeatureFlagsLogWarnings (defaults to true) — silences the warnings emitted by OnlyAccessed-empty and Only(...) with unknown keys.
  • Bug fix: the legacy \$feature_flag_called event hard-coded locally_evaluated=false even for locally-evaluated flags. Locally-evaluated flags now correctly emit locally_evaluated=true, \$feature_flag_reason=\"Evaluated locally\", and a new \$feature_flag_definitions_loaded_at timestamp surfaced from LocalFeatureFlagsLoader. Two existing tests were updated to assert the corrected output.

Pre-flight answers

  1. Existing single-flag dedup helper. PostHogClient.CaptureFeatureFlagCalledEvent (legacy line 490) wrote into _featureFlagCalledEventCache with 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.
  2. Rich /flags response handler. FlagsApiResult already surfaces Flags: Dict<string, FeatureFlagResult> with Id, Version, and Reason.Description. FeatureFlag.CreateFromFlagsApi upgrades to the internal FeatureFlagWithMetadata when those fields are present. The snapshot reads them through that same mechanism.
  3. Local evaluation. LocalFeatureFlagsLoader polls definitions; LocalEvaluator does the per-flag work via EvaluateAllFlags(distinctId, groups, personProperties, warnOnUnknownGroups: false). This PR adds LocalFeatureFlagsLoader.FlagDefinitionsLoadedAt (Unix ms) — set after each successful load, cleared by Clear().
  4. Capture conventions. The existing Capture is positional (distinctId, eventName, properties, groups, sendFeatureFlags, timestamp). Adding a default-valued flags parameter would be source-breaking, so this PR adds a separate overload taking FeatureFlagEvaluations? flags and routes both through a private CaptureCore. Mirrored on CaptureException.
  5. File path. src/PostHog/Features/FeatureFlagEvaluations.cs (next to FeatureFlag.cs), with EvaluatedFlagRecord and IFeatureFlagEvaluationsHost as siblings.

Test plan

  • dotnet build PostHog.sln clean.
  • 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.
  • New regression test Capture_LegacyAndSnapshotPaths_ShareDedupCache exercises the cross-path cache.
  • New regression coverage that locally-evaluated flags now carry locally_evaluated=true + reason + loaded-at on \$feature_flag_called.

Phase 2 (separate PR)

Mark IsFeatureEnabledAsync, GetFeatureFlagAsync, and Capture(..., sendFeatureFlags, ...) as [Obsolete] with a pointer to the snapshot API. Not included here.


Created with PostHog Code

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
@dmarticus
Copy link
Copy Markdown
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.

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