Skip to content

Ruby SDK: All tests passing (412/412 unit, 137/138 cross-SDK)#39

Open
joalves wants to merge 12 commits intomainfrom
feat/ruby-3.3-compatibility
Open

Ruby SDK: All tests passing (412/412 unit, 137/138 cross-SDK)#39
joalves wants to merge 12 commits intomainfrom
feat/ruby-3.3-compatibility

Conversation

@joalves
Copy link
Contributor

@joalves joalves commented Feb 21, 2026

Summary

  • Upgrade Ruby compatibility to 3.3.10 with frozen string literal fixes
  • Fix cross-SDK test compatibility issues (audience matcher, match operator, exposure deduplication, variable parser)
  • Add comprehensive test coverage improvements (340 -> 412 unit tests)
  • Add peek alias for peekTreatment consistency with other SDKs
  • Add Rails and Sinatra integration examples

Test Results

  • Unit Tests: 412/412 passed, exit code 0
  • Cross-SDK Tests: 137/138 passed (1 skipped: scenario 67 requires asyncContext), exit code 0

Key Fixes

  • Fix frozen string literal issues across codebase for Ruby 3.3+
  • Add explicit require 'ostruct' for Ruby 3.3+ compatibility
  • Fix audience matcher null handling edge cases
  • Fix match operator regex evaluation
  • Fix context event exposure deduplication logic
  • Add type_utils module for consistent type handling
  • Update experiment model with custom field support
  • Fix variable parser edge cases

Test plan

  • All 412 unit tests pass with bundle exec rspec
  • All 137/138 cross-SDK scenarios pass via Docker orchestrator
  • Ruby 3.3.10 compatibility verified

Summary by CodeRabbit

Release Notes

  • New Features

    • Added asynchronous context creation for non-blocking initialisation.
    • Introduced class aliases (SDK, SDKConfig) for simplified naming.
    • Added global configuration method for easier setup and reuse.
    • Enhanced initialization with named parameters and optional timeout/retry settings.
    • Improved thread-safety for concurrent operations.
  • Documentation

    • Comprehensive README updates with expanded setup guides and API examples.
  • Bug Fixes

    • Enhanced error handling and logging across core components.

- Add custom_field_value method for accessing custom field values
- Fix type coercion for custom field values (json, number, boolean types)
- Upgrade Ruby version from 3.0.6 to 3.3.10
- Update gem dependencies for compatibility
- Add peek alias to peek_treatment method
- 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
- Add publish behavior tests
- Add refresh error handling tests
- Add HTTP retry logic tests
- Add error recovery and resilience tests
- Add concurrent operations tests
- Add attribute timestamp validation tests
- Add variable override precedence tests

Total: 55 new tests added, all 316 tests pass
Add 36 Murmur3 parameterized tests and ~36 context tests
covering cache invalidation, treatment/exposure queuing,
publish data, finalize, unit management, refresh logging,
variable value edge cases, track validation, and event logging.
- Fix frozen string literal issues across codebase
- Add explicit ostruct require for Ruby 3.3+ compatibility
- Fix audience matcher null handling
- Fix match operator regex evaluation
- Fix context event exposure deduplication
- Add type_utils module for consistent type handling
- Update experiment model with custom field support
- Fix variable parser edge cases
@coderabbitai
Copy link

coderabbitai bot commented Feb 21, 2026

Walkthrough

This pull request introduces substantial enhancements to the Ruby SDK's initialization, context creation, and error handling infrastructure. The ABSmartly class is refactored to support both configuration-object and named-parameter initialization patterns. A new asynchronous context creation API (create_context_async) is added alongside improved thread-safety primitives in the Context class. Error handling is refined across multiple deserializers and operators with targeted exception handling and logging. A new TypeUtils module consolidates utility methods for type conversions and string comparisons. Backwards-compatibility aliases (SDK and SDKConfig) are introduced. Ruby version is updated to 3.3.10. Comprehensive test coverage is added for asynchronous contexts, concurrency, and backwards compatibility, whilst documentation in the README is significantly expanded.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • calthejuggler
  • cwmpinho
  • marcio-absmartly

Poem

🐰 A hoppy new tale of async delight,
Where contexts spring forth without blocking the night,
Thread-safe and ready, with aliases true,
The SDK bounces with features brand-new!
Error handling wise, and utilities neat,
This refactor's a carrot of perfectly sweet! 🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main objective of the pull request, highlighting successful test completion and Ruby 3.3 compatibility as the primary focus.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/ruby-3.3-compatibility

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Fix EqualsOperator for numeric string comparisons and InOperator for
string containment. Add named params and backwards compatibility specs.
Add create_context_async for non-blocking context creation, plus
updated README with async examples.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/json/experiment.rb (1)

30-39: ⚠️ Potential issue | 🟠 Major

Use the normalised key for conversion branches.

ALLOWED_KEYS accepts multiple key forms, but Line 30/Line 32/Line 34 branch on raw name. Keys like "applications" or :custom_field_values can bypass class conversion and leave raw hashes in instance state.

💡 Proposed fix
-      if name == :applications
+      if key_str == "applications"
         `@applications` = assign_to_klass(ExperimentApplication, value)
-      elsif name == :variants
+      elsif key_str == "variants"
         `@variants` = assign_to_klass(ExperimentVariant, value)
