Skip to content

feat(flags): minimize $feature_flag_called events for non-experiment flags#220

Open
haacked wants to merge 3 commits into
mainfrom
haacked/minimal-flag-called-events
Open

feat(flags): minimize $feature_flag_called events for non-experiment flags#220
haacked wants to merge 3 commits into
mainfrom
haacked/minimal-flag-called-events

Conversation

@haacked

@haacked haacked commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

$feature_flag_called events carry the full enriched properties dict (rails request context, super/context properties, custom event properties) 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 (minimalFlagCalledEvents in the v2 /flags response, or top-level minimal_flag_called_events in the local-evaluation flag definitions payload) and the evaluated flag's has_experiment is exactly false. Any missing signal (gate absent, has_experiment unknown, legacy response shape, request failure) 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_called as a special system event.

Implementation notes:

  • Single enforcement point: minimal events are built by slicing the allowlist in Client#capture after context merge and FieldParser assembly, so context properties and parser-added metadata can never leak in. Kept: $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, $session_id, $is_server, $lib, $lib_version (string and symbol forms).
  • Local-evaluation gate lives on the poller: refreshed on each definitions poll, preserved across 304 Not Modified responses, reset when flags are unset on quota limit, and persisted through flag_definition_cache_provider implementations so it survives restarts in multi-worker setups.
  • Remote evaluation reads the gate per /flags response. For mixed snapshots (evaluate_flags), the team-level response gate supersedes the poller gate; a response without the gate fails the whole snapshot safe to full events.
  • Both $feature_flag_called emit paths are covered: the legacy single-flag path (get_feature_flag_result and friends) and the snapshot path (evaluate_flags / FeatureFlagEvaluations).

Part of a cross-SDK rollout; reference implementation and fuller context: PostHog/posthog-python#748

💚 How did you test it?

  • bundle exec rspec — 630 examples, 0 failures
  • bundle exec rubocop — 82 files inspected, no offenses
  • bundle exec rake public_api:check — passing (no public API surface changes)

Coverage added:

  • Gate matrix from both signal sources (local-eval definitions payload and v2 /flags response), across both emit paths: gated + no experiment → minimal; gated + experiment → full; gate missing / has_experiment missing → full.
  • Exact-key-set assertions on minimal events, including $session_id and $is_server retention and stripping of context properties, $feature/<key>, and payload props.
  • Regression test proving a caller-supplied _minimal_flag_called_event key inside capture properties cannot trigger minimization.
  • Cache-provider tests simulating restart: gate stored on fetch, applied when definitions load from cached data.

Released as a minor changeset for posthog-ruby.

📝 Checklist

  • I reviewed the submitted code.
  • I added tests to verify the changes.
  • I updated the docs if needed.
  • No breaking change or entry added to the changelog.

If releasing new changes

  • Ran pnpm changeset to generate a changeset file

Created with PostHog Code

@haacked haacked self-assigned this Jul 16, 2026
@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown

posthog-ruby Compliance Report

Date: 2026-07-18 05:22:04 UTC
Duration: 98183ms

✅ All Tests Passed!

46/46 tests passed


Capture Tests

29/29 tests passed

View Details
Test Status Duration
Format Validation.Event Has Required Fields 110ms
Format Validation.Event Has Uuid 107ms
Format Validation.Event Has Lib Properties 109ms
Format Validation.Distinct Id Is String 107ms
Format Validation.Token Is Present 107ms
Format Validation.Custom Properties Preserved 107ms
Format Validation.Event Has Timestamp 108ms
Retry Behavior.Retries On 503 5314ms
Retry Behavior.Does Not Retry On 400 2109ms
Retry Behavior.Does Not Retry On 401 2109ms
Retry Behavior.Respects Retry After Header 8113ms
Retry Behavior.Implements Backoff 15523ms
Retry Behavior.Retries On 500 5212ms
Retry Behavior.Retries On 502 5212ms
Retry Behavior.Retries On 504 5214ms
Retry Behavior.Max Retries Respected 15510ms
Deduplication.Generates Unique Uuids 112ms
Deduplication.Preserves Uuid On Retry 5212ms
Deduplication.Preserves Uuid And Timestamp On Retry 10317ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry 5215ms
Deduplication.No Duplicate Events In Batch 114ms
Deduplication.Different Events Have Different Uuids 109ms
Compression.Sends Gzip When Enabled 109ms
Batch Format.Uses Proper Batch Structure 106ms
Batch Format.Flush With No Events Sends Nothing 5ms
Batch Format.Multiple Events Batched Together 110ms
Error Handling.Does Not Retry On 403 2110ms
Error Handling.Does Not Retry On 413 2109ms
Error Handling.Retries On 408 5213ms

Feature_Flags Tests

17/17 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id 107ms
Request Payload.Flags Request Uses V2 Query Param 107ms
Request Payload.Flags Request Hits Flags Path Not Decide 106ms
Request Payload.Flags Request Omits Authorization Header 107ms
Request Payload.Token In Flags Body Matches Init 106ms
Request Payload.Groups Round Trip 107ms
Request Payload.Groups Default To Empty Object 108ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False 109ms
Request Payload.Disable Geoip Omitted Defaults To False 109ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key 107ms
Request Lifecycle.No Flags Request On Init Alone 3ms
Request Lifecycle.No Flags Request On Normal Capture 105ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests 110ms
Request Lifecycle.Mock Response Value Is Returned To Caller 106ms
Retry Behavior.Retries Flags On 502 209ms
Retry Behavior.Retries Flags On 504 253ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event 109ms

Emit a slim $feature_flag_called event when the server enables the
minimal_flag_called_events gate and the evaluated flag has no linked
experiment. Minimal events keep a strict allowlist of flag-evaluation
properties and strip everything else, including context properties,
$feature/<key>, payloads, and system metadata. The gate is read from the
top-level minimalFlagCalledEvents field of the v2 /flags response and
the top-level minimal_flag_called_events key of the local evaluation
definitions payload, and is persisted through the flag definition cache
provider. Any missing signal fails safe to the full event.

Generated-By: PostHog Code
Task-Id: ffe402fd-d75c-4043-8e5d-d2fe513cac6f
@haacked
haacked force-pushed the haacked/minimal-flag-called-events branch from 8881c0a to 6966273 Compare July 18, 2026 01:01
haacked added 2 commits July 17, 2026 18:04
Add a regression test for the mixed-snapshot case where the local poller's
gate is true but the remote /flags response omits the field, the same bug
class that shipped twice in the Python port of this feature. Add a missing
304-persistence test for the gate, dedupe a duplicated test helper, and
document cache-provider backward compatibility.
@haacked
haacked marked this pull request as ready for review July 18, 2026 05:22
@haacked
haacked requested a review from a team as a code owner July 18, 2026 05:22
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