Skip to content

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

Merged
haacked merged 2 commits into
mainfrom
haacked/minimal-flag-called-events
Jul 20, 2026
Merged

feat(flags): minimize $feature_flag_called events for non-experiment flags#263
haacked merged 2 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 (context properties, super 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 minimal_flag_called_events in the local-evaluation payload) and the flag's HasExperiment is exactly false. Any missing signal (gate absent, HasExperiment unknown, legacy response shapes, parse 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:

  • The gate is stored on FlagsResult, so it persists alongside cached flags via IFeatureFlagCache, and on the LocalEvaluator, which the poller already swaps whole via Interlocked.Exchange; the gate lives with the poll state and needs no extra synchronization.
  • Mixed local+remote snapshots from EvaluateFlagsAsync carry the gate per record, read from whichever source evaluated that flag.
  • The minimal event is built by construction from the allowlist in CaptureCore, after all property merging (context properties, $groups, super properties, $is_server), so nothing merged upstream can leak in.
  • A caller's explicit $geoip_disable value is preserved on minimal events; when unset, the usual default applies.
  • Includes a changeset (minor) and a PublicAPI.Unshipped.txt entry for the new FlagsResult.MinimalFlagCalledEvents property.

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

💚 How did you test it?

  • dotnet test -f net8.0: 957 passed / 2 skipped (UnitTests), 49 passed (UnitTests.AspNetCore), 19 passed (PostHog.AI.Tests), 0 failed. The netcoreapp3.1 target compiles but cannot execute on ARM hosts; CI covers it.
  • bin/fmt --check: clean.
  • New coverage: gate x has_experiment matrix as theories for both signal sources (/flags v2 and local evaluation), exact-key-set assertions on minimal events, $groups retention when evaluating with a group collection, explicit $geoip_disable preservation, and per-record gating on a mixed local+remote snapshot.

📝 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

Emit a minimal $feature_flag_called event when the server enables the
gate (minimalFlagCalledEvents in the /flags v2 response, or
minimal_flag_called_events in the local evaluation payload) and the
evaluated flag reports has_experiment == false. Minimal events keep a
strict allowlist of flag evaluation properties and strip everything
else, including the $feature/<key> enumeration and super properties.
Any missing signal (gate absent, has_experiment unknown, legacy
response shapes) sends the full event, exactly as before.

Generated-By: PostHog Code
Task-Id: ffe402fd-d75c-4043-8e5d-d2fe513cac6f
@haacked haacked self-assigned this Jul 16, 2026
@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

posthog-dotnet Compliance Report

Date: 2026-07-18 04:24:30 UTC
Duration: 4482ms

✅ All Tests Passed!

17/17 tests passed


Feature_Flags Tests

17/17 tests passed

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

@haacked
haacked marked this pull request as ready for review July 18, 2026 00:51
@haacked
haacked requested a review from a team as a code owner July 18, 2026 00:51
Extract ShouldSendMinimalFeatureFlagCalledEvent(gate, hasExperiment) to
remove the duplicated inline rule at both call sites. Add coverage for the
snapshot path's fallback to the full event when has_experiment is unknown,
and for $feature_flag_error surviving the minimal-event allowlist.
@haacked
haacked merged commit dd92ea1 into main Jul 20, 2026
20 checks passed
@haacked
haacked deleted the haacked/minimal-flag-called-events branch July 20, 2026 17:04
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.

2 participants