-      elsif name == :customFieldValues
-        if value != nil
-          `@custom_field_values` = assign_to_klass(CustomFieldValue, value)
-        end
+      elsif key_str == "custom_field_values"
+        `@custom_field_values` = assign_to_klass(CustomFieldValue, value) unless value.nil?
       else
         self.instance_variable_set("@#{key_str}", value)
       end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/json/experiment.rb` around lines 30 - 39, The conversion branches are
using the raw name variable so keys like "applications" or :custom_field_values
bypass class conversion; modify the branch conditions to check the normalized
key used by ALLOWED_KEYS (e.g., use key_str or the normalized symbol instead of
name) so that assign_to_klass(ExperimentApplication, value),
assign_to_klass(ExperimentVariant, value) and assign_to_klass(CustomFieldValue,
value) run for all equivalent key forms; update the if/elsif/elsif to compare
the normalized key (the same normalization ALLOWED_KEYS expects) before setting
`@applications`, `@variants`, or `@custom_field_values` and keep the fallback
instance_variable_set as-is.
🧹 Nitpick comments (9)
lib/context_event_logger_callback.rb (1)

11-11: Redundant nil check.

The condition @callable && !@callable.nil? is redundant — if @callable is truthy, it is inherently not nil. This can be simplified.

✨ Suggested simplification
-    `@callable.call`(event, data) if `@callable` && !@callable.nil?
+    `@callable.call`(event, data) if `@callable`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/context_event_logger_callback.rb` at line 11, Replace the redundant
nil-check in the call site: instead of "@callable.call(event, data) if `@callable`
&& !@callable.nil?" simplify to either "if `@callable`" or use the safe navigation
operator "@callable&.call(event, data)". Update the line where `@callable` is
invoked so only one truthiness check is performed (reference: the
"@callable.call(event, data)" invocation and the "@callable" instance variable).
lib/json_expr/operators/match_operator.rb (1)

23-33: Prefer regexp-level timeout when available, with fallback for older Ruby.

Using Timeout.timeout around regex matching raises asynchronous exceptions unpredictably. Regexp-native timeout, available in Ruby 3.2+, is safer. Since this repository supports Ruby 2.7, the fallback to Timeout.timeout remains necessary.

The proposed refactor below uses runtime detection for compatibility, though caching the check result would avoid repeated inspection:

♻️ Proposed compatibility-safe refactor
-    begin
-      Timeout.timeout(MATCH_TIMEOUT) do
-        Regexp.new(pattern).match(text)
-      end
+    begin
+      supports_regexp_timeout = Regexp.method(:new).parameters.any? { |_, name| name == :timeout }
+
+      if supports_regexp_timeout
+        Regexp.new(pattern, timeout: MATCH_TIMEOUT).match(text)
+      else
+        Timeout.timeout(MATCH_TIMEOUT) { Regexp.new(pattern).match(text) }
+      end
     rescue Timeout::Error
       warn("Regex match timeout: pattern=#{pattern[0..50].inspect}...")
       nil
lib/context.rb (1)

632-632: Minor: unnecessary ||= in initialisation.

@exposures ||= [] uses conditional assignment, but since this is fresh initialisation, a simple assignment @exposures = [] would be clearer and consistent with the other initialisations in this method.

Proposed fix
-    `@exposures` ||= []
+    `@exposures` = []
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/context.rb` at line 632, Replace the conditional assignment for the
instance variable `@exposures` with a direct assignment to match the other
initialisations in the same method: change the initialization from using ||= to
a simple = assignment so `@exposures` is explicitly set to an empty array during
context setup (look for the line setting `@exposures` and the surrounding
initialisation block in the same method).
spec/http_retry_spec.rb (1)

8-15: Remove the unused client setup in this example.

Line 12 currently adds setup noise without contributing to the assertion.

💡 Proposed fix
     it "configures max_retries correctly" do
       config = DefaultHttpClientConfig.create
       config.max_retries = 3
-
-      client = DefaultHttpClient.create(config)
 
       expect(config.max_retries).to eq(3)
     end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@spec/http_retry_spec.rb` around lines 8 - 15, The test creates an unused
client variable which adds noise; remove the unnecessary call to
DefaultHttpClient.create(config) (the line assigning to client) from the example
so the spec only sets config via DefaultHttpClientConfig.create and asserts
config.max_retries equals 3; references: DefaultHttpClient.create,
DefaultHttpClientConfig.create, and the local variable client.
spec/concurrency_spec.rb (1)

67-72: Trim redundant begin wrappers in thread blocks.

These begin ... rescue wrappers are unnecessary in block scope and add noise; you can keep the same rescue behaviour with less nesting.

Also applies to: 113-117, 161-170, 206-210, 231-236

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@spec/concurrency_spec.rb` around lines 67 - 72, Remove the redundant
begin...end wrappers inside the thread blocks that call
context.treatment("exp_test_ab"); instead write the rescue directly on the block
(thread do ... rescue StandardError => e ... end) so behaviour is identical but
nesting is reduced—keep mutex.synchronize { results << result } and
mutex.synchronize { errors << e } unchanged; apply the same change for the other
thread blocks that use context.treatment, mutex, results and errors in this
spec.
spec/context_spec.rb (1)

1364-1364: Remove unused local original_experiments.

Line 1364 assigns original_experiments but never uses it.

♻️ Tiny cleanup
-      original_experiments = context.experiments.dup
-
       context.refresh
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@spec/context_spec.rb` at line 1364, Remove the unused local variable
assignment by deleting the line that sets original_experiments =
context.experiments.dup; locate the assignment by the symbol
original_experiments and the call to context.experiments.dup in
spec/context_spec.rb and remove it to clean up the unused local without changing
any other behavior.
README.md (1)

14-14: Consider removing shell prompts in command blocks.

Line 14 and Line 20 use $ prefixes, which triggers MD014 and is usually avoided in copy/paste snippets.

