Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/include-group-context-in-flag-called-dedupe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'posthog-ruby': patch
---

Include group context in the `$feature_flag_called` dedupe key so group-scoped flags fire a separate event for each group a user is evaluated under, instead of being dedup-ed against the first group context the same `(distinct_id, flag, response)` was seen under.
14 changes: 13 additions & 1 deletion lib/posthog/client.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true

require 'time'
require 'json'
require 'securerandom'

require 'posthog/defaults'
Expand Down Expand Up @@ -760,11 +761,22 @@ def property_key?(properties, key)
# Shared by the legacy single-flag path ({#get_feature_flag_result}) and the
# snapshot's access-recording. Owns dedup-key construction, the
# per-distinct_id sent-flags cache, and the `$feature_flag_called` capture call.
# Group context is included in the dedup key so group-scoped flags fire a
# separate event for each group a user is evaluated under.
def _capture_feature_flag_called_if_needed(
distinct_id: nil, key: nil, response: nil, properties: nil,
groups: nil, disable_geoip: nil
)
reported_key = "#{key}_#{response.nil? ? '::null::' : response}"
response_repr = response.nil? ? '::null::' : response
groups_repr =
if groups && !groups.empty?
# Canonicalize so two equal hashes with keys inserted in a different
# order produce the same dedup key.
"_#{groups.sort.to_json}"
else
''
end
reported_key = "#{key}_#{response_repr}#{groups_repr}"
return if @distinct_id_has_sent_flag_calls[distinct_id].include?(reported_key)

msg = {
Expand Down
106 changes: 106 additions & 0 deletions spec/posthog/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,112 @@ module PostHog
)).to eq(true)
end

it '$feature_flag_called fires per group context (group-scoped dedup)' do
api_feature_flag_res = {
'flags' => [
{
'id' => 1,
'name' => 'Group flag',
'key' => 'group-flag',
'active' => true,
'filters' => {
'groups' => [
{ 'properties' => [], 'rollout_percentage' => 100 }
]
}
}
]
}

stub_request(
:get,
'https://us.i.posthog.com/flags/definitions?token=testsecret&send_cohorts=true'
).to_return(status: 200, body: api_feature_flag_res.to_json)

c = Client.new(api_key: API_KEY, personal_api_key: API_KEY, test_mode: true)
allow(c).to receive(:capture)

# Same user, same flag, same response β€” but two different group contexts.
expect(c).to receive(:capture).with(hash_including(
distinct_id: 'user-1',
event: '$feature_flag_called',
groups: { organization: 'org-a' }
)).exactly(1).times
expect(c).to receive(:capture).with(hash_including(
distinct_id: 'user-1',
event: '$feature_flag_called',
groups: { organization: 'org-b' }
)).exactly(1).times

c.get_feature_flag('group-flag', 'user-1', groups: { organization: 'org-a' })
c.get_feature_flag('group-flag', 'user-1', groups: { organization: 'org-b' })
end

it '$feature_flag_called dedupes across repeated calls under the same group context' do
api_feature_flag_res = {
'flags' => [
{
'id' => 1,
'name' => 'Group flag',
'key' => 'group-flag',
'active' => true,
'filters' => {
'groups' => [
{ 'properties' => [], 'rollout_percentage' => 100 }
]
}
}
]
}

stub_request(
:get,
'https://us.i.posthog.com/flags/definitions?token=testsecret&send_cohorts=true'
).to_return(status: 200, body: api_feature_flag_res.to_json)

c = Client.new(api_key: API_KEY, personal_api_key: API_KEY, test_mode: true)
allow(c).to receive(:capture)

expect(c).to receive(:capture).with(hash_including(
event: '$feature_flag_called',
groups: { organization: 'org-a' }
)).exactly(1).times

c.get_feature_flag('group-flag', 'user-1', groups: { organization: 'org-a' })
c.get_feature_flag('group-flag', 'user-1', groups: { organization: 'org-a' })
end

it '$feature_flag_called dedupes when same groups are passed in different key order' do
api_feature_flag_res = {
'flags' => [
{
'id' => 1,
'name' => 'Group flag',
'key' => 'group-flag',
'active' => true,
'filters' => {
'groups' => [
{ 'properties' => [], 'rollout_percentage' => 100 }
]
}
}
]
}

stub_request(
:get,
'https://us.i.posthog.com/flags/definitions?token=testsecret&send_cohorts=true'
).to_return(status: 200, body: api_feature_flag_res.to_json)

c = Client.new(api_key: API_KEY, personal_api_key: API_KEY, test_mode: true)
allow(c).to receive(:capture)

expect(c).to receive(:capture).with(hash_including(event: '$feature_flag_called')).exactly(1).times

c.get_feature_flag('group-flag', 'user-1', groups: { organization: 'org-a', team: 'red' })
c.get_feature_flag('group-flag', 'user-1', groups: { team: 'red', organization: 'org-a' })
end
Comment on lines +619 to +682
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Duplicated test setup across all three new examples

The api_feature_flag_res hash, stub_request, and Client.new lines are copy-pasted verbatim in each of the three new it blocks. Extracting them into let/before inside a shared context would satisfy the OnceAndOnlyOnce rule and align with the team preference for parameterised tests. The deduplication pair (same-context β†’ one event, different-context β†’ two events) are also natural candidates for a parameterised example to remove repetition between tests 2 and 3.

Context Used: Do not attempt to comment on incorrect alphabetica... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: spec/posthog/client_spec.rb
Line: 619-682

Comment:
**Duplicated test setup across all three new examples**

The `api_feature_flag_res` hash, `stub_request`, and `Client.new` lines are copy-pasted verbatim in each of the three new `it` blocks. Extracting them into `let`/`before` inside a shared `context` would satisfy the OnceAndOnlyOnce rule and align with the team preference for parameterised tests. The deduplication pair (same-context β†’ one event, different-context β†’ two events) are also natural candidates for a parameterised example to remove repetition between tests 2 and 3.

**Context Used:** Do not attempt to comment on incorrect alphabetica... ([source](https://app.greptile.com/review/custom-context?memory=instruction-0))

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!


it 'captures groups' do
client.capture(
{
Expand Down
Loading