Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
absmartly-sdk (1.2.1)
absmartly-sdk (1.2.2)
base64 (~> 0.2)
faraday (~> 2.0)
faraday-retry (~> 2.0)
Expand Down
87 changes: 64 additions & 23 deletions lib/a_b_smartly.rb
Original file line number Diff line number Diff line change
@@ -1,51 +1,92 @@
# frozen_string_literal: true

require "time"
require "singleton"
require "forwardable"
require_relative "context"
require_relative "audience_matcher"
require_relative "a_b_smartly_config"
require_relative "absmartly/version"
require_relative "default_context_data_provider"
require_relative "default_context_event_handler"
require_relative "default_variable_parser"
require_relative "default_audience_deserializer"
require_relative "scheduled_thread_pool_executor"

class ABSmartly
extend Forwardable
attr_accessor :context_data_provider, :context_event_handler,
:variable_parser, :scheduler, :context_event_logger,
:audience_deserializer, :client

attr_reader :config

def_delegators :@config, :context_data_provider, :context_event_handler, :variable_parser, :context_event_logger,
:audience_deserializer, :client

def_delegators :@config, :endpoint, :api_key, :application, :environment
def self.configure_client(&block)
@@init_http = block
end
Comment on lines +17 to +19
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
# Search for any usage of @@init_http or init_http across the codebase
rg -n '@@init_http|init_http' --type ruby

Repository: absmartly/ruby-sdk

Length of output: 151


Remove unused @@init_http class variable and configure_client method.

The @@init_http class variable is assigned in the configure_client method but is never referenced anywhere in the codebase. It is immediately reset to nil in the initialize method and serves no purpose. Remove the dead code to keep the implementation clean.

Suggested fix
-  def self.configure_client(&block)
-    @@init_http = block
-  end
-
   def self.create(config)
     ABSmartly.new(config)
   end

   def initialize(config)
-    @@init_http = nil
     `@context_data_provider` = config.context_data_provider
🤖 Prompt for AI Agents
In `@lib/a_b_smartly.rb` around lines 17 - 19, Remove the dead class variable and
method: delete the class variable @@init_http and the method configure_client
from the class (they are assigned but never used and reset to nil in
initialize). Also remove any references to @@init_http within initialize or
elsewhere (if present) so there are no leftover references; ensure initialize’s
behavior is unchanged aside from dropping the nil assignment. Run tests to
confirm no usages remain.


def self.create(config)
new(config)
ABSmartly.new(config)
end

def initialize(config)
config.validate!
@@init_http = nil
@context_data_provider = config.context_data_provider
@context_event_handler = config.context_event_handler
@context_event_logger = config.context_event_logger
@variable_parser = config.variable_parser
@audience_deserializer = config.audience_deserializer
@scheduler = config.scheduler

if @context_data_provider.nil? || @context_event_handler.nil?
@client = config.client
raise ArgumentError.new("Missing Client instance configuration") if @client.nil?

if @context_data_provider.nil?
@context_data_provider = DefaultContextDataProvider.new(@client)
end

if @context_event_handler.nil?
@context_event_handler = DefaultContextEventHandler.new(@client)
end
end

@config = config
if @variable_parser.nil?
@variable_parser = DefaultVariableParser.new
end

if @audience_deserializer.nil?
@audience_deserializer = DefaultAudienceDeserializer.new
end
if @scheduler.nil?
@scheduler = ScheduledThreadPoolExecutor.new(1)
end
end

def create_context(context_config)
Context.create(get_utc_format, context_config, context_data,
context_data_provider, context_event_handler, context_event_logger, variable_parser,
AudienceMatcher.new(audience_deserializer))
def create_context(config)
validate_params(config)
Context.create(get_utc_format, config, @context_data_provider.context_data,
@context_data_provider, @context_event_handler, @context_event_logger, @variable_parser,
AudienceMatcher.new(@audience_deserializer))
end

