feat(flags): minimize $feature_flag_called events for non-experiment flags#261
Merged
Conversation
When the /flags?v=2 response carries top-level minimalFlagCalledEvents or the local-evaluation definitions payload carries minimal_flag_called_events, and the evaluated flag has has_experiment=false, serialize the $feature_flag_called event with a strict allowlist of evaluation properties and skip system context, DefaultEventProperties, and the snapshot path's $feature/<key> and payload extras. Any missing signal (gate absent, has_experiment unknown, experiment-linked flag, legacy response shapes) keeps the full event shape. The local-eval gate is cached in the poller's atomic state alongside flag definitions and survives 304 responses; quota-limit resets clear it. Generated-By: PostHog Code Task-Id: ffe402fd-d75c-4043-8e5d-d2fe513cac6f
Contributor
posthog-go Compliance ReportDate: 2026-07-18 04:27:17 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
|
Reviewer feedback on the Python equivalent of this PR pointed out that $os, $os_version, $os_distro, and $go_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. Merge system context into the minimal event the same way the full event does, for both the v0 and v1 wire formats.
haacked
marked this pull request as ready for review
July 18, 2026 00:51
Extract Capture.selectedProperties() to remove the duplicated minimal/full branching between APIfy (v0) and apifyEvent (v1). Add a direct test locking the full 17-entry minimal allowlist contract, and harden the 304 gate- persistence test to fail loudly on unexpected requests.
dustinbyrne
approved these changes
Jul 20, 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 (Config.DefaultEventProperties, request context, 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 (minimalFlagCalledEventsin the v2/flagsresponse, orminimal_flag_called_eventsin the local-evaluation definitions payload) and the flag'shas_experimentis exactlyfalse. Any missing signal — gate absent,has_experimentunknown, experiment-linked flag, legacy response shapes — sends the full legacy shape unchanged.This is server-gated because rolling it out unconditionally could break existing insights; announcement/comms come before enablement. The goal is to establish
$feature_flag_calledas a special system event.Implementation specifics for this SDK:
flagsStateswaps: lock-free reads, preserved across 304 Not Modified refreshes, reset on quota-limit. When local evaluation is inconclusive and the SDK falls back to a remote/flagsrequest, the gate is reset and re-read from that response, since it is now the source of the flag value./batchand capture-v1) from one shared allowlist, soDefaultEventProperties, request-context properties, system context, and the snapshot path's$feature/<key>/$feature_flag_payloadextras are excluded structurally rather than deleted piecemeal.$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_error,locally_evaluated,$groups,$process_person_profile,$geoip_disable,$is_server,$session_id,$window_id,$device_id, plus$lib/$lib_versionadded at serialization.$geoip_disableand$is_serverstay because they are processing-control/classification sentinels; the session/window/device IDs are linkage identifiers.BeforeSendhook replaces the message, the minimal marker is lost and the event degrades fail-safe to the full shape.Part of a cross-SDK rollout; reference implementation and fuller context: PostHog/posthog-python#748.
💚 How did you test it?
go test -race -count=1 -timeout=5m ./...— okgo vet ./...— cleangofmt -l .— cleanbin/api-diff— public API snapshot up to dateTest coverage added:
/flagsresponse and local-evaluation definitions payload): gated + no experiment, gated + experiment, gated + unknownhas_experiment, ungated.EvaluateFlagssnapshot paths), so any property leaking into — or dropping out of — the minimal shape fails the test.$session_idtop-level lift on a minimal event and$groupsretention.A changeset (
.changeset/minimal-flag-called-events.md, minor) documents the behavior.📝 Checklist
If releasing new changes
pnpm changesetto generate a changeset fileCreated with PostHog Code