From e97c996142a68ef2ebbc2b56df4cd4c8e6e66764 Mon Sep 17 00:00:00 2001 From: "posthog[bot]" <206114724+posthog[bot]@users.noreply.github.com> Date: Wed, 30 Sep 2026 06:05:35 +0000 Subject: [PATCH] feat(flags): accept a caller default in FeatureFlagEvaluations#enabled? The is-feature-enabled contract requires the SDK to accept a caller-supplied boolean default and return it whenever the flag has no value. `enabled?` collapsed every miss to a hardcoded `false`, leaving callers no way to make an unknown flag resolve to `true`. Add an optional `default_value:` keyword, returned only when the key is absent from the snapshot. A flag that has a value, including `false` and variant strings, still wins. Omitting the keyword keeps today's `false` result. Co-Authored-By: Claude Opus 5 Generated-By: PostHog Desktop Task-Id: 2fdafc3d-62a5-4644-b74a-650fbca5dc05 --- .../flag-evaluations-enabled-default-value.md | 5 +++ lib/posthog/feature_flag_evaluations.rb | 12 +++++-- public_api_snapshot.txt | 2 +- spec/posthog/feature_flag_evaluations_spec.rb | 34 +++++++++++++++++++ 4 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 .changeset/flag-evaluations-enabled-default-value.md diff --git a/.changeset/flag-evaluations-enabled-default-value.md b/.changeset/flag-evaluations-enabled-default-value.md new file mode 100644 index 0000000..c0533e6 --- /dev/null +++ b/.changeset/flag-evaluations-enabled-default-value.md @@ -0,0 +1,5 @@ +--- +'posthog-ruby': minor +--- + +Add an optional `default_value:` keyword to `PostHog::FeatureFlagEvaluations#enabled?`. It is returned when the flag has no value in the snapshot — it was never loaded, the evaluation failed, or no flag with that key exists — while a flag that does have a value, including `false` and variant strings, still wins over the default. Calls that omit the keyword keep returning `false` for a missing flag. diff --git a/lib/posthog/feature_flag_evaluations.rb b/lib/posthog/feature_flag_evaluations.rb index 6e7fd5c..8d91c74 100644 --- a/lib/posthog/feature_flag_evaluations.rb +++ b/lib/posthog/feature_flag_evaluations.rb @@ -81,11 +81,19 @@ def keys end # @param key [String, Symbol] The feature flag key. - # @return [Boolean] true when the flag is enabled, false when disabled or missing. - def enabled?(key) + # @param default_value [Boolean, nil] Returned when the flag has no value in this + # snapshot — it was never loaded, the evaluation failed, or no flag with that key + # exists. A flag that does have a value, including `false` and variant strings, + # always wins over this default. Defaults to `nil`, which keeps the historical + # `false` result for a missing flag. + # @return [Boolean] true when the flag is enabled, false when disabled, and the + # caller-supplied default (or false when none was supplied) when the flag is missing. + def enabled?(key, default_value: nil) key = key.to_s flag = @flags[key] _record_access(key, flag) + return !!default_value if flag.nil? && !default_value.nil? + flag&.enabled ? true : false end diff --git a/public_api_snapshot.txt b/public_api_snapshot.txt index b97cd83..278bd8b 100644 --- a/public_api_snapshot.txt +++ b/public_api_snapshot.txt @@ -56,7 +56,7 @@ constant PostHog::Defaults::Request::RETRIES: Integer constant PostHog::Defaults::Request::SSL: Boolean class PostHog::FeatureFlagEvaluations instance_method PostHog::FeatureFlagEvaluations#distinct_id() -instance_method PostHog::FeatureFlagEvaluations#enabled?(key) +instance_method PostHog::FeatureFlagEvaluations#enabled?(key, default_value: ...) instance_method PostHog::FeatureFlagEvaluations#evaluated_at() instance_method PostHog::FeatureFlagEvaluations#flag_definitions_loaded_at() instance_method PostHog::FeatureFlagEvaluations#get_flag(key) diff --git a/spec/posthog/feature_flag_evaluations_spec.rb b/spec/posthog/feature_flag_evaluations_spec.rb index fc93258..d366a54 100644 --- a/spec/posthog/feature_flag_evaluations_spec.rb +++ b/spec/posthog/feature_flag_evaluations_spec.rb @@ -128,6 +128,40 @@ def capture_stderr expect(snapshot.enabled?('not-a-flag')).to be(false) end + it 'enabled? resolves a missing flag to the caller-supplied default' do + stub_flags(flags_response) + snapshot = client.evaluate_flags('user-1') + expect(snapshot.enabled?('not-a-flag', default_value: true)).to be(true) + expect(snapshot.enabled?('not-a-flag', default_value: false)).to be(false) + end + + it 'enabled? prefers an existing flag value over the caller-supplied default' do + stub_flags(flags_response) + snapshot = client.evaluate_flags('user-1') + expect(snapshot.enabled?('disabled-flag', default_value: true)).to be(false) + expect(snapshot.enabled?('variant-flag', default_value: false)).to be(true) + expect(snapshot.enabled?('boolean-flag', default_value: false)).to be(true) + end + + it 'enabled? uses the caller default on an empty snapshot' do + snapshot = client.evaluate_flags('') + expect(snapshot.enabled?('anything', default_value: true)).to be(true) + expect(snapshot.enabled?('anything')).to be(false) + end + + it 'enabled? still reports the evaluated response, not the default, on $feature_flag_called' do + stub_flags(flags_response) + snapshot = client.evaluate_flags('user-1') + snapshot.enabled?('not-a-flag', default_value: true) + + msgs = drain_messages(client).select do |m| + m[:event] == '$feature_flag_called' && m[:properties]['$feature_flag'] == 'not-a-flag' + end + expect(msgs.length).to eq(1) + expect(msgs.first[:properties]['$feature_flag_response']).to be_nil + expect(msgs.first[:properties]['$feature_flag_error']).to eq('flag_missing') + end + it 'enabled? and get_flag on a variant flag dedupe to a single event with the variant response' do stub_flags(flags_response) snapshot = client.evaluate_flags('user-1')