Skip to content

feat: add evaluateFlags() API for single-call flag evaluation#130

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

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

Conversation

@dmarticus
Copy link
Copy Markdown
Contributor

Summary

Phase 1 of the Server SDK Feature Flag Evaluations RFC ported to posthog-php. Mirrors the Node (posthog-js#3476) and Python (posthog-python#539) implementations.

Adds a single-call flag evaluation API:

  • Client::evaluateFlags(distinctId, …) returns a FeatureFlagEvaluations snapshot. Reads on the snapshot do not trigger additional /flags requests.
  • isEnabled($key) / getFlag($key) record access and fire a deduped \$feature_flag_called event the first time each key is touched, with the rich metadata (\$feature_flag_id, \$feature_flag_version, \$feature_flag_reason, \$feature_flag_request_id).
  • getFlagPayload($key) is silent — no event, no access tracking.
  • only([...]) and onlyAccessed() return filtered clones with their own access set, so child reads don't back-propagate. onlyAccessed() warns and falls back to the full snapshot when no flags have been accessed.
  • capture(['flags' => \$snapshot, …]) attaches \$feature/<key> and \$active_feature_flags from the snapshot — no extra /flags request. Takes precedence over the existing send_feature_flags path; send_feature_flags is not deprecated in this PR.

The single-flag dedup block in getFeatureFlagResult() is now extracted to Client::captureFlagCalledIfNeeded() and shared by both code paths against the existing per-distinct_id SizeLimitedHash cache.

Pre-flight answers

  1. Existing single-flag dedup helper? Inline block in getFeatureFlagResult() — extracted in this PR to Client::captureFlagCalledIfNeeded(). Cache: Client::\$distinctIdsFeatureFlagsReported (cap 50,000).
  2. Rich /flags response handler? Client::flags() already returns the v4 shape (flags[\$key]['metadata']['{id,version,payload}'], flags[\$key]['reason']['description'], top-level requestId, evaluatedAt). No new lower-level call needed.
  3. Local evaluation poller? Yes — Client::loadFlags() polls /api/feature_flag/local_evaluation with ETag/304 support. There is no flag_definitions_loaded_at timestamp in posthog-php today, so locally-evaluated records leave it null. Adding it is left for a follow-up.
  4. Capture options convention? Client::capture(array \$message) takes a single associative array; the new flags parameter slots in as a top-level key.
  5. Snapshot module path? New file lib/FeatureFlagEvaluations.php (PSR-4 → PostHog\\FeatureFlagEvaluations), with companions lib/EvaluatedFlagRecord.php and lib/FeatureFlagEvaluationsHost.php.

Pre-existing dedup bug fix

SizeLimitedHash::contains/add used array_key_exists to compare values to keys, so the per-distinct_id \$feature_flag_called dedup never actually matched after the first event. Existing tests only ever made a single call per (flag, distinct_id), so the bug was invisible. The new snapshot path requires real dedup; this PR fixes both helpers to operate on a per-key set as intended.

Phase 2 — follow-up

Phase 2 (a separate minor) will deprecate the legacy methods now superseded by evaluateFlags():

  • Client::getFeatureFlag() / PostHog::getFeatureFlag()
  • Client::isFeatureEnabled() / PostHog::isFeatureEnabled()
  • Client::getFeatureFlagResult() / PostHog::getFeatureFlagResult()
  • Client::getAllFlags() / PostHog::getAllFlags()
  • The send_feature_flags capture parameter

Client::getFeatureFlagPayload() (already deprecated) and the rich-result-for-one-flag method (getFeatureFlagResult()) are intentionally not removed in either phase — there is no public equivalent on the new API for fetching a single flag's full result.

Test plan

  • vendor/bin/phpunit --filter FeatureFlagEvaluationsTest — 19 new tests, 65 assertions, all passing
  • vendor/bin/phpunit — full suite passes (1 pre-existing unrelated failure in `testLoadFeatureFlagsWrongKey` reproduces on the upstream branch this is based on)
  • `vendor/bin/phpcs` — no new errors on touched files
  • Smoke-test against a real PostHog project: one `/flags` request, `$feature_flag_called` fires once per accessed flag with rich metadata, captured `page_view` event has the expected `$feature/*` properties

Created with PostHog Code

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

Superseded by #131, which is rebased onto current main. Closing this stale branch — same diff lives at #131.

@dmarticus dmarticus closed this Apr 27, 2026
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