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..3101663 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] 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. @@ -598,7 +598,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 +623,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 +726,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..db51e82 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 = flag_keys.map(&:to_s) if flag_keys.is_a?(Array) 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, @@ -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..515d503 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 symbol keys in the flag_keys filter array' 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