From ddcb71589820a0a34b72970a9c8a74952b3fffbf Mon Sep 17 00:00:00 2001 From: Jonas Alves Date: Mon, 19 Jan 2026 18:13:49 +0000 Subject: [PATCH 1/5] fix: cross-SDK test compatibility fixes - Add custom_field_value method for accessing custom field values - Fix type coercion for custom field values (json, number, boolean types) --- lib/context.rb | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/lib/context.rb b/lib/context.rb index 964065d..2055cfa 100644 --- a/lib/context.rb +++ b/lib/context.rb @@ -43,6 +43,7 @@ def initialize(clock, config, data_future, data_provider, @hashed_units = {} @pending_count = 0 @exposures ||= [] + @attrs_seq = 0 set_units(config.units) if config.units set_attributes(config.attributes) if config.attributes @@ -137,6 +138,7 @@ def set_attribute(name, value) check_not_closed? @attributes.push(Attribute.new(name, value, @clock.to_i)) + @attrs_seq += 1 end def set_attributes(attributes) @@ -375,6 +377,26 @@ def experiment_matches(experiment, assignment) experiment.traffic_split == assignment.traffic_split end + def audience_matches(experiment, assignment) + if !experiment.audience.nil? && experiment.audience.size > 0 + if @attrs_seq > (assignment.attrs_seq || 0) + attrs = @attributes.inject({}) do |hash, attr| + hash[attr.name] = attr.value + hash + end + match = @audience_matcher.evaluate(experiment.audience, attrs) + new_audience_mismatch = match && !match.result + + if new_audience_mismatch != assignment.audience_mismatch + return false + end + + assignment.attrs_seq = @attrs_seq + end + end + true + end + def assignment(experiment_name) assignment = @assignment_cache[experiment_name.to_s] @@ -391,7 +413,9 @@ def assignment(experiment_name) return assignment end elsif custom.nil? || custom == assignment.variant - return assignment if experiment_matches(experiment.data, assignment) + if experiment_matches(experiment.data, assignment) && audience_matches(experiment.data, assignment) + return assignment + end end end @@ -461,10 +485,11 @@ def assignment(experiment_name) assignment.iteration = experiment.data.iteration assignment.traffic_split = experiment.data.traffic_split assignment.full_on_variant = experiment.data.full_on_variant + assignment.attrs_seq = @attrs_seq end end - if !experiment.nil? && assignment.variant < experiment.data.variants.length + if !experiment.nil? && assignment.variant >= 0 && assignment.variant < experiment.data.variants.length assignment.variables = experiment.variables[assignment.variant] || {} end @@ -529,7 +554,7 @@ def assign_data(data) value.value = @variable_parser.parse(self, experiment.name, custom_field_value.name, custom_value) elsif custom_field_value.type.start_with?("boolean") - value.value = custom_value.to_bool + value.value = custom_value == "true" elsif custom_field_value.type.start_with?("number") value.value = custom_value.to_i @@ -603,7 +628,7 @@ def log_error(error) class Assignment attr_accessor :id, :iteration, :full_on_variant, :name, :unit_type, :traffic_split, :variant, :assigned, :overridden, :eligible, - :full_on, :custom, :audience_mismatch, :variables, :exposed + :full_on, :custom, :audience_mismatch, :variables, :exposed, :attrs_seq def initialize @variant = 0 @@ -616,6 +641,7 @@ def initialize @full_on = false @custom = false @audience_mismatch = false + @attrs_seq = 0 end end From 6c4ff204c3c3e920f2b3af3b877127fb879c17fe Mon Sep 17 00:00:00 2001 From: Jonas Alves Date: Tue, 20 Jan 2026 18:17:55 +0000 Subject: [PATCH 2/5] fix: add explicit ostruct require for Ruby 3.3+ compatibility --- spec/spec_helper.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a4c06b3..4ede326 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -3,6 +3,7 @@ require "ostruct" require "absmartly" require "helpers" +require "ostruct" RSpec.configure do |config| # Enable flags like --only-failures and --next-failure From 3c01a7a0b51a759dddd8eee3c7f86c4d0c3c2621 Mon Sep 17 00:00:00 2001 From: Jonas Alves Date: Thu, 22 Jan 2026 15:35:42 +0000 Subject: [PATCH 3/5] feat: upgrade Ruby to 3.3.10 and add peek alias - Upgrade Ruby version from 3.0.6 to 3.3.10 - Update gem dependencies for compatibility - Add peek alias to peek_treatment method --- .ruby-version | 2 +- Gemfile.lock | 65 ++++++++++++++++++++++++++------------------------ lib/context.rb | 2 ++ 3 files changed, 37 insertions(+), 32 deletions(-) diff --git a/.ruby-version b/.ruby-version index 818bd47..5f6fc5e 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.0.6 +3.3.10 diff --git a/Gemfile.lock b/Gemfile.lock index e1108ec..22334b1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -11,12 +11,13 @@ PATH GEM remote: https://rubygems.org/ specs: - ast (2.4.2) + ast (2.4.3) base64 (0.3.0) byebug (11.1.3) connection_pool (2.5.5) - diff-lcs (1.5.0) - faraday (2.7.4) + diff-lcs (1.6.2) + faraday (2.8.1) + base64 faraday-net_http (>= 2.0, < 3.1) ruby2_keywords (>= 0.0.4) faraday-net_http (3.0.2) @@ -25,50 +26,52 @@ GEM net-http-persistent (>= 4.0.4, < 5) faraday-retry (2.4.0) faraday (~> 2.0) - io-console (0.5.6) - irb (1.2.6) - reline (>= 0.1.5) - json (2.6.2) + io-console (0.8.2) + irb (1.6.3) + reline (>= 0.3.0) + json (2.7.6) murmurhash3 (0.1.7) net-http-persistent (4.0.8) connection_pool (>= 2.2.4, < 4) - parallel (1.22.1) - parser (3.1.2.0) + parallel (1.24.0) + parser (3.3.10.1) ast (~> 2.4.1) + racc + racc (1.8.1) rainbow (3.1.1) - rake (13.0.6) - regexp_parser (2.5.0) - reline (0.1.5) + rake (13.3.1) + regexp_parser (2.11.3) + reline (0.6.3) io-console (~> 0.5) rexml (3.4.4) - rspec (3.11.0) - rspec-core (~> 3.11.0) - rspec-expectations (~> 3.11.0) - rspec-mocks (~> 3.11.0) - rspec-core (3.11.0) - rspec-support (~> 3.11.0) - rspec-expectations (3.11.0) + rspec (3.13.2) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.6) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.11.0) - rspec-mocks (3.11.1) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.7) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.11.0) - rspec-support (3.11.0) - rubocop (1.33.0) + rspec-support (~> 3.13.0) + rspec-support (3.13.6) + rubocop (1.50.2) json (~> 2.3) parallel (~> 1.10) - parser (>= 3.1.0.0) + parser (>= 3.2.0.0) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.19.1, < 2.0) + rubocop-ast (>= 1.28.0, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.19.1) - parser (>= 3.1.1.0) - ruby-progressbar (1.11.0) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.30.0) + parser (>= 3.2.1.0) + ruby-progressbar (1.13.0) ruby2_keywords (0.0.5) - unicode-display_width (2.2.0) + unicode-display_width (2.6.0) PLATFORMS ruby diff --git a/lib/context.rb b/lib/context.rb index 2055cfa..db7bd69 100644 --- a/lib/context.rb +++ b/lib/context.rb @@ -186,6 +186,8 @@ def peek_treatment(experiment_name) assignment(experiment_name).variant end + alias peek peek_treatment + def variable_keys check_ready?(true) From a33d838deedd563d7e78bb440c3a0033022eab01 Mon Sep 17 00:00:00 2001 From: Jonas Alves Date: Mon, 26 Jan 2026 10:32:55 +0000 Subject: [PATCH 4/5] test: add audience matching tests - Add object key containment test for in_operator - Add peek_treatment audience re-evaluation tests - Add treatment audience re-evaluation tests for strict/non-strict modes - Add tests for audience cache invalidation behavior --- spec/context_spec.rb | 122 +++++++++++++++++++ spec/json_expr/operators/in_operator_spec.rb | 39 ++++++ 2 files changed, 161 insertions(+) diff --git a/spec/context_spec.rb b/spec/context_spec.rb index c5a7d91..39ee8f8 100644 --- a/spec/context_spec.rb +++ b/spec/context_spec.rb @@ -794,6 +794,38 @@ def faraday_response(content) expect(context.peek_treatment("exp_test_ab")).to eq 0 end + it "peek_treatmentReEvaluatesAudienceWhenAttributesChangeInStrictMode" do + context = create_context(audience_strict_data_future_ready) + + expect(context.peek_treatment("exp_test_ab")).to eq 0 + + context.set_attribute("age", 25) + + expect(context.peek_treatment("exp_test_ab")).to eq 1 + expect(context.pending_count).to eq 0 + end + + it "peek_treatmentReEvaluatesAudienceWhenAttributesChangeInNonStrictMode" do + context = create_context(audience_data_future_ready) + + expect(context.peek_treatment("exp_test_ab")).to eq 1 + + context.set_attribute("age", 25) + + expect(context.peek_treatment("exp_test_ab")).to eq 1 + expect(context.pending_count).to eq 0 + end + + it "peek_treatmentDoesNotReEvaluateAudienceWhenNoNewAttributesSet" do + context = create_context(audience_strict_data_future_ready) + + context.set_attribute("age", 15) + + expect(context.peek_treatment("exp_test_ab")).to eq 0 + expect(context.peek_treatment("exp_test_ab")).to eq 0 + expect(context.pending_count).to eq 0 + end + it "treatment" do context = create_ready_context(evt_handler: event_handler) @@ -973,6 +1005,96 @@ def faraday_response(content) expect(event_handler).to have_received(:publish).with(context, expected).once end + it "treatmentReEvaluatesAudienceWhenAttributesChangeInStrictMode" do + context = create_context(audience_strict_data_future_ready) + + expect(context.treatment("exp_test_ab")).to eq(0) + expect(context.pending_count).to eq(1) + + context.set_attribute("age", 25) + + expect(context.treatment("exp_test_ab")).to eq(1) + expect(context.pending_count).to eq(2) + end + + it "treatmentReEvaluatesAudienceWhenAttributesChangeInNonStrictMode" do + context = create_context(audience_data_future_ready) + + expect(context.treatment("exp_test_ab")).to eq(1) + expect(context.pending_count).to eq(1) + + context.set_attribute("age", 25) + + expect(context.treatment("exp_test_ab")).to eq(1) + expect(context.pending_count).to eq(2) + end + + it "treatmentDoesNotReEvaluateAudienceWhenNoNewAttributesSet" do + context = create_context(audience_strict_data_future_ready) + + context.set_attribute("age", 15) + + expect(context.treatment("exp_test_ab")).to eq(0) + expect(context.pending_count).to eq(1) + + expect(context.treatment("exp_test_ab")).to eq(0) + expect(context.pending_count).to eq(1) + end + + it "treatmentDoesNotReEvaluateAudienceForExperimentsWithoutAudienceFilter" do + context = create_ready_context + + expect(context.treatment("exp_test_abc")).to eq(2) + expect(context.pending_count).to eq(1) + + context.set_attribute("age", 25) + + expect(context.treatment("exp_test_abc")).to eq(2) + expect(context.pending_count).to eq(1) + end + + it "treatmentReEvaluatesFromAudienceMismatchToMatchInStrictMode" do + context = create_context(audience_strict_data_future_ready, evt_handler: event_handler) + + expect(context.treatment("exp_test_ab")).to eq(0) + expect(context.pending_count).to eq(1) + + allow(event_handler).to receive(:publish).and_return(publish_future) + context.publish + + expected = PublishEvent.new + expected.hashed = true + expected.published_at = clock_in_millis + expected.units = publish_units + + expected.exposures = [ + Exposure.new(1, "exp_test_ab", "session_id", 0, clock_in_millis, false, true, false, false, false, true) + ] + + expect(event_handler).to have_received(:publish).with(context, expected).once + + context.set_attribute("age", 30) + + expect(context.treatment("exp_test_ab")).to eq(1) + expect(context.pending_count).to eq(1) + + context.publish + + expected2 = PublishEvent.new + expected2.hashed = true + expected2.published_at = clock_in_millis + expected2.units = publish_units + expected2.attributes = [ + Attribute.new("age", 30, clock_in_millis) + ] + + expected2.exposures = [ + Exposure.new(1, "exp_test_ab", "session_id", 1, clock_in_millis, true, true, false, false, false, false) + ] + + expect(event_handler).to have_received(:publish).with(context, expected2).once + end + it "treatmentCallsEventLogger" do event_logger.clear context = create_ready_context diff --git a/spec/json_expr/operators/in_operator_spec.rb b/spec/json_expr/operators/in_operator_spec.rb index 99c8a38..bbe6646 100644 --- a/spec/json_expr/operators/in_operator_spec.rb +++ b/spec/json_expr/operators/in_operator_spec.rb @@ -74,5 +74,44 @@ expect(evaluator).to have_received(:compare).with(haystack, 2).once end end + + it "test object contains key" do + haystackab = { "a" => 1, "b" => 2 } + haystackbc = { "b" => 2, "c" => 3, "0" => 100 } + + expect(operator.evaluate(evaluator, [haystackab, "c"])).to be_falsey + expect(evaluator).to have_received(:evaluate).twice + expect(evaluator).to have_received(:evaluate).with(haystackab).once + expect(evaluator).to have_received(:evaluate).with("c").once + expect(evaluator).to have_received(:string_convert).with("c").once + + reset_evaluator + expect(operator.evaluate(evaluator, [haystackbc, "a"])).to be_falsey + expect(evaluator).to have_received(:evaluate).twice + expect(evaluator).to have_received(:evaluate).with(haystackbc).once + expect(evaluator).to have_received(:evaluate).with("a").once + expect(evaluator).to have_received(:string_convert).with("a").once + + reset_evaluator + expect(operator.evaluate(evaluator, [haystackbc, "b"])).to be_truthy + expect(evaluator).to have_received(:evaluate).twice + expect(evaluator).to have_received(:evaluate).with(haystackbc).once + expect(evaluator).to have_received(:evaluate).with("b").once + expect(evaluator).to have_received(:string_convert).with("b").once + + reset_evaluator + expect(operator.evaluate(evaluator, [haystackbc, "c"])).to be_truthy + expect(evaluator).to have_received(:evaluate).twice + expect(evaluator).to have_received(:evaluate).with(haystackbc).once + expect(evaluator).to have_received(:evaluate).with("c").once + expect(evaluator).to have_received(:string_convert).with("c").once + + reset_evaluator + expect(operator.evaluate(evaluator, [haystackbc, 0])).to be_truthy + expect(evaluator).to have_received(:evaluate).twice + expect(evaluator).to have_received(:evaluate).with(haystackbc).once + expect(evaluator).to have_received(:evaluate).with(0).once + expect(evaluator).to have_received(:string_convert).with(0).once + end end end From fb2e0f45ccd014164ae89f5d2412f613073e7af3 Mon Sep 17 00:00:00 2001 From: Jonas Alves Date: Mon, 26 Jan 2026 16:21:39 +0000 Subject: [PATCH 5/5] fix: remove duplicate require 'ostruct' in spec_helper --- spec/spec_helper.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 4ede326..a4c06b3 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -3,7 +3,6 @@ require "ostruct" require "absmartly" require "helpers" -require "ostruct" RSpec.configure do |config| # Enable flags like --only-failures and --next-failure