feat: add evaluateFlags() API for single-call flag evaluation#131
Open
feat: add evaluateFlags() API for single-call flag evaluation#131
Conversation
Phase 1 of the Server SDK Feature Flag Evaluations RFC. Mirrors the Node (posthog-js#3476) and Python (posthog-python#539) implementations. * `Client::evaluateFlags()` returns a `FeatureFlagEvaluations` snapshot. Reads on the snapshot do not trigger additional `/flags` requests; access via `isEnabled` / `getFlag` fires a deduped `$feature_flag_called` event the first time each key is touched. `getFlagPayload` is silent. * `capture()` accepts a `flags` snapshot to attach `$feature/<key>` and `$active_feature_flags` properties without a fresh `/flags` round trip. * The single-flag dedup is extracted to `Client::captureFlagCalledIfNeeded()`, shared by the legacy path and the snapshot. * `flag_keys_to_evaluate` and `geoip_disable` are forwarded on the `/flags` request body when callers pass `flagKeys` or `disableGeoip`. * New `feature_flags_log_warnings` option silences filter warnings emitted from `only()` / `onlyAccessed()`. Also fixes a pre-existing bug in `SizeLimitedHash::contains/add` that caused the per-distinct_id `$feature_flag_called` dedup to never match after the first event. The new snapshot path requires real dedup, and existing tests only ever made a single call so the bug was invisible until now. Generated-By: PostHog Code Task-Id: 1f29305a-ee56-456e-a341-8faa4eb8716d
4 tasks
The file pairs `require_once 'test/error_log_mock.php'` with class declarations, matching the pattern in FeatureFlagLocalEvaluationTest. The existing tests do the same thing; suppressing the rule per-file is consistent with that precedent. Generated-By: PostHog Code Task-Id: 1f29305a-ee56-456e-a341-8faa4eb8716d
Mirrors the fixes applied to posthog-python#539 after review: * `onlyAccessed()` returns an empty snapshot when nothing has been accessed instead of warning + falling back to all flags. The fallback was contradictory with the method's name and surprising for callers doing `capture(flags: $snapshot->onlyAccessed())` early in a request before any flag had been read. * `FeatureFlagEvaluations` tracks `errorsWhileComputingFlags` and `quotaLimited` from the /flags response and combines them with the per-flag `flag_missing` error in `$feature_flag_called`, matching the granularity the single-flag path emits today. * `capture()` now logs a warning when both `flags` and `send_feature_flags` are passed; precedence is unchanged (snapshot wins) but the conflict is no longer silent. * Tightened the `flagKeys` docstring on `Client::evaluateFlags()` and the `PostHog::evaluateFlags()` facade so it's clear it scopes the underlying /flags request, distinct from the in-memory `only([keys])` filter. Generated-By: PostHog Code Task-Id: 1f29305a-ee56-456e-a341-8faa4eb8716d
…lags Phase 2 of the Server SDK Feature Flag Evaluations RFC, shipped alongside Phase 1 (mirroring posthog-python#539's eda573d). * `Client::isFeatureEnabled()`, `Client::getFeatureFlag()`, and `Client::getFeatureFlagPayload()` (along with their `PostHog::*` static facades) now emit `E_USER_DEPRECATED` pointing at `evaluateFlags()`. * `capture(['send_feature_flags' => true])` emits the same deprecation when the legacy block actually runs (the existing precedence — snapshot wins over `send_feature_flags` — is unchanged). * `Client::isFeatureEnabled()` now calls `getFeatureFlagResult()` directly instead of routing through the public `getFeatureFlag()`, so a single user-level call surfaces exactly one deprecation warning, not two. * `Client::getFeatureFlagResult()` and `Client::getAllFlags()` are intentionally NOT deprecated — they expose data (rich single-flag result, arbitrary key list) that the new snapshot API doesn't yet cover. `getFeatureFlagPayload()` was already marked `@deprecated` in v4.0.0 with a message pointing at `getFeatureFlagResult()`. Updated the message to point at `evaluateFlags()` and added the runtime `trigger_error` so users who pin warnings to errors (or read PHP error logs) get the heads-up. Generated-By: PostHog Code Task-Id: 1f29305a-ee56-456e-a341-8faa4eb8716d
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
Adds
evaluateFlags(), a single-call flag evaluation API that returns aFeatureFlagEvaluationssnapshot for one distinct id, and deprecates the legacy single-flag entry points in favor of it:/flagsrequest per call; reads on the snapshot make zero further requests.isEnabled($key)/getFlag($key)fire a deduped$feature_flag_calledevent with full metadata (id, version, reason, request_id, plus combined response-level errors) on first access.getFlagPayload($key)is silent.only([...])/onlyAccessed()return filtered clones for scoping which flags attach to a captured event.capture(['flags' => $snapshot, …])attaches$feature/<key>and$active_feature_flagswithout a fresh/flagsround trip; warns when bothflagsandsend_feature_flagsare passed.isFeatureEnabled(),getFeatureFlag(),getFeatureFlagPayload(), and thesend_feature_flagscapture option now emitE_USER_DEPRECATEDpointing atevaluateFlags(). Existing callers keep working unchanged until the next major.RFC · reference SDK PRs: posthog-python#539, posthog-js#3476.
Design decisions
FeatureFlagEvaluationsHostinterface (captureFlagCalledIfNeeded,logWarning) instead of a fullClient, so unit tests can use a fake without spinning up the SDK.onlyAccessed()returns an empty snapshot when nothing has been accessed (no warning, no fallback). The earlier "warn + return all flags" fallback was contradictory with the method's name and surprised callers doingcapture(flags: $snapshot->onlyAccessed())early in a request.recordAccessrather than at every call site, soisEnabled/getFlagstill return sane values without leaking events with empty actors.$feature_flag_request_id(it's per-/flags-response) but emitlocally_evaluated=trueand reason"Evaluated locally"to match the existing single-flag local path.errorsWhileComputingFlags,quota_limited) are tracked at snapshot construction and combined with per-flagflag_missingin$feature_flag_error(e.g.errors_while_computing_flags,flag_missing), matching the granularity the legacy single-flag path emits today.flags=oncapture()takes precedence oversend_feature_flags; passing both logs a warning so the conflict isn't silent.isFeatureEnabled()callsgetFeatureFlagResult()directly (skipping the publicgetFeatureFlag()) so a single user-level call surfaces exactly one deprecation warning, not two.getFeatureFlagResult()andgetAllFlags()are intentionally not deprecated — they expose data (rich single-flag result, arbitrary key list) the snapshot API doesn't yet cover.SizeLimitedHash::contains/addwere comparing values to keys and pushing onto the outer map, so the per-distinct_id$feature_flag_calleddedup never matched after the first event. The new snapshot path requires real dedup, so it's fixed here.Out of scope (explicit non-goals)
flag_definitions_loaded_atthrough the snapshot. posthog-php doesn't track that timestamp on the local poller today; left for a follow-up.Created with PostHog Code