feat(flags): minimize $feature_flag_called events for non-experiment flags - #166
Merged
Conversation
Emit a minimal, allowlisted $feature_flag_called event when the /flags response carries the top-level minimalFlagCalledEvents gate and the evaluated flag reports has_experiment: false. Any missing signal (gate absent or unparseable, has_experiment unknown, experiment-linked flag) keeps the full legacy event shape. The gate is stored on each FeatureFlags.Result so events fired later from an Evaluations snapshot respect it. Minimal events are assembled the same way capture/3 would (context, then global properties) and then projected onto the allowlist, so context tags and customer global properties are stripped while $groups and $process_person_profile survive when set. Generated-By: PostHog Code Task-Id: ffe402fd-d75c-4043-8e5d-d2fe513cac6f
Contributor
posthog-elixir Compliance ReportDate: 2026-07-31 00:40:02 UTC
|
| Test | Status | Duration |
|---|---|---|
| Format Validation.Event Has Required Fields | ✅ | 610ms |
| Format Validation.Event Has Uuid | ✅ | 611ms |
| Format Validation.Event Has Lib Properties | ✅ | 610ms |
| Format Validation.Distinct Id Is String | ✅ | 610ms |
| Format Validation.Token Is Present | ✅ | 610ms |
| Format Validation.Custom Properties Preserved | ✅ | 610ms |
| Format Validation.Event Has Timestamp | ✅ | 610ms |
| Retry Behavior.Retries On 503 | ✅ | 5615ms |
| Retry Behavior.Does Not Retry On 400 | ✅ | 2612ms |
| Retry Behavior.Does Not Retry On 401 | ✅ | 2612ms |
| Retry Behavior.Respects Retry After Header | ✅ | 5615ms |
| Retry Behavior.Implements Backoff | ✅ | 15626ms |
| Retry Behavior.Retries On 500 | ✅ | 5615ms |
| Retry Behavior.Retries On 502 | ✅ | 5616ms |
| Retry Behavior.Retries On 504 | ✅ | 5615ms |
| Retry Behavior.Max Retries Respected | ❌ | 15625ms |
| Deduplication.Generates Unique Uuids | ✅ | 621ms |
| Deduplication.Preserves Uuid On Retry | ❌ | 5615ms |
| Deduplication.Preserves Uuid And Timestamp On Retry | ❌ | 10621ms |
| Deduplication.Preserves Uuid And Timestamp On Batch Retry | ❌ | 5617ms |
| Deduplication.No Duplicate Events In Batch | ✅ | 615ms |
| Deduplication.Different Events Have Different Uuids | ✅ | 613ms |
| Compression.Sends Gzip When Enabled | ✅ | 611ms |
| Batch Format.Uses Proper Batch Structure | ✅ | 610ms |
| Batch Format.Flush With No Events Sends Nothing | ✅ | 607ms |
| Batch Format.Multiple Events Batched Together | ✅ | 615ms |
| Error Handling.Does Not Retry On 403 | ✅ | 2611ms |
| Error Handling.Does Not Retry On 413 | ✅ | 2613ms |
| Error Handling.Retries On 408 | ✅ | 5615ms |
Failures
retry_behavior.max_retries_respected
Expected 4 requests, got 6
deduplication.preserves_uuid_on_retry
UUIDs changed on retry: ['019fb59c-6d96-78ea-bea9-2326c1c56a1e'] != []
deduplication.preserves_uuid_and_timestamp_on_retry
UUIDs changed on retry: ['019fb59c-8386-77fb-ac57-99d887aff9cd'] != []
deduplication.preserves_uuid_and_timestamp_on_batch_retry
UUIDs changed on retry: ['019fb59c-ad06-75b5-9519-5f815f35dfb8', '019fb59c-ad04-7dce-b22f-f0f84245d47c', '019fb59c-ad03-7345-8bb2-a0b7ac71ea49'] != []
Feature_Flags Tests
View Details
| Test | Status | Duration |
|---|---|---|
| Request Payload.Request With Person Properties Device Id | ✅ | 610ms |
| Request Payload.Flags Request Uses V2 Query Param | ✅ | 609ms |
| Request Payload.Flags Request Hits Flags Path Not Decide | ✅ | 610ms |
| Request Payload.Flags Request Omits Authorization Header | ✅ | 609ms |
| Request Payload.Token In Flags Body Matches Init | ✅ | 609ms |
| Request Payload.Groups Round Trip | ✅ | 609ms |
| Request Payload.Groups Default To Empty Object | ✅ | 609ms |
| Request Payload.Disable Geoip False Propagates As Geoip Disable False | ✅ | 609ms |
| Request Payload.Disable Geoip Omitted Defaults To False | ✅ | 609ms |
| Request Payload.Flag Keys To Evaluate Contains Only Requested Key | ✅ | 609ms |
| Request Lifecycle.No Flags Request On Init Alone | ✅ | 4ms |
| Request Lifecycle.No Flags Request On Normal Capture | ✅ | 609ms |
| Request Lifecycle.Two Flag Calls Produce Two Remote Requests | ✅ | 1215ms |
| Request Lifecycle.Mock Response Value Is Returned To Caller | ✅ | 609ms |
| Retry Behavior.Retries Flags On 502 | ❌ | 312ms |
| Retry Behavior.Retries Flags On 504 | ❌ | 311ms |
| Side Effect Events.Get Feature Flag Captures Feature Flag Called Event | ✅ | 1211ms |
Failures
retry_behavior.retries_flags_on_502
500, message='Internal Server Error', url='http://sdk-adapter:8080/get_feature_flag'
retry_behavior.retries_flags_on_504
500, message='Internal Server Error', url='http://sdk-adapter:8080/get_feature_flag'
haacked
marked this pull request as ready for review
July 18, 2026 04:28
dustinbyrne
reviewed
Jul 20, 2026
Contributor
|
This PR hasn't seen activity in a week! Should it be merged, closed, or further worked on? If you want to keep it open, post a comment or remove the |
The Plug integration stores the x-posthog-session-id header in event context as $session_id, but the minimal event allowlist dropped it, so gated non-experiment flag calls lost their session linkage. The cross-SDK contract keeps $session_id alongside the other linkage and SDK identity properties.
dustinbyrne
approved these changes
Jul 31, 2026
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.
💡 Motivation and Context
$feature_flag_calledevents carry the full enriched properties (event context, global properties, system context) on every flag call, even for flags not linked to an experiment. This PR trims those events to a strict allowlist iff the server-controlled gate is on (top-levelminimalFlagCalledEventsin the v2/flagsresponse) and the flag'shas_experimentis exactlyfalse. Any missing signal (gate absent or unparseable,has_experimentunknown, experiment-linked flag) sends the full legacy shape unchanged.Server-gated because rolling this out unconditionally could break existing insights; announcement/comms come before enablement. The goal is to establish
$feature_flag_calledas a special system event.Part of a cross-SDK rollout; reference implementation and fuller context: PostHog/posthog-python#748.
How it works in this SDK
/flagsremote path applies: the gate is read from the top-levelminimalFlagCalledEventsfield of the v2 response.PostHog.FeatureFlags.Result(alongside the other response-level fields likerequest_idanderrors_while_computing), so events fired later from anEvaluationssnapshot respect it per flag.capture/3+bare_capture/4) and then projected onto the allowlist viaMap.takewith a dual atom/string key allowlist. Kept: flag identity and evaluation metadata ($feature_flag,$feature_flag_response,$feature_flag_has_experiment,$feature_flag_id,$feature_flag_version,$feature_flag_reason,$feature_flag_request_id,$feature_flag_evaluated_at,$feature_flag_errorwhen set),$groups,$process_person_profile,$session_id,$lib,$lib_version, and$is_server. Everything else is stripped, including the$feature/<key>property, context tags, and customer global properties.before_sendstill runs after the projection and may re-inflate the event — the accepted customer escape hatch; the SDK itself adds nothing after the allowlist.capture_prepared/4that skips re-enrichment;bare_capture/4behavior is unchanged and the public API snapshot is untouched.💚 How did you test it?
mix test— 346 tests, 0 failures (18 integration-tagged excluded)mix format --check-formatted— cleanmix credo --strict— 0 issuesmix compile --warnings-as-errors— cleanmix posthog.public_api --check— snapshot up to dateNote: run locally on Elixir 1.18.3 / OTP 27.3.3 (
.tool-versionspins 1.20.0-otp-29, which wasn't installed locally); CI runs the pinned toolchain.Coverage added:
false/ unparseable ("yes") → full; gated buthas_experimentmissing → full.$is_server: true,$lib,$lib_version, and$feature_flag_errorwhen the response signals errors), so any extra or missing property fails the test.x-posthog-session-idrun throughPostHog.Integrations.Plug, asserting$session_idsurvives minimization while$current_url/$host/$pathname/$ip/$request_method/$user_agentare stripped.$groups/$process_person_profilesurvive the allowlist while string-keyed junk is stripped (pins the string half of the dual allowlist).Evaluationssnapshot flow: per-flag gate behavior from one snapshot (minimal for the non-experiment flag, full for the experiment flag) plus the missing-flag full-shape fallback.A minor changeset is included (
.sampo/changesets/mellow-ibex-sointu.md).📝 Checklist
If releasing new changes
sampo addto generate a changeset fileCreated with PostHog Code