def create_context_with(context_config, data)
Context.create(get_utc_format, context_config, data,
context_data_provider, context_event_handler, context_event_logger, variable_parser,
AudienceMatcher.new(audience_deserializer))
def create_context_with(config, data)
Context.create(get_utc_format, config, data,
@context_data_provider, @context_event_handler, @context_event_logger, @variable_parser,
AudienceMatcher.new(@audience_deserializer))
end
Comment on lines +66 to 70
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

Missing validation in create_context_with.

Unlike create_context, this method does not call validate_params(config). This inconsistency could allow invalid unit configurations to slip through when using pre-fetched data.

🛠️ Suggested fix
 def create_context_with(config, data)
+  validate_params(config)
   Context.create(get_utc_format, config, data,
                  `@context_data_provider`, `@context_event_handler`, `@context_event_logger`, `@variable_parser`,
                  AudienceMatcher.new(`@audience_deserializer`))
 end
🤖 Prompt for AI Agents
In `@lib/a_b_smartly.rb` around lines 66 - 70, create_context_with is missing the
same parameter validation performed in create_context; call
validate_params(config) at the start of create_context_with (before using
config) to ensure invalid unit configurations are rejected, then proceed to call
Context.create(...) just like create_context does; reference the
create_context_with method and the validate_params(config) utility and ensure
the validation happens prior to constructing the Context and before invoking
AudienceMatcher.new(`@audience_deserializer`).


def context_data
context_data_provider.context_data
@context_data_provider.context_data
end

private
def get_utc_format
Time.now.utc.iso8601(3)
end

def validate_params(params)
params.units.each do |key, value|
unless value.is_a?(String) || value.is_a?(Numeric)
raise ArgumentError.new("Unit '#{key}' UID is of unsupported type '#{value.class}'. UID must be one of ['string', 'number']")
end

if value.to_s.size.zero?
raise ArgumentError.new("Unit '#{key}' UID length must be >= 1")
end
end
end
end
70 changes: 27 additions & 43 deletions lib/a_b_smartly_config.rb
Original file line number Diff line number Diff line change
@@ -1,65 +1,49 @@
# frozen_string_literal: true

require "forwardable"

require_relative "client"
require_relative "client_config"
require_relative "default_context_data_provider"
require_relative "default_context_event_handler"
require_relative "default_variable_parser"
require_relative "default_audience_deserializer"

class ABSmartlyConfig
extend Forwardable

attr_accessor :scheduler

attr_writer :context_data_provider, :context_event_handler, :audience_deserializer, :variable_parser, :client

attr_reader :client_config, :context_event_logger

def_delegators :@client_config, :endpoint, :api_key, :application, :environment
def_delegators :@client_config, :connect_timeout, :connection_request_timeout, :retry_interval, :max_retries

attr_accessor :context_data_provider, :context_event_handler,
:variable_parser, :scheduler, :context_event_logger,
:client, :audience_deserializer
def self.create
new
ABSmartlyConfig.new
end

def initialize
@client_config = ClientConfig.new
def context_data_provider=(context_data_provider)
@context_data_provider = context_data_provider
self
end

def validate!
raise ArgumentError.new("event logger not configured") if context_event_logger.nil?
raise ArgumentError.new("failed to initialize client") if client.nil?
raise ArgumentError.new("failed to initialize context_data_provider") if context_data_provider.nil?
def context_event_handler=(context_event_handler)
@context_event_handler = context_event_handler
self
end

def context_event_logger=(context_event_logger)
if context_event_logger.is_a?(Proc)
@context_event_logger = ContextEventLoggerCallback.new(context_event_logger)
else
@context_event_logger = context_event_logger
end
def context_data_provide
@context_event_handler
end
Comment on lines +21 to 23
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cat -n lib/a_b_smartly_config.rb | head -50

Repository: absmartly/ruby-sdk

Length of output: 1448


🏁 Script executed:

rg -n 'context_data_provide' --type ruby

Repository: absmartly/ruby-sdk

Length of output: 2005


🏁 Script executed:

rg -n 'context_data_provider' --type ruby

Repository: absmartly/ruby-sdk