Also applies to: 20-20

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 14, Remove the leading shell prompt from the command
examples so they are plain copy/paste-friendly; replace occurrences of "$ bundle
add absmartly-sdk" and the other "$ ..." command in the README with the command
text without the "$" prefix (e.g., "bundle add absmartly-sdk") in the code
blocks to resolve MD014 and improve usability.
spec/a_b_smartly_named_params_spec.rb (1)

37-57: Default timeout/retries tests do not assert the defaults they name.

Line 37 and Line 48 currently pass even if defaults regress, because they only check sdk is non-nil.

💡 Suggested test tightening
   it "uses default timeout of 3000ms" do
+    captured_config = nil
+    allow(Client).to receive(:create) do |cfg, *_|
+      captured_config = cfg
+      instance_double(Client)
+    end
+
     sdk = ABSmartly.new(
       valid_params[:endpoint],
       api_key: valid_params[:api_key],
       application: valid_params[:application],
       environment: valid_params[:environment]
     )
 
     expect(sdk).not_to be_nil
+    expect(captured_config.connect_timeout).to eq(3.0)
+    expect(captured_config.connection_request_timeout).to eq(3.0)
   end
 
   it "uses default retries of 5" do
+    captured_config = nil
+    allow(Client).to receive(:create) do |cfg, *_|
+      captured_config = cfg
+      instance_double(Client)
+    end
+
     sdk = ABSmartly.new(
       valid_params[:endpoint],
       api_key: valid_params[:api_key],
       application: valid_params[:application],
       environment: valid_params[:environment]
     )
 
     expect(sdk).not_to be_nil
