Skip to content
Open
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
35 changes: 31 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,43 @@ end
| connection_request_timeout | `number` | ❌ | `3.0` | The request timeout in seconds. |
| retry_interval | `number` | ❌ | `0.5` | The initial retry interval in seconds (uses exponential backoff). |
| max_retries | `number` | ❌ | `5` | The maximum number of retries before giving up. |
| eventLogger | `(context, eventName, data) => void` | ❌ | See "Using a Custom Event Logger" below | A callback function which runs after SDK events. |
| event_logger | `ContextEventLogger` | ❌ | See "Using a Custom Event Logger" below | A `ContextEventLogger` instance implementing `handle_event(event, data)` to receive SDK events. |

### Using a Custom Event Logger

The A/B Smartly SDK can be instantiated with an event logger used for all
contexts. In addition, an event logger can be specified when creating a
particular context, in the `[CONTEXT_CONFIG_VARIABLE]`.
particular context in the context config.

```
Custom Event Logger Code
```ruby
class MyEventLogger < ContextEventLogger
def handle_event(event, data)
case event
when EVENT_TYPE::EXPOSURE
puts "Exposure: #{data}"
when EVENT_TYPE::GOAL
puts "Goal: #{data}"
when EVENT_TYPE::ERROR
puts "Error: #{data}"
when EVENT_TYPE::PUBLISH
puts "Publish: #{data}"
when EVENT_TYPE::READY
puts "Ready: #{data}"
when EVENT_TYPE::REFRESH
puts "Refresh: #{data}"
when EVENT_TYPE::CLOSE
puts "Close"
end
end
end

