From db281f02a87f9dc7a8202560fffe41897d22a2bc Mon Sep 17 00:00:00 2001 From: Dustin Byrne Date: Tue, 12 May 2026 10:12:00 -0400 Subject: [PATCH 1/2] fix: accept symbol feature flag keys --- .changeset/symbol-flag-keys.md | 5 +++ lib/posthog/client.rb | 16 +++++---- lib/posthog/feature_flags.rb | 10 ++++-- spec/posthog/feature_flag_evaluations_spec.rb | 18 ++++++++++ spec/posthog/feature_flag_spec.rb | 36 +++++++++++++++++++ spec/posthog/flags_spec.rb | 20 +++++++++++ 6 files changed, 97 insertions(+), 8 deletions(-) create mode 100644 .changeset/symbol-flag-keys.md diff --git a/.changeset/symbol-flag-keys.md b/.changeset/symbol-flag-keys.md new file mode 100644 index 0000000..27a2776 --- /dev/null +++ b/.changeset/symbol-flag-keys.md @@ -0,0 +1,5 @@ +--- +'posthog-ruby': patch +--- + +Accept symbol feature flag keys in flag APIs. diff --git a/lib/posthog/client.rb b/lib/posthog/client.rb index 16b1f78..e4853b5 100644 --- a/lib/posthog/client.rb +++ b/lib/posthog/client.rb @@ -364,15 +364,15 @@ def is_feature_enabled( # rubocop:disable Naming/PredicateName !!response end - # @param [String] flag_key The unique flag key of the feature flag + # @param [String, Symbol] flag_key The unique flag key of the feature flag # @return [String] The decrypted value of the feature flag payload def get_remote_config_payload(flag_key) - @feature_flags_poller.get_remote_config_payload(flag_key) + @feature_flags_poller.get_remote_config_payload(flag_key.to_s) end # Returns whether the given feature flag is enabled for the given user or not # - # @param [String] key The key of the feature flag + # @param [String, Symbol] key The key of the feature flag # @param [String] distinct_id The distinct id of the user # @param [Hash] groups # @param [Hash] person_properties key-value pairs of properties to associate with the user. @@ -455,7 +455,7 @@ def get_feature_flag_result( # @param [Hash] group_properties # @param [Boolean] only_evaluate_locally Skip the remote /flags call entirely # @param [Boolean] disable_geoip Stamped on captured access events - # @param [Array] flag_keys When set, scopes the underlying /flags + # @param [Array, String, Symbol] flag_keys When set, scopes the underlying /flags # request to only these flag keys (sent as `flag_keys_to_evaluate`). # Distinct from {FeatureFlagEvaluations#only}, which filters the # already-fetched snapshot in memory. @@ -479,9 +479,11 @@ def evaluate_flags( distinct_id, groups, person_properties, group_properties ) + flag_keys = Array(flag_keys).map(&:to_s) if flag_keys + records = {} locally_evaluated_keys = Set.new - flag_keys_set = flag_keys&.to_set(&:to_s) + flag_keys_set = flag_keys&.to_set @feature_flags_poller.load_feature_flags poller_flags_by_key = @feature_flags_poller.feature_flags_by_key || {} @@ -598,7 +600,7 @@ def get_all_flags( # @deprecated Use {#get_feature_flag_result} instead, which returns both the flag value and payload # and properly raises the $feature_flag_called event. # - # @param [String] key The key of the feature flag + # @param [String, Symbol] key The key of the feature flag # @param [String] distinct_id The distinct id of the user # @option [String or boolean] match_value The value of the feature flag to be matched # @option [Hash] groups @@ -623,6 +625,7 @@ def get_feature_flag_payload( 'instead — this consolidates flag evaluation into a single `/flags` request per ' \ 'incoming request.' ) + key = key.to_s person_properties, group_properties = add_local_person_and_group_properties(distinct_id, groups, person_properties, group_properties) @feature_flags_poller.get_feature_flag_payload(key, distinct_id, match_value, groups, person_properties, @@ -725,6 +728,7 @@ def _get_feature_flag_result( only_evaluate_locally: false, send_feature_flag_events: true ) + key = key.to_s person_properties, group_properties = add_local_person_and_group_properties( distinct_id, groups, person_properties, group_properties ) diff --git a/lib/posthog/feature_flags.rb b/lib/posthog/feature_flags.rb index 189a8ce..5f950f9 100644 --- a/lib/posthog/feature_flags.rb +++ b/lib/posthog/feature_flags.rb @@ -125,6 +125,7 @@ def get_feature_payloads( def get_flags(distinct_id, groups = {}, person_properties = {}, group_properties = {}, flag_keys = nil, disable_geoip = nil) + flag_keys = Array(flag_keys).map(&:to_s) if flag_keys request_data = { distinct_id: distinct_id, groups: groups, @@ -160,7 +161,7 @@ def get_flags(distinct_id, groups = {}, person_properties = {}, group_properties end def get_remote_config_payload(flag_key) - _request_remote_config_payload(flag_key) + _request_remote_config_payload(flag_key.to_s) end def get_feature_flag( @@ -171,6 +172,8 @@ def get_feature_flag( group_properties = {}, only_evaluate_locally = false ) + key = key.to_s + # make sure they're loaded on first run load_feature_flags @@ -363,6 +366,8 @@ def get_feature_flag_payload( group_properties = {}, only_evaluate_locally = false ) + key = key.to_s + if match_value.nil? match_value = get_feature_flag( key, @@ -377,7 +382,7 @@ def get_feature_flag_payload( response = _compute_flag_payload_locally(key, match_value) unless match_value.nil? if response.nil? && !only_evaluate_locally flags_payloads = get_feature_payloads(distinct_id, groups, person_properties, group_properties) - response = flags_payloads[key.downcase] || nil + response = flags_payloads.key?(key) ? flags_payloads[key] : flags_payloads[key.downcase] end response end @@ -923,6 +928,7 @@ def _compute_flag_locally(flag, distinct_id, groups = {}, person_properties = {} def _compute_flag_payload_locally(key, match_value) return nil if @feature_flags_by_key.nil? + key = key.to_s response = nil if [true, false].include? match_value response = @feature_flags_by_key.dig(key, :filters, :payloads, match_value.to_s.to_sym) diff --git a/spec/posthog/feature_flag_evaluations_spec.rb b/spec/posthog/feature_flag_evaluations_spec.rb index 6ac4472..a653193 100644 --- a/spec/posthog/feature_flag_evaluations_spec.rb +++ b/spec/posthog/feature_flag_evaluations_spec.rb @@ -104,6 +104,16 @@ def capture_stderr expect(unknown[:properties]['$feature_flag_error']).to eq('flag_missing') end + it 'accepts symbol flag keys when reading a snapshot' do + stub_flags(flags_response) + snapshot = client.evaluate_flags('user-1') + + expect(snapshot.enabled?(:'boolean-flag')).to be(true) + expect(snapshot.get_flag(:'variant-flag')).to eq('variant-value') + expect(snapshot.get_flag_payload(:'variant-flag')).to eq('key' => 'value') + expect(snapshot.only(:'boolean-flag').keys).to eq(['boolean-flag']) + end + it 'enabled? returns false for unknown flags' do stub_flags(flags_response) snapshot = client.evaluate_flags('user-1') @@ -156,6 +166,14 @@ def capture_stderr ) end + it 'accepts a symbol flag_keys filter' do + stub_flags(flags_response) + client.evaluate_flags('user-1', flag_keys: :'boolean-flag') + expect(WebMock).to have_requested(:post, FLAGS_ENDPOINT).with( + body: hash_including(flag_keys_to_evaluate: %w[boolean-flag]) + ) + end + it 'returns a usable empty snapshot for empty distinct_id and does not call /flags' do stub_flags(flags_response) snapshot = client.evaluate_flags('') diff --git a/spec/posthog/feature_flag_spec.rb b/spec/posthog/feature_flag_spec.rb index d5b54e2..735e36d 100644 --- a/spec/posthog/feature_flag_spec.rb +++ b/spec/posthog/feature_flag_spec.rb @@ -57,6 +57,42 @@ module PostHog person_properties: { 'region' => 'Canada' })).to eq(false) end + it 'accepts symbol flag keys when evaluating locally' do + api_feature_flag_res = { + 'flags' => [ + { + 'id' => 1, + 'name' => 'Symbol Flag', + 'key' => 'symbol-flag', + 'is_simple_flag' => true, + 'active' => true, + 'filters' => { + 'groups' => [{ 'rollout_percentage' => 100 }], + 'payloads' => { 'true' => '{"source":"local"}' } + } + } + ] + } + 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) + stub_request(:post, flags_endpoint).to_return(status: 400) + + c = Client.new(api_key: API_KEY, personal_api_key: API_KEY, test_mode: true) + + expect(c.get_feature_flag(:'symbol-flag', 'some-distinct-id', + send_feature_flag_events: false)).to eq(true) + expect(c.is_feature_enabled(:'symbol-flag', 'some-distinct-id', + send_feature_flag_events: false)).to eq(true) + + result = c.get_feature_flag_result(:'symbol-flag', 'some-distinct-id', send_feature_flag_events: false) + expect(result.key).to eq('symbol-flag') + expect(result.value).to eq(true) + expect(c.get_feature_flag_payload(:'symbol-flag', 'some-distinct-id')).to eq('{"source":"local"}') + assert_not_requested :post, flags_endpoint + end + it 'evaluates group properties' do api_feature_flag_res = { 'flags' => [ diff --git a/spec/posthog/flags_spec.rb b/spec/posthog/flags_spec.rb index 9988bb6..1524953 100644 --- a/spec/posthog/flags_spec.rb +++ b/spec/posthog/flags_spec.rb @@ -361,6 +361,15 @@ module PostHog }) ) end + + it 'accepts symbol flag keys' do + stub_request(:post, flags_endpoint) + .to_return(status: 200, body: flags_v4_response.to_json) + + expect(client.get_feature_flag(:'enabled-flag', 'test-distinct-id', + send_feature_flag_events: false)).to eq(true) + expect(client.get_feature_flag_payload(:'enabled-flag', 'test-distinct-id')).to eq('{"foo": 1}') + end end end @@ -400,6 +409,17 @@ module PostHog # Verify the request was made to the correct URL with token parameter expect(WebMock).to have_requested(:get, remote_config_endpoint) end + + it 'accepts a symbol flag key' do + remote_config_response = { test: 'payload' } + stub_request(:get, remote_config_endpoint) + .to_return(status: 200, body: remote_config_response.to_json) + + result = poller.get_remote_config_payload(:'test-flag') + + expect(result[:test]).to eq('payload') + expect(WebMock).to have_requested(:get, remote_config_endpoint) + end end describe 'Cohort evaluation' do From c9c3778292ce1bd72b27d4f5862a76a1b1f355c3 Mon Sep 17 00:00:00 2001 From: Dustin Byrne Date: Tue, 12 May 2026 10:34:28 -0400 Subject: [PATCH 2/2] refactor: cleanup and simplify --- lib/posthog/client.rb | 6 ++---- lib/posthog/feature_flags.rb | 4 ++-- spec/posthog/feature_flag_evaluations_spec.rb | 4 ++-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/lib/posthog/client.rb b/lib/posthog/client.rb index e4853b5..3101663 100644 --- a/lib/posthog/client.rb +++ b/lib/posthog/client.rb @@ -455,7 +455,7 @@ def get_feature_flag_result( # @param [Hash] group_properties # @param [Boolean] only_evaluate_locally Skip the remote /flags call entirely # @param [Boolean] disable_geoip Stamped on captured access events - # @param [Array, String, Symbol] flag_keys When set, scopes the underlying /flags + # @param [Array] flag_keys When set, scopes the underlying /flags # request to only these flag keys (sent as `flag_keys_to_evaluate`). # Distinct from {FeatureFlagEvaluations#only}, which filters the # already-fetched snapshot in memory. @@ -479,11 +479,9 @@ def evaluate_flags( distinct_id, groups, person_properties, group_properties ) - flag_keys = Array(flag_keys).map(&:to_s) if flag_keys - records = {} locally_evaluated_keys = Set.new - flag_keys_set = flag_keys&.to_set + flag_keys_set = flag_keys&.to_set(&:to_s) @feature_flags_poller.load_feature_flags poller_flags_by_key = @feature_flags_poller.feature_flags_by_key || {} diff --git a/lib/posthog/feature_flags.rb b/lib/posthog/feature_flags.rb index 5f950f9..db51e82 100644 --- a/lib/posthog/feature_flags.rb +++ b/lib/posthog/feature_flags.rb @@ -125,7 +125,7 @@ def get_feature_payloads( def get_flags(distinct_id, groups = {}, person_properties = {}, group_properties = {}, flag_keys = nil, disable_geoip = nil) - flag_keys = Array(flag_keys).map(&:to_s) if flag_keys + flag_keys = flag_keys.map(&:to_s) if flag_keys.is_a?(Array) request_data = { distinct_id: distinct_id, groups: groups, @@ -382,7 +382,7 @@ def get_feature_flag_payload( response = _compute_flag_payload_locally(key, match_value) unless match_value.nil? if response.nil? && !only_evaluate_locally flags_payloads = get_feature_payloads(distinct_id, groups, person_properties, group_properties) - response = flags_payloads.key?(key) ? flags_payloads[key] : flags_payloads[key.downcase] + response = flags_payloads[key.downcase] || nil end response end diff --git a/spec/posthog/feature_flag_evaluations_spec.rb b/spec/posthog/feature_flag_evaluations_spec.rb index a653193..515d503 100644 --- a/spec/posthog/feature_flag_evaluations_spec.rb +++ b/spec/posthog/feature_flag_evaluations_spec.rb @@ -166,9 +166,9 @@ def capture_stderr ) end - it 'accepts a symbol flag_keys filter' do + it 'accepts symbol keys in the flag_keys filter array' do stub_flags(flags_response) - client.evaluate_flags('user-1', flag_keys: :'boolean-flag') + client.evaluate_flags('user-1', flag_keys: [:'boolean-flag']) expect(WebMock).to have_requested(:post, FLAGS_ENDPOINT).with( body: hash_including(flag_keys_to_evaluate: %w[boolean-flag]) )