+    expect(captured_config.max_retries).to eq(5)
   end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@spec/a_b_smartly_named_params_spec.rb` around lines 37 - 57, The tests
currently only assert sdk is non-nil but should verify the actual default
values; update the two examples that call ABSmartly.new to replace the
expect(sdk).not_to be_nil with assertions that the instance uses the expected
defaults (e.g. expect(sdk.options[:timeout]).to eq(3000) for the timeout test
and expect(sdk.options[:retries]).to eq(5) for the retries test), locating the
instance created via ABSmartly.new (variable sdk) and adjusting the accessor
(options, config or accessor methods like timeout/retries) to match your SDK
internals.
spec/create_context_async_spec.rb (1)

130-143: The pre-ready test leaves a long-running worker thread behind.

Line 132 uses a 5-second delay and the example exits without draining that async fetch. This can slow or destabilise the suite.

💡 Suggested adjustment
-      config.client = slow_client_mock(delay: 5)
+      config.client = slow_client_mock(delay: 0.2)
@@
       expect { context.treatment("exp_test_ab") }.to raise_error(
         IllegalStateException, "ABSmartly Context is not yet ready"
       )
+
+      # Drain async worker for test hygiene
+      context.wait_until_ready(1)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@spec/create_context_async_spec.rb` around lines 130 - 143, The test leaves a
long-running worker from the slow_client_mock( delay: 5 ) behind; change the
mock to use a tiny delay (e.g. delay: 0.01) or replace with a
non-blocking/faster stub so create_context_async (via ABSmartly.create and
sdk.create_context_async) doesn't spawn a long-lived fetch, and/or ensure the
background worker is drained by calling the SDK shutdown/cleanup (e.g.
sdk.shutdown or equivalent) after the expectation so the test process doesn't
retain the async thread.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/a_b_smartly.rb`:
- Around line 89-93: Replace the permissive .to_i/.to_f coercions with strict
Integer()/Float() conversions for parameters so invalid strings raise
immediately: validate timeout using Float(timeout) and ensure it is > 0 (raise
ArgumentError with a clear message on failure), validate retries using
Integer(retries) and ensure it is >= 0 (raise ArgumentError on failure), and
when assigning to client_config.max_retries use Integer(retries) (and similarly
use Float(timeout) where the numeric timeout is forwarded) so only real numeric
values are accepted and propagated to Faraday.
- Around line 127-130: The background thread that calls
data_provider.context_data and then context.set_data must rescue exceptions so
wait_until_ready() isn't left blocked; wrap the Thread.new body in a
begin/rescue, catch any exception from data_provider.context_data,
construct/passthrough a failure object (or the exception) to context.set_data so
that context.set_data_failed runs and `@ready_condvar` is broadcast, and re-use
the existing symbols: Thread.new, data_provider.context_data, context.set_data,
set_data_failed, wait_until_ready, and `@ready_condvar`.

In `@lib/audience_matcher.rb`:
- Around line 38-39: The rescue handler in audience_matcher.rb can raise if
e.backtrace is nil; update the warn call that logs the exception (the rescue
block using "rescue StandardError => e" and the warn("Audience evaluation
failed: #{e.class} - #{e.message}\n#{e.backtrace.first(5).join("\n")}")
invocation) to normalize the backtrace with Array(e.backtrace) before calling
first(5).join so the log is nil-safe and won't raise a NoMethodError.

In `@lib/client.rb`:
- Around line 55-57: Replace raw response-body usage in the error path: instead
of creating Exception.new(`@promise.body`) and logging warn("Failed to fetch
context data: #{`@promise.body`}"), build the exception and log using
non-sensitive metadata (e.g. `@promise.status`, `@promise.code`, or `@promise.reason`)
and a sanitized/truncated body snippet if required (e.g. first N chars with
sensitive patterns redacted). Update the two locations that set `@exception` and
call warn (the block using `@promise` shown and the similar block around lines
69-72) to use Exception.new with a safe message and warn with status/code plus a
short, redacted/truncated body preview rather than the full `@promise.body`.

In `@lib/context.rb`:
- Line 610: The `@index` instance variable is initialized as an Array but used as
a Hash elsewhere (e.g., assign_data and accesses like `@index`[experiment.name]);
change the initialization from [] to {} so `@index` is consistently a Hash,
ensuring code paths guarded by check_ready? continue to work with the correct
type (update the declaration of `@index` to use {}).

In `@lib/default_audience_deserializer.rb`:
- Line 9: Replace the inclusive-range slice used in the JSON.parse call
(currently JSON.parse(bytes[offset..length], ...)) with the start/length form to
match the other deserializer; i.e., change the slice to bytes[offset, length] so
both deserializers use the same semantics when parsing the JSON payload.

In `@lib/json/experiment.rb`:
- Around line 47-52: The method assign_to_klass uses a method-level `return`
inside arr.map which prematurely exits the method; replace the `return item if
item.is_a?(klass)` with a non-return expression (e.g., yield the item back to
the map by using `item` or `next item`) so the block always produces values for
mapping, keep the `klass.new(*item.values)` branch as-is, and ensure the final
`.compact` call still returns an array of instances; reference arr.map,
item.is_a?(klass), klass.new(*item.values), and assign_to_klass when making the
change.

In `@README.md`:
- Around line 351-366: The example CustomEventLogger#handle_event assumes event
payloads are Hashes (using data[:...]) but the SDK provides typed objects (e.g.,
Exposure, GoalAchievement, ContextData), which will raise NoMethodError; update
the example to access attributes on the objects (e.g., use data.name or
data.experiments instead of data[:name]/data[:experiments], and call length on
the returned array where needed) for the cases handling EVENT_TYPE::EXPOSURE,
EVENT_TYPE::GOAL, EVENT_TYPE::PUBLISH, EVENT_TYPE::READY, and
EVENT_TYPE::REFRESH so the code uses object attribute access consistent with the
payload types.

---

Outside diff comments:
In `@lib/json/experiment.rb`:
- Around line 30-39: The conversion branches are using the raw name variable so
keys like "applications" or :custom_field_values bypass class conversion; modify
the branch conditions to check the normalized key used by ALLOWED_KEYS (e.g.,
use key_str or the normalized symbol instead of name) so that
assign_to_klass(ExperimentApplication, value),
assign_to_klass(ExperimentVariant, value) and assign_to_klass(CustomFieldValue,
value) run for all equivalent key forms; update the if/elsif/elsif to compare
the normalized key (the same normalization ALLOWED_KEYS expects) before setting
`@applications`, `@variants`, or `@custom_field_values` and keep the fallback
instance_variable_set as-is.

---

Nitpick comments:
In `@lib/context_event_logger_callback.rb`:
- Line 11: Replace the redundant nil-check in the call site: instead of
"@callable.call(event, data) if `@callable` && !@callable.nil?" simplify to either
"if `@callable`" or use the safe navigation operator "@callable&.call(event,
data)". Update the line where `@callable` is invoked so only one truthiness check
is performed (reference: the "@callable.call(event, data)" invocation and the
"@callable" instance variable).

In `@lib/context.rb`:
- Line 632: Replace the conditional assignment for the instance variable
`@exposures` with a direct assignment to match the other initialisations in the
same method: change the initialization from using ||= to a simple = assignment
so `@exposures` is explicitly set to an empty array during context setup (look for
the line setting `@exposures` and the surrounding initialisation block in the same
method).

In `@README.md`:
- Line 14: Remove the leading shell prompt from the command examples so they are
plain copy/paste-friendly; replace occurrences of "$ bundle add absmartly-sdk"
and the other "$ ..." command in the README with the command text without the
"$" prefix (e.g., "bundle add absmartly-sdk") in the code blocks to resolve
MD014 and improve usability.

In `@spec/a_b_smartly_named_params_spec.rb`:
- Around line 37-57: The tests currently only assert sdk is non-nil but should
verify the actual default values; update the two examples that call
ABSmartly.new to replace the expect(sdk).not_to be_nil with assertions that the
instance uses the expected defaults (e.g. expect(sdk.options[:timeout]).to
eq(3000) for the timeout test and expect(sdk.options[:retries]).to eq(5) for the
retries test), locating the instance created via ABSmartly.new (variable sdk)
and adjusting the accessor (options, config or accessor methods like
timeout/retries) to match your SDK internals.

In `@spec/concurrency_spec.rb`:
- Around line 67-72: Remove the redundant begin...end wrappers inside the thread
blocks that call context.treatment("exp_test_ab"); instead write the rescue
directly on the block (thread do ... rescue StandardError => e ... end) so
behaviour is identical but nesting is reduced—keep mutex.synchronize { results
<< result } and mutex.synchronize { errors << e } unchanged; apply the same
change for the other thread blocks that use context.treatment, mutex, results
and errors in this spec.

In `@spec/context_spec.rb`:
- Line 1364: Remove the unused local variable assignment by deleting the line
that sets original_experiments = context.experiments.dup; locate the assignment
by the symbol original_experiments and the call to context.experiments.dup in
spec/context_spec.rb and remove it to clean up the unused local without changing
any other behavior.

In `@spec/create_context_async_spec.rb`:
- Around line 130-143: The test leaves a long-running worker from the
slow_client_mock( delay: 5 ) behind; change the mock to use a tiny delay (e.g.
delay: 0.01) or replace with a non-blocking/faster stub so create_context_async
(via ABSmartly.create and sdk.create_context_async) doesn't spawn a long-lived
fetch, and/or ensure the background worker is drained by calling the SDK
shutdown/cleanup (e.g. sdk.shutdown or equivalent) after the expectation so the
test process doesn't retain the async thread.

In `@spec/http_retry_spec.rb`:
- Around line 8-15: The test creates an unused client variable which adds noise;
remove the unnecessary call to DefaultHttpClient.create(config) (the line
assigning to client) from the example so the spec only sets config via
DefaultHttpClientConfig.create and asserts config.max_retries equals 3;
references: DefaultHttpClient.create, DefaultHttpClientConfig.create, and the
local variable client.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0fef1d1 and f7c057b.

⛔ Files ignored due to path filters (1)
  • Gemfile.lock is excluded by !**/*.lock
📒 Files selected for processing (35)
  • .gitignore
  • .ruby-version
  • CHANGELOG.md
  • README.md
  • absmartly.gemspec
  • example/example.rb
  • lib/a_b_smartly.rb
  • lib/a_b_smartly_config.rb
  • lib/absmartly.rb
  • lib/audience_matcher.rb
  • lib/client.rb
  • lib/context.rb
  • lib/context_event_logger_callback.rb
  • lib/default_audience_deserializer.rb
  • lib/default_context_data_deserializer.rb
  • lib/default_variable_parser.rb
  • lib/json/experiment.rb
  • lib/json_expr/expr_evaluator.rb
  • lib/json_expr/operators/and_combinator.rb
  • lib/json_expr/operators/equals_operator.rb
  • lib/json_expr/operators/in_operator.rb
  • lib/json_expr/operators/match_operator.rb
  • lib/json_expr/operators/or_combinator.rb
  • lib/scheduled_thread_pool_executor.rb
  • lib/type_utils.rb
  • spec/a_b_smartly_named_params_spec.rb
  • spec/backwards_compatibility_spec.rb
  • spec/concurrency_spec.rb
  • spec/context_spec.rb
  • spec/create_context_async_spec.rb
  • spec/default_variable_parser_spec.rb
  • spec/hashing_spec.rb
  • spec/http_retry_spec.rb
  • spec/json_expr/operators/equals_operator_spec.rb
  • spec/json_expr/operators/in_operator_spec.rb

Comment on lines +89 to +93
retries ||= 5

raise ArgumentError.new("timeout must be a positive number") if timeout.to_i <= 0
raise ArgumentError.new("retries must be a non-negative number") if retries.to_i < 0

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n lib/a_b_smartly.rb | sed -n '85,110p'

Repository: absmartly/ruby-sdk

Length of output: 1468


🏁 Script executed:

fd -t f -n "client_config\|ClientConfig" --ignore-case | head -20

Repository: absmartly/ruby-sdk

Length of output: 292


🏁 Script executed:

fd -t f "client_config|ClientConfig" --ignore-case | head -20

Repository: absmartly/ruby-sdk

Length of output: 183


🏁 Script executed:

cat -n lib/client_config.rb

Repository: absmartly/ruby-sdk

Length of output: 2158


🏁 Script executed:

cat -n lib/default_http_client_config.rb

Repository: absmartly/ruby-sdk

Length of output: 732


🏁 Script executed:

rg "max_retries" -A 3 -B 1

Repository: absmartly/ruby-sdk

Length of output: 6460


🏁 Script executed:

rg "request :retry" -B 5 -A 5 lib/default_http_client.rb

Repository: absmartly/ruby-sdk

Length of output: 391


🏁 Script executed:

cat -n spec/http_retry_spec.rb

Repository: absmartly/ruby-sdk

Length of output: 6285


🏁 Script executed:

rg "retries.*abc|to_i|Integer|Float" -B 2 -A 2 spec/

Repository: absmartly/ruby-sdk

Length of output: 1117


🏁 Script executed:

cat -n lib/a_b_smartly.rb | sed -n '1,30p'

Repository: absmartly/ruby-sdk

Length of output: 1222


🏁 Script executed:

rg "ArgumentError.*retries|ArgumentError.*timeout" -B 3 -A 3 spec/

Repository: absmartly/ruby-sdk

Length of output: 1453


🏁 Script executed:

cat -n spec/a_b_smartly_named_params_spec.rb | sed -n '1,100p'

Repository: absmartly/ruby-sdk

Length of output: 3755


🏁 Script executed:

cat -n spec/a_b_smartly_named_params_spec.rb | sed -n '98,130p'

Repository: absmartly/ruby-sdk

Length of output: 1295


Use Integer() and Float() for strict parameter coercion instead of .to_i and .to_f.

The validation on line 92 uses .to_i < 0, which allows non-numeric strings like "abc" to pass because "abc".to_i returns 0. The raw value is then forwarded to client_config.max_retries on line 101, where it eventually reaches Faraday's retry middleware expecting a numeric value, causing a runtime error.

Use Ruby's strict conversion methods (Integer() and Float()) to reject invalid input at initialization time:

💡 Proposed fix
-    timeout ||= 3000
-    retries ||= 5
-
-    raise ArgumentError.new("timeout must be a positive number") if timeout.to_i <= 0
-    raise ArgumentError.new("retries must be a non-negative number") if retries.to_i < 0
+    timeout = 3000 if timeout.nil?
+    retries = 5 if retries.nil?
+
+    begin
+      timeout = Float(timeout)
+    rescue ArgumentError, TypeError
+      raise ArgumentError.new("timeout must be a positive number")
+    end
+
+    begin
+      retries = Integer(retries)
+    rescue ArgumentError, TypeError
+      raise ArgumentError.new("retries must be a non-negative number")
+    end
+
+    raise ArgumentError.new("timeout must be a positive number") if timeout <= 0
+    raise ArgumentError.new("retries must be a non-negative number") if retries < 0
@@
-    client_config.connect_timeout = timeout.to_f / 1000.0
-    client_config.connection_request_timeout = timeout.to_f / 1000.0
+    client_config.connect_timeout = timeout / 1000.0
+    client_config.connection_request_timeout = timeout / 1000.0
     client_config.max_retries = retries

Also applies to: 101-101

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/a_b_smartly.rb` around lines 89 - 93, Replace the permissive .to_i/.to_f
coercions with strict Integer()/Float() conversions for parameters so invalid
strings raise immediately: validate timeout using Float(timeout) and ensure it
is > 0 (raise ArgumentError with a clear message on failure), validate retries
using Integer(retries) and ensure it is >= 0 (raise ArgumentError on failure),
and when assigning to client_config.max_retries use Integer(retries) (and
similarly use Float(timeout) where the numeric timeout is forwarded) so only
real numeric values are accepted and propagated to Faraday.