Absmartly.configure_client do |config|
config.endpoint = "https://your-company.absmartly.io/v1"
config.api_key = "YOUR-API-KEY"
config.application = "website"
config.environment = "development"
config.event_logger = MyEventLogger.new
end
```

The data parameter depends on the type of event. Currently, the SDK logs the
Expand Down
4 changes: 3 additions & 1 deletion lib/absmartly.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ class Error < StandardError

class << self
attr_accessor :endpoint, :api_key, :application, :environment,
:connect_timeout, :connection_request_timeout, :retry_interval, :max_retries
:connect_timeout, :connection_request_timeout, :retry_interval, :max_retries,
:event_logger

def configure_client
yield self
Expand Down Expand Up @@ -56,6 +57,7 @@ def client_config
def sdk_config
@sdk_config = ABSmartlyConfig.create
@sdk_config.client = Client.create(client_config)
@sdk_config.context_event_logger = @event_logger
@sdk_config
end

Expand Down
219 changes: 219 additions & 0 deletions spec/absmartly_spec.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,39 @@
# frozen_string_literal: true

require "context"
require "context_config"
require "default_context_data_deserializer"
require "default_variable_parser"
require "default_audience_deserializer"
require "context_data_provider"
require "default_context_data_provider"
require "context_event_handler"
require "context_event_logger"
require "audience_matcher"
require "json/unit"
require "logger"

class MockContextEventLoggerProxy < ContextEventLogger
attr_accessor :called, :events, :logger

def initialize
@called = 0
@events = []
@logger = Logger.new(STDOUT)
@logger.level = Logger::WARN
end

def handle_event(event, data)
@called += 1
@events << { event: event, data: data }
end

def clear
@called = 0
@events = []
end
end

RSpec.describe Absmartly do
it "has a version number" do
expect(Absmartly::VERSION).not_to be nil
Expand Down Expand Up @@ -39,4 +73,189 @@
expect(Absmartly.max_retries).to eq(3)
end
end

describe ".event_logger" do
after do
Absmartly.event_logger = nil
end

it "has event_logger accessor" do
expect(Absmartly).to respond_to(:event_logger)
expect(Absmartly).to respond_to(:event_logger=)
end

it "can be set via configure_client" do
logger = double("event_logger")

Absmartly.configure_client do |config|
config.event_logger = logger
end

expect(Absmartly.event_logger).to eq(logger)
end

it "flows through to ABSmartlyConfig.context_event_logger" do
logger = double("event_logger")

Absmartly.configure_client do |config|
config.endpoint = "https://test.absmartly.io/v1"
config.api_key = "test-api-key"
config.application = "test-app"
config.environment = "test"
config.event_logger = logger
end

sdk_config = Absmartly.send(:sdk_config)
expect(sdk_config.context_event_logger).to eq(logger)
end
end

describe ".event_logger integration" do
let(:units) do
{
session_id: "e791e240fcd3df7d238cfc285f475e8152fcc0ec",
user_id: "123456789",
email: "[email protected]"
}
end
let(:publish_units) do
[
Unit.new("session_id", "pAE3a1i5Drs5mKRNq56adA"),
Unit.new("user_id", "JfnnlDI7RTiF9RgfG2JNCw"),
Unit.new("email", "IuqYkNRfEx5yClel4j3NbA")
]
end
let(:clock) { Time.at(1620000000000 / 1000) }
let(:clock_in_millis) { clock.to_i }

let(:descr) { DefaultContextDataDeserializer.new }
let(:json) { resource("context.json") }
let(:data) { descr.deserialize(json, 0, json.length) }

let(:data_future) { OpenStruct.new(data_future: nil, success?: true) }

let(:data_provider) { DefaultContextDataProvider.new(client_mock) }
let(:data_future_ready) { data_provider.context_data }

let(:publish_future) { OpenStruct.new(success?: true) }
let(:event_handler) do
ev = instance_double(ContextEventHandler)
allow(ev).to receive(:publish).and_return(publish_future)
ev
end

let(:mock_logger) do
logger = MockContextEventLoggerProxy.new
allow(logger).to receive(:handle_event).and_call_original
logger
end

let(:variable_parser) { DefaultVariableParser.new }
let(:audience_matcher) { AudienceMatcher.new(DefaultAudienceDeserializer.new) }

def client_mock
client = instance_double(Client)
allow(client).to receive(:context_data).and_return(OpenStruct.new(data_future: data, success?: true))
allow(client).to receive(:publish).and_return(OpenStruct.new(success?: true))
client
end

def create_ready_context
config = ContextConfig.create
config.set_units(units)

Absmartly.create_context(config)
end

after do
Absmartly.endpoint = nil
Absmartly.api_key = nil
Absmartly.application = nil
Absmartly.environment = nil
Absmartly.event_logger = nil
Absmartly.instance_variable_set(:@sdk, nil)
Absmartly.instance_variable_set(:@sdk_config, nil)
end

context "when configured globally" do
before do
allow(Client).to receive(:create).and_return(client_mock)

Absmartly.configure_client do |config|
config.endpoint = "https://test.absmartly.io/v1"
config.api_key = "test-key"
config.application = "test-app"
config.environment = "test"
config.event_logger = mock_logger
end
end

it "receives READY event on context creation" do
mock_logger.clear
create_ready_context
expect(mock_logger).to have_received(:handle_event)
.with(ContextEventLogger::EVENT_TYPE::READY, data).once
end

it "receives EXPOSURE event with correct values when treatment() is called" do
mock_logger.clear
context = create_ready_context
mock_logger.clear

context.treatment("exp_test_ab")

expect(mock_logger).to have_received(:handle_event)
.with(ContextEventLogger::EVENT_TYPE::EXPOSURE, satisfy { |exposure|
exposure.id == 1 &&
exposure.name == "exp_test_ab" &&
exposure.unit == "session_id" &&
exposure.variant == 1 &&
exposure.assigned == true &&
exposure.eligible == true &&
exposure.overridden == false &&
exposure.full_on == false &&
exposure.custom == false &&
exposure.audience_mismatch == false
}).once
end

it "receives GOAL event with correct values when track() is called" do
mock_logger.clear
context = create_ready_context
mock_logger.clear

properties = { amount: 125, hours: 245 }
context.track("goal1", properties)

expect(mock_logger).to have_received(:handle_event)
.with(ContextEventLogger::EVENT_TYPE::GOAL, satisfy { |goal|
goal.name == "goal1" &&
goal.properties == properties
}).once
end

it "receives PUBLISH event when publish() is called" do
mock_logger.clear
context = create_ready_context
context.track("goal1", { amount: 125 })
mock_logger.clear

context.publish

expect(mock_logger).to have_received(:handle_event)
.with(ContextEventLogger::EVENT_TYPE::PUBLISH, instance_of(PublishEvent)).once
end

it "receives CLOSE event when close() is called" do
mock_logger.clear
context = create_ready_context
mock_logger.clear

context.close

expect(mock_logger).to have_received(:handle_event)
.with(ContextEventLogger::EVENT_TYPE::CLOSE, nil).once
end
end
end
end