diff --git a/README b/README deleted file mode 100644 index dc450af..0000000 --- a/README +++ /dev/null @@ -1,180 +0,0 @@ -# A/B Smartly SDK - -A/B Smartly Ruby SDK - -## Compatibility - -The A/B Smartly Ruby SDK is compatible with Ruby versions 2.7 and later. For the best performance and code readability, Ruby 3 or later is recommended. This SDK is being constantly tested with the nightly builds of Ruby, to ensure it is compatible with the latest Ruby version. - -## Getting Started - -### Install the SDK - -Install the gem and add to the application's Gemfile by executing: - - $ bundle add absmartly-sdk - -If bundler is not being used to manage dependencies, install the gem by executing: - - $ gem install absmartly-sdk - -## Basic Usage - -Once the SDK is installed, it can be initialized in your project. - -You can create an SDK instance using the API key, application name, environment, and the endpoint URL obtained from A/B Smartly. - -```Ruby -require 'absmartly' - -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" -end -``` -#### Creating a new Context with raw promises - -```Ruby -# define a new context request -context_config = Absmartly.create_context_config -context_config.set_unit("session_id", "bf06d8cb5d8137290c4abb64155584fbdb64d8") -context_config.set_unit("user_id", "123456") - -context = Absmartly.create_context(context_config) -``` - -### Selecting A Treatment - -```Ruby -treatment = context.treatment('exp_test_experiment') - -if treatment.zero? - # user is in control group (variant 0) -else - # user is in treatment group -end -``` - -### Treatment Variables - -```Ruby -default_button_color_value = 'red' -button_color = context.variable_value('button.color') -``` - -### Peek at Treatment Variants - -Although generally not recommended, it is sometimes necessary to peek at a treatment or variable without triggering an exposure. The A/B Smartly SDK provides a `Context.peek_treatment()` method for that. - -```Ruby -treatment = context.peek_treatment('exp_test_experiment') - -if treatment.zero? - # user is in control group (variant 0) -else - # user is in treatment group -end -``` - -#### Peeking at variables - -```Ruby -button_color = context.peek_variable_value('button.color', 'red') -``` - -### Overriding Treatment Variants - -During development, for example, it is useful to force a treatment for an -experiment. This can be achieved with the `Context.set_override()` and/or `Context.set_overrides()` methods. These methods can be called before the context is ready. - -```Ruby -context.set_override("exp_test_experiment", 1) # force variant 1 of treatment - -context.set_overrides( - 'exp_test_experiment' => 1, - 'exp_another_experiment' => 0, -) -``` - -## Advanced - -### Context Attributes - -Attributes are used to pass meta-data about the user and/or the request. -They can be used later in the Web Console to create segments or audiences. -They can be set using the `context.set_attribute()` or `context.set_attributes()` methods, before or after the context is ready. - -```Ruby -context.set_attribute('session_id', session_id) -context.set_attributes( - 'customer_age' => 'new_customer' -) -``` - -### Custom Assignments - -Sometimes it may be necessary to override the automatic selection of a variant. For example, if you wish to have your variant chosen based on data from an API call. This can be accomplished using the `Context.set_custom_assignment()` method. - -```Ruby -chosen_variant = 1 -context.set_custom_assignment("experiment_name", chosen_variant) -``` - -If you are running multiple experiments and need to choose different custom assignments for each one, you can do so using the `Context->setCustomAssignments()` method. - -```Ruby -assignments = [ - "experiment_name" => 1, - "another_experiment_name" => 0, - "a_third_experiment_name" => 2 -] - -context.set_custom_assignments(assignments) -``` - -### Publish - -Sometimes it is necessary to ensure all events have been published to the A/B Smartly collector, before proceeding. You can explicitly call the `context.publish()` method. - -```Ruby -context.publish -``` - -### Finalize - -The `close()` method will ensure all events have been published to the A/B Smartly collector, like `context.publish()`, and will also "seal" the context, throwing an error if any method that could generate an event is called. - -```Ruby -context.close -``` - -### Tracking Goals - -```Ruby -context.track( - 'payment', - { item_count: 1, total_amount: 1999.99 } -) -``` - - - -## Development - -After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. - -To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org). - -## Contributing - -Bug reports and pull requests are welcome on GitHub at https://github.com/omairazam/absmartly. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/omairazam/absmartly/blob/master/CODE_OF_CONDUCT.md). - -## License - -The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). - -## Code of Conduct - -Everyone interacting in the Absmartly project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/omairazam/absmartly/blob/master/CODE_OF_CONDUCT.md). diff --git a/README.md b/README.md new file mode 100644 index 0000000..ad73463 --- /dev/null +++ b/README.md @@ -0,0 +1,253 @@ +# A/B Smartly SDK + +A/B Smartly Ruby SDK + +## Compatibility + +The A/B Smartly Ruby SDK is compatible with Ruby versions 2.7 and later. For the best performance and code readability, Ruby 3 or later is recommended. This SDK is being constantly tested with the nightly builds of Ruby, to ensure it is compatible with the latest Ruby version. + + +## Getting Started + +### Install the SDK + +Install the gem and add to the application's Gemfile by executing: + + $ bundle add absmartly-sdk + +If bundler is not being used to manage dependencies, install the gem by executing: + + $ gem install absmartly-sdk + +## Import and Initialize the SDK + +Once the SDK is installed, it can be initialized in your project. + +```ruby +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" +end +``` + +**SDK Options** + +| Config | Type | Required? | Default | Description | +| :---------- | :----------------------------------- | :-------: | :-------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| endpoint | `string` | ✅ | `undefined` | The URL to your API endpoint. Most commonly `"your-company.absmartly.io"` | +| apiKey | `string` | ✅ | `undefined` | Your API key which can be found on the Web Console. | +| environment | `"production"` or `"development"` | ✅ | `undefined` | The environment of the platform where the SDK is installed. Environments are created on the Web Console and should match the available environments in your infrastructure. | +| application | `string` | ✅ | `undefined` | The name of the application where the SDK is installed. Applications are created on the Web Console and should match the applications where your experiments will be running. | +| retries | `number` | ❌ | `5` | The number of retries before the SDK stops trying to connect. | +| timeout | `number` | ❌ | `3000` | An amount of time, in milliseconds, before the SDK will stop trying to connect. | +| eventLogger | `(context, eventName, data) => void` | ❌ | See "Using a Custom Event Logger" below | A callback function which runs after 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]`. + +``` +Custom Event Logger Code +``` + +The data parameter depends on the type of event. Currently, the SDK logs the +following events: + +| eventName | when | data | +| ------------ | ------------------------------------------------------- | -------------------------------------------- | +| `"error"` | `Context` receives an error | error object thrown | +| `"ready"` | `Context` turns ready | data used to initialize the context | +| `"refresh"` | `Context.refresh()` method succeeds | data used to refresh the context | +| `"publish"` | `Context.publish()` method succeeds | data sent to the A/B Smartly event collector | +| `"exposure"` | `Context.treatment()` method succeeds on first exposure | exposure data enqueued for publishing | +| `"goal"` | `Context.track()` method succeeds | goal data enqueued for publishing | +| `"close"` | `Context.close()` method succeeds the first time | nil | + +## Create a New Context Request + + +```ruby +context_config = Absmartly.create_context_config +``` + +**With Prefetched Data** + +```ruby +client_config = ClientConfig.new( + endpoint: 'https://your-company.absmartly.io/v1', + api_key: 'YOUR-API-KEY', + application: 'website', + environment: 'development') + +sdk_config = ABSmartlyConfig.create +sdk_config.client = Client.create(client_config) + +sdk = Absmartly.create(sdk_config) +``` + +**Refreshing the Context with Fresh Experiment Data** + +For long-running contexts, the context is usually created once when the +application is first started. However, any experiments being tracked in your +production code, but started after the context was created, will not be +triggered. + +Alternatively, the `refresh` method can be called manually. The +`refresh` method pulls updated experiment data from the A/B +Smartly collector and will trigger recently started experiments when +`treatment` is called again. + +**Setting Extra Units** + +You can add additional units to a context by calling the `set_unit()` or +`set_units()` methods. These methods may be used, for example, when a user +logs in to your application and you want to use the new unit type in the +context. + +Please note, you cannot override an already set unit type as that would be +a change of identity and would throw an exception. In this case, you must +create a new context instead. The `set_unit()` and +`set_units()` methods can be called before the context is ready. + +```ruby +context_config.set_unit('session_id', 'bf06d8cb5d8137290c4abb64155584fbdb64d8') +context_config.set_unit('user_id', '123456') +context = Absmartly.create_context(context_config) +``` +or +```ruby +context_config.set_units( + session_id: 'bf06d8cb5d8137290c4abb64155584fbdb64d8', + user_id: '123456' +) +context = Absmartly.create_context(context_config) +``` + +## Basic Usage + +### Selecting A Treatment + +```ruby +treatment = context.treatment('exp_test_experiment') + +if treatment.zero? + # user is in control group (variant 0) +else + # user is in treatment group +end +``` + +### Treatment Variables + +```ruby +default_button_color_value = 'red' + +context.variable_value('experiment_name', default_button_color_value) +``` + +### Peek at Treatment Variants + +Although generally not recommended, it is sometimes necessary to peek at +a treatment or variable without triggering an exposure. The A/B Smartly +SDK provides a `peek_treatment()` method for that. + +```ruby +treatment = context.peek_treatment('exp_test_experiment') +``` + +#### Peeking at variables + +```ruby +treatment = context.peek_variable_value('exp_test_experiment') +``` + +### Overriding Treatment Variants + +During development, for example, it is useful to force a treatment for an +experiment. This can be achieved with the `set_override()` and/or `set_overrides()` +methods. These methods can be called before the context is ready. + +```ruby +context.set_override("exp_test_experiment", 1) # force variant 1 of treatment + +context.set_overrides( + 'exp_test_experiment' => 1, + 'exp_another_experiment' => 0, +) +``` + +## Advanced + +### Context Attributes + +Attributes are used to pass meta-data about the user and/or the request. +They can be used later in the Web Console to create segments or audiences. +They can be set using the `set_attribute()` or `set_attributes()` +methods, before or after the context is ready. + +```ruby +context.set_attribute('session_id', session_id) +context.set_attributes( + 'customer_age' => 'new_customer' +) +``` + +### Custom Assignments + +Sometimes it may be necessary to override the automatic selection of a +variant. For example, if you wish to have your variant chosen based on +data from an API call. This can be accomplished using the +`set_custom_assignment()` method. + +```ruby +chosen_variant = 1 +context.set_custom_assignment('experiment_name', chosen_variant) +``` + +If you are running multiple experiments and need to choose different +custom assignments for each one, you can do so using the +`set_custom_assignments()` method. + +```ruby +assignments = [ + 'experiment_name' => 1, + 'another_experiment_name' => 0, + 'a_third_experiment_name' => 2 +] + +context.set_custom_assignments(assignments) +``` + +### Publish + +Sometimes it is necessary to ensure all events have been published to the +A/B Smartly collector, before proceeding. You can explicitly call the +`publish()` methods. + +``` +context.publish +``` + +### Finalize + +The `close()` method will ensure all events have been +published to the A/B Smartly collector, like `publish()`, and will also +"seal" the context, throwing an error if any method that could generate +an event is called. + +``` +context.close +``` + +### Tracking Goals + +```ruby +context.track( + 'payment', + { item_count: 1, total_amount: 1999.99 } +) +``` diff --git a/absmartly.gemspec b/absmartly.gemspec index 25f5d71..8e086ca 100644 --- a/absmartly.gemspec +++ b/absmartly.gemspec @@ -15,7 +15,7 @@ Gem::Specification.new do |spec| spec.license = "MIT" spec.required_ruby_version = ">= 2.7.0" - spec.extra_rdoc_files = ['README'] + spec.extra_rdoc_files = ["README"] spec.metadata["homepage_uri"] = spec.homepage spec.metadata["source_code_uri"] = "https://github.com/absmartly/ruby-sdk" diff --git a/example/example.rb b/example/example.rb index eb71160..5fd6f18 100644 --- a/example/example.rb +++ b/example/example.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "../lib/absmartly" # config file @@ -25,7 +27,7 @@ puts(treatment3) # 1 ctx.set_unit("db_user_id", 1000013) -ctx.set_units(db_user_id2: 1000013, session_id: 12311) +ctx.set_units(db_user_id2: 1000013, session_id2: 12311) ctx.set_attribute("user_agent", "Chrome 2022") ctx.set_attributes( diff --git a/lib/a_b_smartly.rb b/lib/a_b_smartly.rb index 31c3c09..246b6e7 100644 --- a/lib/a_b_smartly.rb +++ b/lib/a_b_smartly.rb @@ -58,7 +58,7 @@ def initialize(config) def create_context(config) validate_params(config) - Context.create(get_utc_format, config, @scheduler, @context_data_provider.context_data, + 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 diff --git a/lib/context.rb b/lib/context.rb index aa33cf0..a2028c6 100644 --- a/lib/context.rb +++ b/lib/context.rb @@ -2,6 +2,7 @@ require_relative "hashing" require_relative "variant_assigner" +require_relative "context_event_logger" require_relative "json/unit" require_relative "json/attribute" require_relative "json/exposure" @@ -11,13 +12,13 @@ class Context attr_reader :data, :pending_count - def self.create(clock, config, scheduler, data_future, data_provider, + def self.create(clock, config, data_future, data_provider, event_handler, event_logger, variable_parser, audience_matcher) - Context.new(clock, config, scheduler, data_future, data_provider, + Context.new(clock, config, data_future, data_provider, event_handler, event_logger, variable_parser, audience_matcher) end - def initialize(clock, config, scheduler, data_future, data_provider, + def initialize(clock, config, data_future, data_provider, event_handler, event_logger, variable_parser, audience_matcher) @index = [] @achievements = [] @@ -31,7 +32,6 @@ def initialize(clock, config, scheduler, data_future, data_provider, @data_provider = data_provider @variable_parser = variable_parser @audience_matcher = audience_matcher - @scheduler = scheduler @closed = false @units = {} @@ -49,8 +49,10 @@ def initialize(clock, config, scheduler, data_future, data_provider, set_custom_assignments(config.custom_assignments) if config.custom_assignments if data_future.success? assign_data(data_future.data_future) + log_event(ContextEventLogger::EVENT_TYPE::READY, data_future.data_future) else set_data_failed(data_future.exception) + log_error(data_future.exception) end end @@ -157,11 +159,11 @@ def queue_exposure(assignment) assignment.exposed = true exposure = Exposure.new - exposure.id = assignment.id + exposure.id = assignment.id || 0 exposure.name = assignment.name exposure.unit = assignment.unit_type exposure.variant = assignment.variant - exposure.exposed_at = @clock + exposure.exposed_at = @clock.to_i exposure.assigned = assignment.assigned exposure.eligible = assignment.eligible exposure.overridden = assignment.overridden @@ -171,6 +173,7 @@ def queue_exposure(assignment) @pending_count += 1 @exposures.push(exposure) + log_event(ContextEventLogger::EVENT_TYPE::EXPOSURE, exposure) end end @@ -221,6 +224,7 @@ def track(goal_name, properties) @pending_count += 1 @achievements.push(achievement) + log_event(ContextEventLogger::EVENT_TYPE::GOAL, achievement) end def publish @@ -236,8 +240,10 @@ def refresh data_future = @data_provider.context_data if data_future.success? assign_data(data_future.data_future) + log_event(ContextEventLogger::EVENT_TYPE::REFRESH, data_future.data_future) else set_data_failed(data_future.exception) + log_error(data_future.exception) end end end @@ -248,6 +254,7 @@ def close flush end @closed = true + log_event(ContextEventLogger::EVENT_TYPE::CLOSE, nil) end end @@ -282,18 +289,20 @@ def flush event.hashed = true event.published_at = @clock.to_i event.units = @units.map do |key, value| - Unit.new(key, unit_hash(key, value)) + Unit.new(key.to_s, unit_hash(key, value)) end - event.attributes = @attributes.empty? ? nil : @attributes event.exposures = exposures - event.goals = achievements - return @event_handler.publish(self, event) + event.attributes = @attributes unless @attributes.empty? + event.goals = achievements unless achievements.nil? + log_event(ContextEventLogger::EVENT_TYPE::PUBLISH, event) + @event_handler.publish(self, event) end end else @exposures = [] @achievements = [] @pending_count = 0 + @data_failed end end @@ -365,7 +374,7 @@ def assignment(experiment_name) hash end match = @audience_matcher.evaluate(experiment.data.audience, attrs) - if match.nil? || !match.result + if match && !match.result assignment.audience_mismatch = true end end @@ -471,6 +480,18 @@ def set_data_failed(exception) @failed = true end + def log_event(event, data) + unless @event_logger.nil? + @event_logger.handle_event(event, data) + end + end + + def log_error(error) + unless @event_logger.nil? + @event_logger.handle_event(ContextEventLogger::EVENT_TYPE::ERROR, error.message) + end + end + attr_accessor :clock, :publish_delay, :event_handler, @@ -478,7 +499,6 @@ def set_data_failed(exception) :data_provider, :variable_parser, :audience_matcher, - :scheduler, :units, :failed, :data_lock, diff --git a/lib/context_config.rb b/lib/context_config.rb index e59a08a..cbe5c64 100644 --- a/lib/context_config.rb +++ b/lib/context_config.rb @@ -67,4 +67,12 @@ def set_custom_assignments(customAssignments) def custom_assignment(experiment_name) @custom_assignments[experiment_name.to_sym] end + + + def set_event_logger(event_logger) + @event_logger = event_logger + self + end + + attr_reader :event_logger end diff --git a/lib/context_event_logger.rb b/lib/context_event_logger.rb index d59de91..4aab7b1 100644 --- a/lib/context_event_logger.rb +++ b/lib/context_event_logger.rb @@ -1,9 +1,17 @@ # frozen_string_literal: true class ContextEventLogger - EVENT_TYPE = %w[error ready refresh publish exposure goal close] - # @interface method - def handle_event - raise NotImplementedError.new("You must implement handleEvent method.") + module EVENT_TYPE + ERROR = "error" + READY = "ready" + REFRESH = "refresh" + PUBLISH = "publish" + EXPOSURE = "exposure" + GOAL = "goal" + CLOSE = "close" + end + + def handle_event(event, data) + raise NotImplementedError.new("You must implement handle_event method.") end end diff --git a/lib/context_event_logger_callback.rb b/lib/context_event_logger_callback.rb new file mode 100644 index 0000000..b6b76b9 --- /dev/null +++ b/lib/context_event_logger_callback.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class ContextEventLoggerCallback < ContextEventLogger + attr_accessor :callable + + def initialize(callable) + @callable = callable + end + + def handle_event(event, data) + @callable.call(event, data) if @callable.present? + end +end diff --git a/lib/json/exposure.rb b/lib/json/exposure.rb index 807a524..a2bb683 100644 --- a/lib/json/exposure.rb +++ b/lib/json/exposure.rb @@ -8,7 +8,7 @@ def initialize(id = nil, name = nil, unit = nil, variant = nil, exposed_at = nil, assigned = nil, eligible = nil, overridden = nil, full_on = nil, custom = nil, audience_mismatch = nil) - @id = id + @id = id || 0 @name = name @unit = unit @variant = variant diff --git a/spec/client_spec.rb b/spec/client_spec.rb index 5a20f22..a9b98fb 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require "byebug" require "client" require "client_config" require "json/context_data" diff --git a/spec/context_spec.rb b/spec/context_spec.rb index ca5a58b..ad81b10 100644 --- a/spec/context_spec.rb +++ b/spec/context_spec.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require "byebug" require "context" require "context_config" require "default_context_data_deserializer" @@ -61,13 +60,13 @@ } let(:publish_units) { [ - Unit.new("user_id", "JfnnlDI7RTiF9RgfG2JNCw"), Unit.new("session_id", "pAE3a1i5Drs5mKRNq56adA"), + Unit.new("user_id", "JfnnlDI7RTiF9RgfG2JNCw"), Unit.new("email", "IuqYkNRfEx5yClel4j3NbA") ] } let(:clock) { Time.at(1620000000000 / 1000) } - let(:clock_in_millis) { 1620000000000 } + let(:clock_in_millis) { clock.to_i } let(:descr) { DefaultContextDataDeserializer.new } let(:json) { resource("context.json") } @@ -105,12 +104,15 @@ allow(ev).to receive(:publish).and_return(publish_future) ev end - let(:event_logger) { instance_double(ContextEventLogger) } + let(:event_logger) do + event_logger = MockContextEventLoggerProxy.new + allow(event_logger).to receive(:handle_event).and_return(nil) + event_logger + end let(:variable_parser) { DefaultVariableParser.new } let(:audience_matcher) { AudienceMatcher.new(DefaultAudienceDeserializer.new) } - let(:scheduler) { instance_double(ScheduledExecutorService) } let(:failure) { Exception.new("FAILED") } - let(:failure_future) { OpenStruct.new(exception: Exception.new("FAILED"), success?: false) } + let(:failure_future) { OpenStruct.new(exception: failure, success?: false, data_future: nil) } def http_client_mock http_client = instance_double(DefaultHttpClient) @@ -126,7 +128,7 @@ def client_mock(data_future = nil) def failed_client_mock client = instance_double(Client) - allow(client).to receive(:context_data).and_return(OpenStruct.new(exception: Exception.new("Failed"), success?: false, data_future: nil)) + allow(client).to receive(:context_data).and_return(failure_future) client end @@ -136,7 +138,7 @@ def create_context(data_future = nil, config: nil, evt_handler: nil, dt_provider config.set_units(units) end - Context.create(clock, config, scheduler, data_future || data_future_ready, dt_provider || data_provider, + Context.create(clock, config, data_future || data_future_ready, dt_provider || data_provider, evt_handler || event_handler, event_logger, variable_parser, audience_matcher) end @@ -144,7 +146,7 @@ def create_ready_context(evt_handler: nil) config = ContextConfig.create config.set_units(units) - Context.create(clock, config, scheduler, data_future_ready, data_provider, + Context.create(clock, config, data_future_ready, data_provider, evt_handler || event_handler, event_logger, variable_parser, audience_matcher) end @@ -152,7 +154,7 @@ def create_failed_context config = ContextConfig.create config.set_units(units) - Context.create(clock, config, scheduler, data_future_failed, failed_data_provider, + Context.create(clock, config, data_future_failed, failed_data_provider, event_handler, event_logger, variable_parser, audience_matcher) end @@ -201,26 +203,16 @@ def faraday_response(content) expect(context.failed?).to be_truthy end - xit "calls event logger when ready" do - context = create_ready_context - - data_future.complete(data) - - expect(event_logger).to have_received(:handle_event).with(context, ContextEventLogger::EVENT_TYPE[:ready], data).once - end + it "calls event logger when ready" do + create_ready_context - xit "callsEventLoggerWithCompletedFuture" do - context = create_ready_context - expect(event_logger).to have_received(:handle_event).with(context, ContextEventLogger::EVENT_TYPE[:ready], data).once + expect(event_logger).to have_received(:handle_event).with(ContextEventLogger::EVENT_TYPE::READY, data).once end - xit "callsEventLoggerWithException" do - context = create_context(data_future) - - error = Exception.new("FAILED") - data_future.completeExceptionally(error) + it "callsEventLoggerWithException" do + create_context(data_future_failed) - expect(event_logger).to have_received(:handle_event).with(context, ContextEventLogger::EVENT_TYPE[:error], data).once + expect(event_logger).to have_received(:handle_event).with(ContextEventLogger::EVENT_TYPE::ERROR, "FAILED").once end it "throwsWhenNotReady" do @@ -619,6 +611,7 @@ def faraday_response(content) context.publish expect(event_handler).to have_received(:publish).once + expect(event_handler).to have_received(:publish).with(context, expected).once end it "getVariableValueQueuesExposureWithAudienceMismatchTrueOnAudienceMismatch" do @@ -646,6 +639,7 @@ def faraday_response(content) context.publish expect(event_handler).to have_received(:publish).once + expect(event_handler).to have_received(:publish).with(context, expected).once end it "getVariableValueQueuesExposureWithAudienceMismatchFalseAndControlVariantOnAudienceMismatchInStrictMode" do @@ -674,11 +668,13 @@ def faraday_response(content) context.publish expect(event_handler).to have_received(:publish).once + expect(event_handler).to have_received(:publish).with(context, expected).once end - xit "getVariableValueCallsEventLogger" do + it "getVariableValueCallsEventLogger" do context = create_ready_context + expect(event_logger).to have_received(:handle_event).with(ContextEventLogger::EVENT_TYPE::READY, data).once context.variable_value("banner.border", nil) context.variable_value("banner.size", nil) @@ -686,13 +682,13 @@ def faraday_response(content) Exposure.new(1, "exp_test_ab", "session_id", 1, clock_in_millis, true, true, false, false, false, false), ] - expect(event_logger).to have_received(:handle_event).exactly(exposures.length).time + expect(event_logger).to have_received(:handle_event).with(ContextEventLogger::EVENT_TYPE::EXPOSURE, exposures.first).exactly(exposures.length).time - # verify not called again with the same exposure + event_logger.clear context.variable_value("banner.border", nil) context.variable_value("banner.size", nil) - expect(event_handler).to have_received(:handle_event).exactly(0).time + expect(event_logger.called).to eq(0) end it "getVariableKeys" do @@ -736,7 +732,7 @@ def faraday_response(content) end it "treatment" do - context = create_ready_context + context = create_ready_context(evt_handler: event_handler) data.experiments.each do |experiment| expect(context.treatment(experiment.name)).to eq(expected_variants[experiment.name.to_sym]) @@ -765,14 +761,13 @@ def faraday_response(content) allow(event_handler).to receive(:publish).and_return(publish_future) context.publish - expect(event_handler).to have_received(:publish).once - + expect(event_handler).to have_received(:publish).with(context, expected).once context.close end it "treatmentReturnsOverrideVariant" do - context = create_ready_context + context = create_ready_context(evt_handler: event_handler) data.experiments.each do |experiment| context.set_override(experiment.name, 11 + expected_variants[experiment.name.to_s.to_sym]) @@ -805,6 +800,7 @@ def faraday_response(content) context.publish expect(event_handler).to have_received(:publish).once + expect(event_handler).to have_received(:publish).with(context, expected).once context.close end @@ -836,7 +832,7 @@ def faraday_response(content) end it "treatmentQueuesExposureWithAudienceMismatchFalseOnAudienceMatch" do - context = create_context(audience_data_future_ready) + context = create_context(audience_data_future_ready, evt_handler: event_handler) context.set_attribute("age", 21) expect(context.treatment("exp_test_ab")).to eq(1) @@ -859,6 +855,7 @@ def faraday_response(content) context.publish expect(event_handler).to have_received(:publish).once + expect(event_handler).to have_received(:publish).with(context, expected).once end it "treatmentQueuesExposureWithAudienceMismatchTrueOnAudienceMismatch" do @@ -885,6 +882,7 @@ def faraday_response(content) context.publish expect(event_handler).to have_received(:publish).once + expect(event_handler).to have_received(:publish).with(context, expected).once end it "treatmentQueuesExposureWithAudienceMismatchTrueAndControlVariantOnAudienceMismatchInStrictMode" do @@ -909,10 +907,13 @@ def faraday_response(content) context.publish expect(event_handler).to have_received(:publish).once + expect(event_handler).to have_received(:publish).with(context, expected).once end - xit "treatmentCallsEventLogger" do + it "treatmentCallsEventLogger" do + event_logger.clear context = create_ready_context + expect(event_logger).to have_received(:handle_event).with(ContextEventLogger::EVENT_TYPE::READY, data).once context.treatment("exp_test_ab") context.treatment("not_found") @@ -922,13 +923,14 @@ def faraday_response(content) Exposure.new(0, "not_found", nil, 0, clock_in_millis, false, true, false, false, false, false), ] - expect(event_logger).to have_received(:handle_event).exactly(exposures.length).time + expect(event_logger).to have_received(:handle_event).with(ContextEventLogger::EVENT_TYPE::EXPOSURE, exposures[0]).once + expect(event_logger).to have_received(:handle_event).with(ContextEventLogger::EVENT_TYPE::EXPOSURE, exposures[1]).once - # verify not called again with the same exposure + event_logger.clear context.treatment("exp_test_ab") context.treatment("not_found") - expect(event_logger).to have_received(:handle_event).exactly(0).time + expect(event_logger.called).to eq(0) end it "track" do @@ -959,6 +961,7 @@ def faraday_response(content) context.publish expect(event_handler).to have_received(:publish).once + expect(event_handler).to have_received(:publish).with(context, expected).once context.close end @@ -981,8 +984,10 @@ def faraday_response(content) expect(event_handler).to have_received(:publish).exactly(0).time end - xit "publishCallsEventLogger" do + it "publishCallsEventLogger" do + event_logger.clear context = create_ready_context + expect(event_logger).to have_received(:handle_event).with(ContextEventLogger::EVENT_TYPE::READY, data).once context.track("goal1", { amount: 125, hours: 245 }) @@ -999,20 +1004,20 @@ def faraday_response(content) context.publish - expect(event_logger).to have_received(:handle_event).once + expect(event_logger).to have_received(:handle_event).with(ContextEventLogger::EVENT_TYPE::PUBLISH, expected).once + expect(event_handler).to have_received(:publish).with(context, expected).once end - xit "publishCallsEventLoggerOnError" do - context = create_ready_context + it "publishCallsEventLoggerOnError" do + context = create_context(data_future_failed) context.track("goal1", { amount: 125, hours: 245 }) allow(event_handler).to receive(:publish).and_return(failure_future) - actual = context.publish expect(actual).to eq(failure) - expect(event_logger).to have_received(:handle_event).once + expect(event_logger).to have_received(:handle_event).with(ContextEventLogger::EVENT_TYPE::ERROR, "FAILED").once end it "publish Does Not Call event handler When Failed" do @@ -1072,3 +1077,23 @@ def faraday_response(content) expect(context.experiments).to eq(experiments) end end + + + +class MockContextEventLoggerProxy < ContextEventLogger + attr_accessor :called, :events + + def initialize + @called = 0 + @events = [] + end + + def handle_event(event, data) + @called += 1 + @events << { event: event, data: data } + end + + def clear + initialize + end +end