feat: add OpenFeature provider - #214
Conversation
Add PostHog.OpenFeature.Provider for the Elixir OpenFeature SDK, with open_feature as an optional dependency. The provider mirrors the posthog-node OpenFeature provider: targeting_key maps to distinct_id, groups and group_properties map to groups, and other context keys map to person properties. Generated-By: PostHog Desktop Task-Id: 45365cd7-19ef-46e1-8d2a-46d2f484d229
|
The PR appears safe to merge; the prior disabled-flag resolution defect is fully fixed and no new actionable issue remains. Reviews (2) · Last reviewed commit: "docs: describe disabled-flag resolution ..." |
posthog-elixir Compliance ReportDate: 2026-09-25T12:52:28.246518+00:00
|
| Test | Status | Duration |
|---|---|---|
| Format Validation.Event Has Required Fields | ✅ | 610ms |
| Format Validation.Event Has Uuid | ✅ | 611ms |
| Format Validation.Event Has Lib Properties | ✅ | 612ms |
| Format Validation.Distinct Id Is String | ✅ | 611ms |
| Format Validation.Token Is Present | ✅ | 611ms |
| Format Validation.Custom Properties Preserved | ✅ | 610ms |
| Format Validation.Event Has Timestamp | ✅ | 611ms |
| Format Validation.Non Utc Event Timestamp Is Converted To Utc | ❌ | 610ms |
| Retry Behavior.Retries On 503 | ✅ | 5616ms |
| Retry Behavior.Does Not Retry On 400 | ✅ | 2614ms |
| Retry Behavior.Does Not Retry On 401 | ✅ | 2612ms |
| Retry Behavior.Respects Retry After Header | ✅ | 5613ms |
| Retry Behavior.Implements Backoff | ✅ | 15627ms |
| Retry Behavior.Retries On 500 | ✅ | 5617ms |
| Retry Behavior.Retries On 502 | ✅ | 5616ms |
| Retry Behavior.Retries On 504 | ✅ | 5617ms |
| Retry Behavior.Max Retries Respected | ✅ | 15623ms |
| Deduplication.Generates Unique Uuids | ✅ | 624ms |
| Deduplication.Preserves Uuid On Retry | ✅ | 5616ms |
| Deduplication.Preserves Uuid And Timestamp On Retry | ✅ | 10622ms |
| Deduplication.Preserves Uuid And Timestamp On Batch Retry | ✅ | 5618ms |
| Deduplication.No Duplicate Events In Batch | ✅ | 617ms |
| Deduplication.Different Events Have Different Uuids | ✅ | 614ms |
| Compression.Sends Gzip When Enabled | ✅ | 611ms |
| Batch Format.Uses Proper Batch Structure | ✅ | 611ms |
| Batch Format.Flush With No Events Sends Nothing | ✅ | 608ms |
| Batch Format.Multiple Events Batched Together | ✅ | 617ms |
| Error Handling.Does Not Retry On 403 | ✅ | 2613ms |
| Error Handling.Does Not Retry On 413 | ✅ | 2613ms |
| Error Handling.Retries On 408 | ✅ | 5615ms |
Failures
format_validation.non_utc_event_timestamp_is_converted_to_utc
Event 0 field 'timestamp' instant '2026-09-25T12:50:44.133245Z' != expected '2025-01-02T03:04:05Z'
Feature_Flags Tests
View Details
| Test | Status | Duration |
|---|---|---|
| Request Payload.Request With Person Properties Device Id | ✅ | 9ms |
| Request Payload.Flags Request Uses V2 Query Param | ✅ | 9ms |
| Request Payload.Flags Request Hits Flags Path Not Decide | ✅ | 8ms |
| Request Payload.Flags Request Omits Authorization Header | ✅ | 9ms |
| Request Payload.Token In Flags Body Matches Init | ✅ | 8ms |
| Request Payload.Groups Round Trip | ✅ | 9ms |
| Request Payload.Groups Default To Empty Object | ❌ | 8ms |
| Request Payload.Disable Geoip False Propagates As Geoip Disable False | ✅ | 8ms |
| Request Payload.Disable Geoip Omitted Defaults To False | ❌ | 9ms |
| Request Payload.Flag Keys To Evaluate Contains Only Requested Key | ✅ | 8ms |
| Request Lifecycle.No Flags Request On Init Alone | ✅ | 4ms |
| Request Lifecycle.No Flags Request On Normal Capture | ✅ | 611ms |
| Request Lifecycle.Two Flag Calls Produce Two Remote Requests | ✅ | 12ms |
| Request Lifecycle.Mock Response Value Is Returned To Caller | ❌ | 7ms |
| Retry Behavior.Retries Flags On 502 | ❌ | 311ms |
| Retry Behavior.Retries Flags On 504 | ❌ | 312ms |
| Side Effect Events.Get Feature Flag Captures Feature Flag Called Event | ❌ | 611ms |
Failures
request_payload.groups_default_to_empty_object
Field 'groups' not found in /flags request body at path 'groups'. Available keys: ['distinct_id', 'api_key', 'flag_keys_to_evaluate']
request_payload.disable_geoip_omitted_defaults_to_false
Field 'geoip_disable' not found in /flags request body at path 'geoip_disable'. Available keys: ['distinct_id', 'api_key', 'flag_keys_to_evaluate']
request_lifecycle.mock_response_value_is_returned_to_caller
Last action result missing field 'value'. Keys: ['error', 'success']
retry_behavior.retries_flags_on_502
Last action result missing field 'value'. Keys: ['error', 'success']
retry_behavior.retries_flags_on_504
Last action result missing field 'value'. Keys: ['error', 'success']
side_effect_events.get_feature_flag_captures_feature_flag_called_event
Expected 1 events with name '$feature_flag_called', got 0
…shot The string, number, and map resolvers now check `enabled: false` before inspecting the variant or payload, so a disabled flag that still carries a remote value resolves to the caller-supplied default. Adds a test for each type. Regenerates public_api.snapshot to include PostHog.OpenFeature.Provider, which the "Compile with warnings as errors" CI job checks. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Generated-By: PostHog Desktop Task-Id: 8167c421-7e51-4669-be55-fc7b39e39d26
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Generated-By: PostHog Desktop Task-Id: 8167c421-7e51-4669-be55-fc7b39e39d26
|
Want your agent to iterate on Greptile's feedback? Try greploops. |
Cross-provider comparisonCompared this PR at
In particular, please confirm the intended disabled-flag behavior and update the differences section so users do not expect exact Node parity. |
…ariants A disabled flag that still carries a variant or payload now resolves to that value with reason `:default`, as the Node and Python providers do. Number resolution accepts `.5`, `-.5`, and `1.`, which JavaScript's Number() and Python's float() accept but Float.parse/1 does not. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Generated-By: PostHog Desktop Task-Id: 8167c421-7e51-4669-be55-fc7b39e39d26
|
Thanks for the comparison. Addressed in 1fe7e5c:
Documented as intentional differences in the PR description's table rather than changed:
The "same rules as |
approved to unblock |
* feat: extend OpenFeature numeric, payload, and reason support * docs: explain upstream OpenFeature array-default limitation * fix: preserve OpenFeature reason metadata on type mismatches
| {:ok, distinct_id} -> | ||
| body = flags_body(distinct_id, key, context) | ||
|
|
||
| case FeatureFlags.evaluate_flags(provider.supervisor_name, body) do |
There was a problem hiding this comment.
Low: Evaluation snapshots leak Agent processes
evaluate_flags/2 creates a snapshot whose accessed_pid is a newly linked Agent, but this provider discards the snapshot without stopping that Agent. An attacker who can repeatedly trigger flag checks handled by the same long-lived process can grow its linked process set until BEAM resources are exhausted; this also occurs when send_feature_flag_events is false. Stop the snapshot's Agent in an after block once the result and optional event have been processed, or use a single-flag evaluation path that does not allocate an access tracker.
PR overviewThis pull request adds an OpenFeature provider backed by PostHog, including feature flag evaluation and optional evaluation event handling. One resource-management issue remains open: each evaluation snapshot can leave behind a linked Agent process. If an attacker can repeatedly trigger flag checks in the same long-lived process, these leaked processes could accumulate and eventually exhaust BEAM resources; no issues have yet been addressed. Open issues (1)
Fixed/addressed: 0 · PR risk: 4/10 |
💡 Motivation and Context
posthog-js has OpenFeature providers (
@posthog/openfeature-node-providerand@posthog/openfeature-web-provider). posthog-elixir has none. This PR addsPostHog.OpenFeature.Providerfor the Elixir OpenFeature SDK. Its behavior is as close as possible to the node provider.open_featureis an optional dependency. The provider module compiles only whenopen_featureis available, so users who do not use OpenFeature see no change.PostHog.FeatureFlags.evaluate_flags/2withflag_keys: [key]. It does not use the deprecated single-flag functions.Behavior that is the same as the node provider
targeting_keybecomes the PostHogdistinct_id. Thedefault_distinct_idoption is the fallback. If neither is there, the result is:targeting_key_missing.groupsandgroup_propertieskeys go to groups. All other context keys go to person properties, with no change to their types. Empty maps are not sent.mapping.ts, including hexadecimal numeric variants and JSON object or array payloads. A disabled flag that still carries a variant or payload returns that value. Reasons are:targeting_matchfor enabled flags and:defaultfor off results, except an explicit PostHogflag_disabledreason code produces:disabled. An enabled flag without a usable value is:type_mismatch. Unknown flags are:flag_not_found.send_feature_flag_eventsistrueby default.Differences
targeting_key,group_properties). Atom and string keys both work.targetingKey,groupPropertiessupervisor_nameoption for named instances:type_mismatch,:targeting_key_missingreason: :errorand the error code, soafterhooks run, noterrorhooks.:generalFLAG_NOT_FOUNDinitializereloadFeatureFlags()%{}), even for arrays;get_map_value/4can return a list..5,1., exponents such as1e3, and unsigned hexadecimal integers such as0x10.Number(variant), which also accepts hex:targeting_matchfor enabled flags;:disabledfor off results with the explicitflag_disabledreason code, otherwise:default.flag_metadata["posthog_reason"]preserves the PostHog reason description, falling back to its code, or a string reason from local evaluation. Metadata is also preserved on type mismatches.TARGETING_MATCHorDEFAULT; no PostHog-specific reason metadataThere is no sdk-specs capability for OpenFeature.
💚 How did you test it?
Original provider validation in Docker with Elixir 1.18.3 / OTP 27 (the CI versions), before the follow-up in #216:
mix format --check-formatted,mix credo --strict, andMIX_ENV=test mix compile --warnings-as-errorspass.mix test: 581 tests, 0 failures. This includes 42 new provider tests. They cover each flag type with the flag on and off, the error cases, the distinct ID and context mapping,$feature_flag_calledevents, and use through the real OpenFeature client.open_feature,mix compile --warnings-as-errorspasses, and no OpenFeature module is compiled.The follow-up in #216 adds regression coverage for hexadecimal variants, JSON arrays (including through the real OpenFeature client), explicit disabled reasons, and PostHog reason metadata, including on type mismatches.
The changeset cleanup does not change runtime code;
git diff --cached --checkpasses. Tests were not rerun for this cleanup.I did not try the provider against a real PostHog project.
📝 Checklist
If releasing new changes
sampo addto generate a changeset file🤖 Agent context
Autonomy: Human-driven (agent-assisted)
:posthogwith an optional dependency (not a separate Hex package), and snake_case context keys.Created with PostHog Desktop
🤖 Generated with Claude Code