Length of output: 1949


Remove dead code method with critical bug.

The method context_data_provide (lines 21–23) is never called anywhere in the codebase and contains two critical errors: the method name is a typo (missing 'r'), and it returns @context_event_handler instead of @context_data_provider. The correct reader context_data_provider is already generated by the attr_accessor on line 4. Delete this method entirely.

🤖 Prompt for AI Agents
In `@lib/a_b_smartly_config.rb` around lines 21 - 23, Delete the unused and buggy
method context_data_provide: it’s a dead/typoed method (missing the "r"),
returns the wrong instance variable `@context_event_handler` instead of
`@context_data_provider`, and is redundant because attr_accessor already generates
context_data_provider; remove the entire def context_data_provide ... end block
to fix.


def variable_parser
@variable_parser ||= DefaultVariableParser.new
def variable_parser=(variable_parser)
@variable_parser = variable_parser
self
end

def audience_deserializer
@audience_deserializer ||= DefaultAudienceDeserializer.new
def scheduler=(scheduler)
@scheduler = scheduler
self
end

def context_data_provider
@context_data_provider ||= DefaultContextDataProvider.new(client)
def context_event_logger=(context_event_logger)
@context_event_logger = context_event_logger
self
end

def context_event_handler
@context_event_handler ||= DefaultContextEventHandler.new(client)
def audience_deserializer=(audience_deserializer)
@audience_deserializer = audience_deserializer
self
end

def client
@client ||= Client.new(client_config)
def client=(client)
@client = client
self
end
end
25 changes: 17 additions & 8 deletions lib/absmartly.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@
require_relative "context_config"

module Absmartly
@@init_config = nil
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:

rg -n '@@init_config' --type ruby

Repository: absmartly/ruby-sdk

Length of output: 103


Remove unused class variable @@init_config.

This class variable is defined at line 11 but never referenced anywhere in the codebase. Remove it to eliminate dead code.

🤖 Prompt for AI Agents
In `@lib/absmartly.rb` at line 11, Delete the unused class variable @@init_config
from lib/absmartly.rb: locate the line defining @@init_config and remove that
declaration (and any trailing whitespace), ensuring no other references to
@@init_config exist elsewhere; run a quick grep across the repo to confirm it’s
not used and run tests/lint to validate no regressions.


class Error < StandardError
end

class << self
MUTEX = Thread::Mutex.new
attr_accessor :endpoint, :api_key, :application, :environment

def configure_client
yield sdk_config

sdk_config.validate!
yield self
end

def create
Expand All @@ -40,15 +40,24 @@ def context_data
sdk.context_data
end

private_constant :MUTEX

private
def client_config
@client_config = ClientConfig.create
@client_config.endpoint = @endpoint
@client_config.api_key = @api_key
@client_config.application = @application
@client_config.environment = @environment
@client_config
end

def sdk_config
MUTEX.synchronize { @sdk_config ||= ABSmartlyConfig.create }
@sdk_config = ABSmartlyConfig.create
@sdk_config.client = Client.create(client_config)
@sdk_config
end

def sdk
MUTEX.synchronize { @sdk ||= create }
@sdk ||= create
end
end
end
2 changes: 1 addition & 1 deletion lib/absmartly/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module Absmartly
VERSION = "1.2.1"
VERSION = "1.2.2"
end
67 changes: 49 additions & 18 deletions lib/client.rb
Original file line number Diff line number Diff line change
@@ -1,49 +1,80 @@
# frozen_string_literal: true

require "forwardable"
require_relative "default_http_client"
require_relative "default_http_client_config"
require_relative "default_context_data_deserializer"
require_relative "default_context_event_serializer"

class Client
extend Forwardable
attr_accessor :url, :query, :headers, :http_client, :executor, :deserializer, :serializer
attr_reader :data_future, :promise, :exception

attr_accessor :http_client
attr_reader :config, :data_future, :promise, :exception

def_delegators :@config, :url, :query, :headers, :deserializer, :serializer
def_delegator :@http_client, :close
def_delegator :@promise, :success?

