Skip to content
Merged
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
61 changes: 61 additions & 0 deletions spec/posthog/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,67 @@ module PostHog
)).to eq(true)
end

context '$feature_flag_called group-context deduplication' do
let(:group_flag_api_res) do
{
'flags' => [
{
'id' => 1,
'name' => 'Group flag',
'key' => 'group-flag',
'active' => true,
'filters' => {
'groups' => [
{ 'properties' => [], 'rollout_percentage' => 100 }
]
}
}
]
}
end

let(:group_flag_client) do
stub_request(
:get,
'https://us.i.posthog.com/flags/definitions?token=testsecret&send_cohorts=true'
).to_return(status: 200, body: group_flag_api_res.to_json)
c = Client.new(api_key: API_KEY, personal_api_key: API_KEY, test_mode: true)
allow(c).to receive(:capture)
c
end

it 'fires once per distinct group context' do
expect(group_flag_client).to receive(:capture).with(hash_including(
distinct_id: 'user-1',
event: '$feature_flag_called',
groups: { organization: 'org-a' }
)).exactly(1).times
expect(group_flag_client).to receive(:capture).with(hash_including(
distinct_id: 'user-1',
event: '$feature_flag_called',
groups: { organization: 'org-b' }
)).exactly(1).times

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

[
['repeated calls with the same context', { organization: 'org-a' }, { organization: 'org-a' }],
['same groups in different key order',
{ organization: 'org-a', team: 'red' },
{ team: 'red', organization: 'org-a' }]
].each do |description, first_groups, second_groups|
it "dedupes on #{description}" do
expect(group_flag_client)
.to receive(:capture).with(hash_including(event: '$feature_flag_called')).exactly(1).times

group_flag_client.get_feature_flag('group-flag', 'user-1', groups: first_groups)
group_flag_client.get_feature_flag('group-flag', 'user-1', groups: second_groups)
end
end
end

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