Skip to content

fix: include group context in $feature_flag_called dedupe key#206

Merged
gustavohstrassburger merged 4 commits into
mainfrom
posthog-code/fix-flag-called-dedupe-groups
May 28, 2026
Merged

fix: include group context in $feature_flag_called dedupe key#206
gustavohstrassburger merged 4 commits into
mainfrom
posthog-code/fix-flag-called-dedupe-groups

Conversation

@gustavohstrassburger
Copy link
Copy Markdown
Contributor

@gustavohstrassburger gustavohstrassburger commented May 25, 2026

💡 Motivation and Context

In captureFlagCalledIfNeeded (posthog.go), the per-distinct_id LRU dedupe key only included (distinct_id, flag_key, device_id). For group-scoped flags this meant that when the same user was evaluated under a different group, no new $feature_flag_called event was fired — causing per-group exposure undercount for experiments scoped to a group key.

Mirrors the posthog-node fix in PostHog/posthog-js#3658 (which closes PostHog/posthog-js#3651). Both SDKs share the same dedupe shape, so backend evaluation needs the same change.

💚 How did you test it?

Added three table-style tests in feature_flag_evaluations_test.go:

  • TestCaptureFlagCalled_FiresPerGroupContext — same user, same flag, two different group contexts → two events fired, both $groups payloads observed.
  • TestCaptureFlagCalled_DedupesAcrossRepeatedCallsUnderSameGroup — repeated access under the same group → one event.
  • TestCaptureFlagCalled_DedupesAcrossGroupKeyOrder — same groups passed with different Go map insertion order → still one event (canonicalized by sort.Strings + JSON encode).

go test -count=1 ./... is green.

📝 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

In `captureFlagCalledIfNeeded`, the per-`distinct_id` LRU dedupe key only included `(distinct_id, flag_key, device_id)`. For group-scoped flags this meant that when the same user was evaluated under a different group, no new `$feature_flag_called` event was fired — causing per-group exposure undercount for experiments scoped to a group key.

Add the group context as a canonical JSON of the sorted group key/value pairs to the LRU cache key (new `groupsRepr` field on `flagUser`) so the same `(user, flag, device)` fires once per distinct group context. Repeated calls under the same group context still dedupe; calls with the same map but different Go map insertion order also dedupe (the JSON is built from a `sort.Strings`-sorted slice of keys).

Mirrors the posthog-node fix in PostHog/posthog-js#3658 (which closes PostHog/posthog-js#3651). Both SDKs share the same dedupe shape, so backend evaluation needs the same change.

Generated-By: PostHog Code
Task-Id: d94308d9-7655-4bac-8f15-c61478b5fca1
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 25, 2026

posthog-go Compliance Report

Date: 2026-05-28 15:32:17 UTC
Duration: 107265ms

⚠️ Some Tests Failed

39/45 tests passed, 6 failed


Capture Tests

29/29 tests passed

View Details
Test Status Duration
Format Validation.Event Has Required Fields 610ms
Format Validation.Event Has Uuid 608ms
Format Validation.Event Has Lib Properties 608ms
Format Validation.Distinct Id Is String 607ms
Format Validation.Token Is Present 608ms
Format Validation.Custom Properties Preserved 608ms
Format Validation.Event Has Timestamp 608ms
Retry Behavior.Retries On 503 5613ms
Retry Behavior.Does Not Retry On 400 2610ms
Retry Behavior.Does Not Retry On 401 2610ms
Retry Behavior.Respects Retry After Header 5613ms
Retry Behavior.Implements Backoff 15623ms
Retry Behavior.Retries On 500 5612ms
Retry Behavior.Retries On 502 5613ms
Retry Behavior.Retries On 504 5612ms
Retry Behavior.Max Retries Respected 15623ms
Deduplication.Generates Unique Uuids 617ms
Deduplication.Preserves Uuid On Retry 5613ms
Deduplication.Preserves Uuid And Timestamp On Retry 10618ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry 5613ms
Deduplication.No Duplicate Events In Batch 612ms
Deduplication.Different Events Have Different Uuids 610ms
Compression.Sends Gzip When Enabled 608ms
Batch Format.Uses Proper Batch Structure 608ms
Batch Format.Flush With No Events Sends Nothing 605ms
Batch Format.Multiple Events Batched Together 612ms
Error Handling.Does Not Retry On 403 2610ms
Error Handling.Does Not Retry On 413 2610ms
Error Handling.Retries On 408 5612ms