def self.create(config = nil, http_client = nil)
new(config, http_client)
def self.create(config, http_client = nil)
Client.new(config, http_client || DefaultHttpClient.create(DefaultHttpClientConfig.create))
end

def initialize(config = nil, http_client = nil)
@config = config || ClientConfig.new
@config.validate!
endpoint = config.endpoint
raise ArgumentError.new("Missing Endpoint configuration") if endpoint.nil? || endpoint.empty?
Comment on lines 16 to +18
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

Misleading default value for config parameter.

The constructor declares config = nil but immediately calls config.endpoint on line 17, which will raise NoMethodError if config is actually nil. Either remove the default value to make it explicit that config is required, or add a nil guard with a clearer error message.

🐛 Suggested fix: remove misleading default
-  def initialize(config = nil, http_client = nil)
+  def initialize(config, http_client = nil)
     endpoint = config.endpoint
🤖 Prompt for AI Agents
In `@lib/client.rb` around lines 16 - 18, The initialize method currently declares
config = nil but immediately calls config.endpoint, which will raise
NoMethodError if nil; update initialize to require a non-nil config (remove the
default nil) or add an explicit nil guard at the start of initialize (e.g.,
check config.nil? and raise ArgumentError with a clear message like "Missing
configuration") before accessing config.endpoint so callers get a clear error;
refer to the initialize method in lib/client.rb when making this change.


api_key = config.api_key
raise ArgumentError.new("Missing APIKey configuration") if api_key.nil? || api_key.empty?

application = config.application
raise ArgumentError.new("Missing Application configuration") if application.nil? || application.empty?

environment = config.environment
raise ArgumentError.new("Missing Environment configuration") if environment.nil? || environment.empty?

@url = "#{endpoint}/context"
@http_client = http_client
@deserializer = config.context_data_deserializer
@serializer = config.context_event_serializer
@executor = config.executor

@http_client = http_client || DefaultHttpClient.create(@config.http_client_config)
@deserializer = DefaultContextDataDeserializer.new if @deserializer.nil?
@serializer = DefaultContextEventSerializer.new if @serializer.nil?

@headers = {
"Content-Type": "application/json",
"X-API-Key": api_key,
"X-Application": application,
"X-Environment": environment,
"X-Application-Version": "0",
"X-Agent": "absmartly-ruby-sdk"
}

@query = {
"application": application,
"environment": environment
}
end

def context_data
@promise = http_client.get(config.url, config.query, config.headers)
@promise = @http_client.get(@url, @query, @headers)
unless @promise.success?
@exception = Exception.new(@promise.body)
return self
end

content = (@promise.body || {}).to_s
@data_future = deserializer.deserialize(content, 0, content.size)
@data_future = @deserializer.deserialize(content, 0, content.size)
self
end

def publish(event)
content = serializer.serialize(event)
response = http_client.put(config.url, nil, config.headers, content)
content = @serializer.serialize(event)
response = @http_client.put(@url, nil, @headers, content)
return Exception.new(response.body) unless response.success?

response
end

def close
@http_client.close
end

def success?
@promise.success?
end
Comment on lines +77 to +79
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

Potential NoMethodError if success? is called before context_data.

The @promise instance variable is only set when context_data is called. If success? is invoked beforehand, it will raise NoMethodError: undefined method 'success?' for nil:NilClass.

🛠️ Suggested fix with nil guard
   def success?
-    `@promise.success`?
+    `@promise`&.success? || false
   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
def success?
@promise.success?
end
def success?
`@promise`&.success? || false
end
🤖 Prompt for AI Agents
In `@lib/client.rb` around lines 77 - 79, The success? method can raise
NoMethodError because `@promise` is only initialized in context_data; update
success? (method name success?) to guard against nil by either invoking
context_data to ensure `@promise` is initialized or returning a safe default when
`@promise` is nil (e.g., use safe navigation on `@promise` or explicit nil-check and
return false), so calling success? before context_data won’t raise an error.

end
Loading