Comment on lines +127 to +130
Thread.new do
data_future = data_provider.context_data
context.set_data(data_future)
end
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the file at the specified lines
cat -n lib/a_b_smartly.rb | sed -n '120,140p'

Repository: absmartly/ruby-sdk

Length of output: 1002


🏁 Script executed:

# Get file size to understand scope
wc -l lib/a_b_smartly.rb

Repository: absmartly/ruby-sdk

Length of output: 84


🏁 Script executed:

# Search for context.set_data definition
rg "def set_data" -A 5 -B 2

Repository: absmartly/ruby-sdk

Length of output: 711


🏁 Script executed:

# Look for context waiting/blocking mechanisms
rg "wait|ready|join" -B 2 -A 2 -t rb

Repository: absmartly/ruby-sdk

Length of output: 88


🏁 Script executed:

# Check if there's existing exception handling in data_provider context calls
rg "data_provider\.context_data" -B 5 -A 5

Repository: absmartly/ruby-sdk

Length of output: 6811


🏁 Script executed:

# Look for similar thread patterns to understand established conventions
rg "Thread\.new" -B 3 -A 5 -t rb

Repository: absmartly/ruby-sdk

Length of output: 88


🏁 Script executed:

# Get full set_data and set_data_failed implementations
cat -n lib/context.rb | grep -A 15 "def set_data"