Feature_Flags Tests

⚠️ 10/16 tests passed, 6 failed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id 9ms
Request Payload.Flags Request Uses V2 Query Param 6ms
Request Payload.Flags Request Hits Flags Path Not Decide 6ms
Request Payload.Flags Request Omits Authorization Header 5ms
Request Payload.Token In Flags Body Matches Init 6ms
Request Payload.Groups Round Trip 6ms
Request Payload.Groups Default To Empty Object 6ms
Request Payload.Person Properties Distinct Id Auto Populated When Caller Omits It 6ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False 6ms
Request Payload.Disable Geoip Omitted Defaults To False 6ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key 7ms
Request Lifecycle.No Flags Request On Init Alone 4ms
Request Lifecycle.No Flags Request On Normal Capture 607ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests 9ms
Request Lifecycle.Mock Response Value Is Returned To Caller 5ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event 610ms

Failures

request_payload.request_with_person_properties_device_id

Field 'distinct_id' not found in /flags request body at path 'person_properties.distinct_id'. Available keys: ['$device_id']

request_payload.person_properties_distinct_id_auto_populated_when_caller_omits_it

Field 'distinct_id' not found in /flags request body at path 'person_properties.distinct_id'. Available keys: ['email']

request_payload.disable_geoip_false_propagates_as_geoip_disable_false

Expected geoip_disable=False, got True

request_payload.disable_geoip_omitted_defaults_to_false

Expected geoip_disable=False, got True

request_payload.flag_keys_to_evaluate_contains_only_requested_key

Field 'flag_keys_to_evaluate' not found in /flags request body at path 'flag_keys_to_evaluate'. Available keys: ['api_key', 'distinct_id', 'groups', 'person_properties', 'group_properties', 'geoip_disable']

side_effect_events.get_feature_flag_captures_feature_flag_called_event

Expected 1 events with name '$feature_flag_called', got 2

Generated-By: PostHog Code
Task-Id: d94308d9-7655-4bac-8f15-c61478b5fca1
@gustavohstrassburger gustavohstrassburger marked this pull request as ready for review May 25, 2026 20:58
@gustavohstrassburger gustavohstrassburger requested a review from a team as a code owner May 25, 2026 20:58
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 25, 2026

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
feature_flag_evaluations_test.go:693-761
**Tests 2 and 3 should be combined into a single table-driven test**

The PR description calls these "table-style tests," but `TestCaptureFlagCalled_DedupesAcrossRepeatedCallsUnderSameGroup` and `TestCaptureFlagCalled_DedupesAcrossGroupKeyOrder` are structurally identical — both set up a fresh client, issue 2-3 calls that should produce exactly one `$feature_flag_called` event, sleep, and assert on the count. Prefer a single parameterised test using `t.Run` subtests with a cases slice (e.g. `{name, calls []Groups}`) so the boilerplate is stated once and both scenarios stay together.

Reviews (1): Last reviewed commit: "chore: add changeset" | Re-trigger Greptile

Comment thread feature_flag_evaluations_test.go Outdated
Merge TestCaptureFlagCalled_DedupesAcrossRepeatedCallsUnderSameGroup
and TestCaptureFlagCalled_DedupesAcrossGroupKeyOrder into a single
parameterised test with a {name, calls []Groups} cases slice and
t.Run subtests, so the shared setup/sleep/assert boilerplate is
stated once.

Generated-By: PostHog Code
Task-Id: ad586036-ee97-4dc5-82e7-e96cb4fb9de7
The combined TestCaptureFlagCalled_DedupesAcrossSameGroupContext
relied on a 150ms sleep before asserting on captured events. On a
slow CI runner with -race enabled, the batch flush sometimes didn't
complete in time and the test saw zero events. Use the existing
waitForEventCount helper (already used by the sibling
TestCaptureFlagCalled_FiresPerGroupContext) to wait for the first
event with a real timeout, then sleep briefly to catch any
stragglers that would indicate broken deduplication, before
counting.

Generated-By: PostHog Code
Task-Id: ad586036-ee97-4dc5-82e7-e96cb4fb9de7
@gustavohstrassburger gustavohstrassburger merged commit 541b82f into main May 28, 2026
14 checks passed
@gustavohstrassburger gustavohstrassburger deleted the posthog-code/fix-flag-called-dedupe-groups branch May 28, 2026 16:30
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.

$feature_flag_called dedupe does not include group context for group-scoped flags

2 participants