feat(flags): minimize $feature_flag_called events for non-experiment flags#748
Merged
Conversation
Emit a minimal $feature_flag_called event iff the server-controlled gate is on (top-level minimalFlagCalledEvents in the v2 /flags response, or minimal_flag_called_events in the local-evaluation payload) and the evaluated flag's has_experiment is false. The minimal shape is a strict allowlist applied to the fully-enriched properties dict, so super properties, context tags, custom properties, and system context are all stripped from minimized events. Any missing signal (gate absent, legacy response shape, has_experiment missing) sends the full legacy shape. The gate is stored alongside cached flag state so it survives polls, 304 responses, and external cache round-trips. Generated-By: PostHog Code Task-Id: ffe402fd-d75c-4043-8e5d-d2fe513cac6f
Add $is_server and $geoip_disable (already present) to the strict allowlist for minimized $feature_flag_called events. Refactor has_experiment extraction into a helper function to reduce duplication and improve maintainability. Add tests for group context preservation and async cache provider with minimization gate enabled.
Contributor
posthog-python Compliance ReportDate: 2026-07-18 00:42:26 UTC ✅ All Tests Passed!111/111 tests passed Capture_V1 Tests✅ 94/94 tests passed View Details
Feature_Flags Tests✅ 17/17 tests passed View Details
|
This was referenced Jul 16, 2026
Open
Open
Open
Open
Open
haacked
marked this pull request as ready for review
July 16, 2026 20:44
Contributor
|
Reviews (1): Last reviewed commit: "Add $is_server to minimal flag called pr..." | Re-trigger Greptile |
evaluate_flags() snapshots fire $feature_flag_called on deferred access, so reading the live client gate at send time let a gate change from a LATER response reshape events for flags evaluated earlier. The gate is now captured on the snapshot at construction and passed through to the capture funnel, so each event's shape matches the evaluation that produced it. The single-flag path is unchanged: it fires immediately after evaluation, so the live gate is that evaluation's gate.
dustinbyrne
approved these changes
Jul 17, 2026
dustinbyrne
left a comment
Contributor
There was a problem hiding this comment.
Human-driven, agent-assisted review.
Reviewer feedback on PR #748 pointed out that $os, $os_version, $os_distro, $python_runtime, and $python_version were stripped from minimal $feature_flag_called events along with the rest of system context, even though they're cheap, static per process, and useful for debugging flag behavior by platform. Keep them in the allowlist.
…tion When evaluating a flag remotely, the gate controlling minimal $feature_flag_called events is now sourced from that specific response, not the client-wide gate. This ensures concurrent gate changes (e.g., from a snapshot refresh or another /flags call) can't reshape an event after the fact. The response's gate is pinned at evaluation time and carried through to event capture. Adds a test case verifying this behavior under concurrent gate updates.
Pin the gate from the response received during remote fallback, rather than reading the client-wide attribute at snapshot construction time. This ensures the gate is consistent even if the poller updates it concurrently. Add test to verify snapshot uses its response's gate, not concurrent updates.
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 dict (super properties, context tags, custom event properties, system context) on every flag call, even for the vast majority of flags that aren't linked to an experiment. This PR minimizes those events for non-experiment flags when the server enables it, cutting event size and ingestion cost without losing anything needed for exposure analysis.The event is trimmed to a strict allowlist iff the server-controlled gate is on (
minimalFlagCalledEventsin the v2/flagsresponse, orminimal_flag_called_eventsin the local-evaluation payload) and the evaluated flag'shas_experimentis exactlyfalse. Any missing signal, gate absent,has_experimentunknown, or the flag linked to an experiment, sends the full legacy shape unchanged. The gate is stored alongside cached flag state so it survives polls, 304 responses, and externalflag_cacheround-trips.The stale-cache fallback path (
_get_stale_flag_fallback) never populatesflag_details, sohas_experimentstays unknown there and those events correctly fall back to the full shape instead of risking an incorrect minimization.The reason for the server-gate is we can't just roll this out to everyone. It could break existing insights, etc. We're going to have to make an announcement first and give people time to adjust.
However, we can use the gate to roll this new behavior out for new teams (for example) after we figure out comms for that. The main goal is to establish
$feature_flag_calledas a special system event and not a general event given we don't charge for these events.💚 How did you test it?
uv run pytest posthog/test/test_feature_flag_called_minimization.py posthog/test/test_flag_definition_cache.py(49 tests). The new test file covers the gate on/off,has_experimenttrue/false/missing, local vs. remote evaluation, group context preservation, and the async cache provider with the gate enabled.📝 Checklist
If releasing new changes
sampo addto generate a changeset file