Repository: absmartly/ruby-sdk

Length of output: 1120


🏁 Script executed:

# Find wait_until_ready implementation
rg "def wait_until_ready" -A 10

Repository: absmartly/ruby-sdk

Length of output: 584


🏁 Script executed:

# Look at context_data provider to understand what it returns
rg "def context_data" -A 10 -B 2

Repository: absmartly/ruby-sdk

Length of output: 4087


🏁 Script executed:

# Check the async creation tests to see expected behavior on exceptions
rg "create_context_async" -B 3 -A 10 spec/

Repository: absmartly/ruby-sdk

Length of output: 8440


🏁 Script executed:

# Check if ostruct is already required in the file
rg "require.*ostruct" lib/

Repository: absmartly/ruby-sdk

Length of output: 44


🏁 Script executed:

# Check what's required at the top of lib/a_b_smartly.rb
head -20 lib/a_b_smartly.rb

Repository: absmartly/ruby-sdk

Length of output: 720


🏁 Script executed:

# Verify if OpenStruct is used elsewhere in the codebase
rg "OpenStruct" -B 2 -A 2

Repository: absmartly/ruby-sdk

Length of output: 6663


Add exception handling to background data fetch to prevent indefinite blocking on wait_until_ready().

If data_provider.context_data raises an exception in the background thread, context.set_data is never called and @ready_condvar is never broadcast, causing any caller to wait_until_ready() to block indefinitely.

Wrap the call in a begin/rescue block and pass a failure object to context.set_data so that set_data_failed is invoked and the condition variable is properly signalled.

Proposed fix
+require "ostruct"
+
     Thread.new do
-      data_future = data_provider.context_data
-      context.set_data(data_future)
+      begin
+        data_future = data_provider.context_data
+        context.set_data(data_future)
+      rescue StandardError => e
+        context.set_data(OpenStruct.new(exception: e, success?: false, data_future: nil))
+      end
     end
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Thread.new do
data_future = data_provider.context_data
context.set_data(data_future)
end
Thread.new do
begin
data_future = data_provider.context_data
context.set_data(data_future)
rescue StandardError => e
context.set_data(OpenStruct.new(exception: e, success?: false, data_future: nil))
end
end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/a_b_smartly.rb` around lines 127 - 130, The background thread that calls
data_provider.context_data and then context.set_data must rescue exceptions so
wait_until_ready() isn't left blocked; wrap the Thread.new body in a
begin/rescue, catch any exception from data_provider.context_data,
construct/passthrough a failure object (or the exception) to context.set_data so
that context.set_data_failed runs and `@ready_condvar` is broadcast, and re-use
the existing symbols: Thread.new, data_provider.context_data, context.set_data,
set_data_failed, wait_until_ready, and `@ready_condvar`.

Comment on lines +38 to +39
rescue StandardError => e
warn("Audience evaluation failed: #{e.class} - #{e.message}\n#{e.backtrace.first(5).join("\n")}")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Locate and examine the file
fd -t f "audience_matcher.rb" | head -5

Repository: absmartly/ruby-sdk

Length of output: 85


🌐 Web query:

Ruby 3.3 Exception#backtrace nil safe handling Array wrapper pattern

💡 Result:

In Ruby 3.3, Exception#backtrace legitimately returns Array(String) or nil: if no backtrace has been set (e.g., StandardError.new.backtrace), Ruby returns nil. [1]

Nil-safe “Array wrapper” pattern

Use Array(...) to normalize to an array:

bt = Array(e.backtrace)   # => [] if nil, else the array
bt.each { |line| warn line }

This works because Array(nil) #=> [], while Array(existing_array) #=> existing_array.

Common alternatives

e.backtrace&.join("\n")           # nil-safe join
(e.backtrace || []).each { ... }  # explicit fallback

Related: backtrace_locations can also be nil

Exception#backtrace_locations similarly returns an array or nil, depending on what backtrace data exists / how it was set. [2]

When/why it might be nil

A backtrace is normally set when an exception is raised, but you can also construct exceptions without raising them, or explicitly set/override backtraces (including via raise(..., backtrace_array)), which affects what #backtrace / #backtrace_locations return. [1][3]

Sources: Ruby 3.3 Exception#backtrace docs [1], Ruby backtrace behavior notes [2], Ruby Kernel#raise backtrace argument docs [3].


🏁 Script executed:

cat -n lib/audience_matcher.rb | sed -n '30,45p'

Repository: absmartly/ruby-sdk

Length of output: 575


Guard against nil backtraces in error logging.

Line 39 calls e.backtrace.first(5) without nil-safety; in Ruby 3.3, Exception#backtrace can return nil (e.g. when an exception is constructed without being raised), which causes NoMethodError within the rescue handler itself. Use Array(e.backtrace) to safely normalise to an empty array if nil:

Proposed fix
  rescue StandardError => e
-    warn("Audience evaluation failed: #{e.class} - #{e.message}\n#{e.backtrace.first(5).join("\n")}")
+    backtrace = Array(e.backtrace).first(5).join("\n")
+    suffix = backtrace.empty? ? "" : "\n#{backtrace}"
+    warn("Audience evaluation failed: #{e.class} - #{e.message}#{suffix}")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
rescue StandardError => e
warn("Audience evaluation failed: #{e.class} - #{e.message}\n#{e.backtrace.first(5).join("\n")}")
rescue StandardError => e
backtrace = Array(e.backtrace).first(5).join("\n")
suffix = backtrace.empty? ? "" : "\n#{backtrace}"
warn("Audience evaluation failed: #{e.class} - #{e.message}#{suffix}")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/audience_matcher.rb` around lines 38 - 39, The rescue handler in
audience_matcher.rb can raise if e.backtrace is nil; update the warn call that
logs the exception (the rescue block using "rescue StandardError => e" and the
warn("Audience evaluation failed: #{e.class} -
#{e.message}\n#{e.backtrace.first(5).join("\n")}") invocation) to normalize the
backtrace with Array(e.backtrace) before calling first(5).join so the log is
nil-safe and won't raise a NoMethodError.

Comment on lines 55 to 57
@exception = Exception.new(@promise.body)
warn("Failed to fetch context data: #{@promise.body}")
return self
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Do not log raw response bodies in warning paths.

These warnings can leak sensitive data into logs. Prefer status/error metadata (or a sanitised/truncated message) instead of full body text.

💡 Proposed fix
     unless `@promise.success`?
       `@exception` = Exception.new(`@promise.body`)
-      warn("Failed to fetch context data: #{`@promise.body`}")
+      warn("Failed to fetch context data (status=#{`@promise.respond_to`?(:status) ? `@promise.status` : 'unknown'})")
       return self
     end
@@
     unless response.success?
       error = Exception.new(response.body)
-      warn("Publish failed: #{response.body}")
+      warn("Publish failed (status=#{response.respond_to?(:status) ? response.status : 'unknown'})")
       return error
     end

Also applies to: 69-72

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/client.rb` around lines 55 - 57, Replace raw response-body usage in the
error path: instead of creating Exception.new(`@promise.body`) and logging
warn("Failed to fetch context data: #{`@promise.body`}"), build the exception and
log using non-sensitive metadata (e.g. `@promise.status`, `@promise.code`, or
`@promise.reason`) and a sanitized/truncated body snippet if required (e.g. first
N chars with sensitive patterns redacted). Update the two locations that set
`@exception` and call warn (the block using `@promise` shown and the similar block
around lines 69-72) to use Exception.new with a safe message and warn with
status/code plus a short, redacted/truncated body preview rather than the full
`@promise.body`.


def init_common(clock, config, data_provider, event_handler, event_logger,
variable_parser, audience_matcher)
@index = []
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Type inconsistency: @index initialised as Array but used as Hash.

@index is initialised as [] here, but assign_data (line 533) sets @index = {} and subsequent code accesses it as a Hash (e.g., @index[experiment.name] on line 582). Whilst check_ready? guards most accesses, initialising as an empty Hash would be more consistent.

Proposed fix
-    `@index` = []
+    `@index` = {}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@index = []
`@index` = {}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/context.rb` at line 610, The `@index` instance variable is initialized as
an Array but used as a Hash elsewhere (e.g., assign_data and accesses like
`@index`[experiment.name]); change the initialization from [] to {} so `@index` is
consistently a Hash, ensuring code paths guarded by check_ready? continue to
work with the correct type (update the declaration of `@index` to use {}).

@@ -7,7 +7,11 @@ class DefaultAudienceDeserializer < AudienceDeserializer

def deserialize(bytes, offset, length)
JSON.parse(bytes[offset..length], symbolize_names: true)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the default_audience_deserializer.rb file around line 9
echo "=== default_audience_deserializer.rb ==="
cat -n lib/default_audience_deserializer.rb

echo ""
echo "=== default_context_data_deserializer.rb ==="
cat -n lib/default_context_data_deserializer.rb

Repository: absmartly/ruby-sdk

Length of output: 1516


🏁 Script executed:

#!/bin/bash
# Check if there are other deserializer classes to understand the pattern
echo "=== Finding all deserializer files ==="
fd -t f "deserializer" lib/ | head -20

echo ""
echo "=== Checking other deserializer implementations ==="
for file in $(fd -t f "deserializer" lib/ | grep -i default); do
  echo "=== $file ==="
  grep -n "bytes\[" "$file" || echo "(no bytes slicing found)"
done

Repository: absmartly/ruby-sdk

Length of output: 522


Inconsistent string slicing between deserializers.

The two deserializers use different slicing approaches with identical method signatures. lib/default_audience_deserializer.rb line 9 uses bytes[offset..length] (inclusive range from offset to length), whilst lib/default_context_data_deserializer.rb line 11 uses bytes[offset, length] (slice: length bytes starting at offset). These have different semantics:

  • bytes[0..10] returns 11 characters (indices 0–10 inclusive)
  • bytes[0, 10] returns 10 characters starting at index 0

Since both methods have identical signatures and receive the same offset and length parameters, they should likely behave consistently. Align the slicing approach across both deserializers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/default_audience_deserializer.rb` at line 9, Replace the inclusive-range
slice used in the JSON.parse call (currently JSON.parse(bytes[offset..length],
...)) with the start/length form to match the other deserializer; i.e., change
the slice to bytes[offset, length] so both deserializers use the same semantics
when parsing the JSON payload.

Comment on lines 47 to +52
arr.map do |item|
next if item.nil?
return item if item.is_a?(klass)

klass.new(*item.values)
end
end.compact
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid method-level return inside map.

At Line 49, return item exits assign_to_klass early and can return a single instance instead of an array.

💡 Proposed fix
   def assign_to_klass(klass, arr)
     arr.map do |item|
       next if item.nil?
-      return item if item.is_a?(klass)
+      next item if item.is_a?(klass)
 
       klass.new(*item.values)
     end.compact
   end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/json/experiment.rb` around lines 47 - 52, The method assign_to_klass uses
a method-level `return` inside arr.map which prematurely exits the method;
replace the `return item if item.is_a?(klass)` with a non-return expression
(e.g., yield the item back to the map by using `item` or `next item`) so the
block always produces values for mapping, keep the `klass.new(*item.values)`
branch as-is, and ensure the final `.compact` call still returns an array of
instances; reference arr.map, item.is_a?(klass), klass.new(*item.values), and
assign_to_klass when making the change.

Comment on lines +351 to +366
class CustomEventLogger < ContextEventLogger
def handle_event(event, data)
case event
when EVENT_TYPE::EXPOSURE
puts "Exposed to experiment: #{data[:name]}"
when EVENT_TYPE::GOAL
puts "Goal tracked: #{data[:name]}"
when EVENT_TYPE::ERROR
puts "Error: #{data}"
when EVENT_TYPE::PUBLISH
puts "Events published: #{data.length} events"
when EVENT_TYPE::READY
puts "Context ready with #{data[:experiments].length} experiments"
when EVENT_TYPE::REFRESH
puts "Context refreshed with #{data[:experiments].length} experiments"
when EVENT_TYPE::CLOSE
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Custom event logger example uses hash access for object payloads.

On Line 355, Line 357, Line 363, and Line 365, data[:...] assumes Hash payloads, but SDK event payloads here are objects (e.g., Exposure, GoalAchievement, ContextData), so this example can raise NoMethodError.

💡 Proposed doc fix
     when EVENT_TYPE::EXPOSURE
-      puts "Exposed to experiment: #{data[:name]}"
+      puts "Exposed to experiment: #{data.name}"
     when EVENT_TYPE::GOAL
-      puts "Goal tracked: #{data[:name]}"
+      puts "Goal tracked: #{data.name}"
@@
     when EVENT_TYPE::READY
-      puts "Context ready with #{data[:experiments].length} experiments"
+      puts "Context ready with #{data.experiments.length} experiments"
     when EVENT_TYPE::REFRESH
-      puts "Context refreshed with #{data[:experiments].length} experiments"
+      puts "Context refreshed with #{data.experiments.length} experiments"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class CustomEventLogger < ContextEventLogger
def handle_event(event, data)
case event
when EVENT_TYPE::EXPOSURE
puts "Exposed to experiment: #{data[:name]}"
when EVENT_TYPE::GOAL
puts "Goal tracked: #{data[:name]}"
when EVENT_TYPE::ERROR
puts "Error: #{data}"
when EVENT_TYPE::PUBLISH
puts "Events published: #{data.length} events"
when EVENT_TYPE::READY
puts "Context ready with #{data[:experiments].length} experiments"
when EVENT_TYPE::REFRESH
puts "Context refreshed with #{data[:experiments].length} experiments"
when EVENT_TYPE::CLOSE
class CustomEventLogger < ContextEventLogger
def handle_event(event, data)
case event
when EVENT_TYPE::EXPOSURE
puts "Exposed to experiment: #{data.name}"
when EVENT_TYPE::GOAL
puts "Goal tracked: #{data.name}"
when EVENT_TYPE::ERROR
puts "Error: #{data}"
when EVENT_TYPE::PUBLISH
puts "Events published: #{data.length} events"
when EVENT_TYPE::READY
puts "Context ready with #{data.experiments.length} experiments"
when EVENT_TYPE::REFRESH
puts "Context refreshed with #{data.experiments.length} experiments"
when EVENT_TYPE::CLOSE
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 351 - 366, The example CustomEventLogger#handle_event
assumes event payloads are Hashes (using data[:...]) but the SDK provides typed
objects (e.g., Exposure, GoalAchievement, ContextData), which will raise
NoMethodError; update the example to access attributes on the objects (e.g., use
data.name or data.experiments instead of data[:name]/data[:experiments], and
call length on the returned array where needed) for the cases handling
EVENT_TYPE::EXPOSURE, EVENT_TYPE::GOAL, EVENT_TYPE::PUBLISH, EVENT_TYPE::READY,
and EVENT_TYPE::REFRESH so the code uses object attribute access consistent with
the payload types.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant