From cb2fedd040ceb15ab149fc12b2293a923da827cb Mon Sep 17 00:00:00 2001 From: Daniel Ansari Date: Mon, 12 Dec 2022 03:01:06 +0700 Subject: [PATCH 1/7] fix expected test --- lib/context.rb | 12 ++++++------ lib/json/exposure.rb | 2 +- spec/context_spec.rb | 22 +++++++++++++++------- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/lib/context.rb b/lib/context.rb index aa33cf0..cae8de1 100644 --- a/lib/context.rb +++ b/lib/context.rb @@ -157,11 +157,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 @@ -282,11 +282,11 @@ 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 + event.attributes = @attributes unless @attributes.empty? + event.goals = achievements unless achievements.nil? return @event_handler.publish(self, event) end end @@ -365,7 +365,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 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/context_spec.rb b/spec/context_spec.rb index ca5a58b..ca7be6b 100644 --- a/spec/context_spec.rb +++ b/spec/context_spec.rb @@ -61,13 +61,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") } @@ -619,6 +619,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 +647,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,6 +676,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 xit "getVariableValueCallsEventLogger" do @@ -736,7 +739,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 +768,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 +807,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 +839,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 +862,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 +889,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,6 +914,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 xit "treatmentCallsEventLogger" do @@ -959,6 +965,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 @@ -1000,6 +1007,7 @@ def faraday_response(content) context.publish expect(event_logger).to have_received(:handle_event).once + expect(event_handler).to have_received(:publish).with(context, expected).once end xit "publishCallsEventLoggerOnError" do From 089c9fe610e8074477d1401dacfee7cf53710f6d Mon Sep 17 00:00:00 2001 From: Daniel Ansari Date: Mon, 12 Dec 2022 03:01:06 +0700 Subject: [PATCH 2/7] fix expected test --- lib/context.rb | 12 ++++++------ lib/json/exposure.rb | 2 +- spec/context_spec.rb | 22 +++++++++++++++------- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/lib/context.rb b/lib/context.rb index aa33cf0..cae8de1 100644 --- a/lib/context.rb +++ b/lib/context.rb @@ -157,11 +157,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 @@ -282,11 +282,11 @@ 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 + event.attributes = @attributes unless @attributes.empty? + event.goals = achievements unless achievements.nil? return @event_handler.publish(self, event) end end @@ -365,7 +365,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 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/context_spec.rb b/spec/context_spec.rb index ca5a58b..ca7be6b 100644 --- a/spec/context_spec.rb +++ b/spec/context_spec.rb @@ -61,13 +61,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") } @@ -619,6 +619,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 +647,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,6 +676,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 xit "getVariableValueCallsEventLogger" do @@ -736,7 +739,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 +768,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 +807,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 +839,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 +862,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 +889,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,6 +914,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 xit "treatmentCallsEventLogger" do @@ -959,6 +965,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 @@ -1000,6 +1007,7 @@ def faraday_response(content) context.publish expect(event_logger).to have_received(:handle_event).once + expect(event_handler).to have_received(:publish).with(context, expected).once end xit "publishCallsEventLoggerOnError" do From 235359f76bc90d1742e6d4dc3df39b1b5068cf8b Mon Sep 17 00:00:00 2001 From: Daniel Ansari Date: Mon, 12 Dec 2022 03:59:03 +0700 Subject: [PATCH 3/7] update readme --- README | 180 -------------------------------------- README.md | 253 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 253 insertions(+), 180 deletions(-) delete mode 100644 README create mode 100644 README.md diff --git a/README b/README deleted file mode 100644 index 37fc647..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 - -If bundler is not being used to manage dependencies, install the gem by executing: - - $ gem install absmartly - -## 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..5ab0d1b --- /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 + +If bundler is not being used to manage dependencies, install the gem by executing: + + $ gem install absmartly + +## 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 } +) +``` From 3c55f237742b4322400c4ca87a84ff1162be510b Mon Sep 17 00:00:00 2001 From: Daniel Ansari Date: Wed, 14 Dec 2022 02:16:24 +0700 Subject: [PATCH 4/7] add event logger and refactor --- lib/context.rb | 29 +++++++-- lib/context_config.rb | 10 ++++ lib/context_event_logger.rb | 16 +++-- lib/context_event_logger_callback.rb | 13 ++++ spec/context_spec.rb | 90 +++++++++++++++++----------- 5 files changed, 113 insertions(+), 45 deletions(-) create mode 100644 lib/context_event_logger_callback.rb diff --git a/lib/context.rb b/lib/context.rb index cae8de1..f4fefa3 100644 --- a/lib/context.rb +++ b/lib/context.rb @@ -11,13 +11,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 +31,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 +48,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 @@ -171,6 +172,7 @@ def queue_exposure(assignment) @pending_count += 1 @exposures.push(exposure) + log_event(ContextEventLogger::EVENT_TYPE::EXPOSURE, exposure) end end @@ -221,6 +223,7 @@ def track(goal_name, properties) @pending_count += 1 @achievements.push(achievement) + log_event(ContextEventLogger::EVENT_TYPE::GOAL, achievement) end def publish @@ -236,8 +239,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 +253,7 @@ def close flush end @closed = true + log_event(ContextEventLogger::EVENT_TYPE::CLOSE, nil) end end @@ -287,6 +293,7 @@ def flush event.exposures = exposures event.attributes = @attributes unless @attributes.empty? event.goals = achievements unless achievements.nil? + log_event(ContextEventLogger::EVENT_TYPE::PUBLISH, event) return @event_handler.publish(self, event) end end @@ -294,6 +301,7 @@ def flush @exposures = [] @achievements = [] @pending_count = 0 + return @data_failed end end @@ -471,6 +479,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 +498,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..14169a6 100644 --- a/lib/context_config.rb +++ b/lib/context_config.rb @@ -67,4 +67,14 @@ 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 + + def event_logger + @event_logger + end 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/spec/context_spec.rb b/spec/context_spec.rb index ca7be6b..55e734d 100644 --- a/spec/context_spec.rb +++ b/spec/context_spec.rb @@ -105,12 +105,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 +129,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 +139,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 +147,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 +155,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 +204,16 @@ def faraday_response(content) expect(context.failed?).to be_truthy end - xit "calls event logger when ready" do + it "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 - - 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 + context = 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 @@ -679,9 +672,10 @@ def faraday_response(content) 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) @@ -689,13 +683,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 @@ -917,8 +911,10 @@ def faraday_response(content) 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") @@ -928,13 +924,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 @@ -988,8 +985,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 }) @@ -1006,21 +1005,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 @@ -1080,3 +1078,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 From 7801922f71388471b775ac56b5b7fa6d1618795b Mon Sep 17 00:00:00 2001 From: Daniel Ansari Date: Wed, 14 Dec 2022 02:18:01 +0700 Subject: [PATCH 5/7] reformat code style --- absmartly.gemspec | 2 +- example/example.rb | 2 ++ lib/context.rb | 8 ++++---- lib/context_config.rb | 4 +--- spec/context_spec.rb | 6 +++--- 5 files changed, 11 insertions(+), 11 deletions(-) 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..0ad775b 100644 --- a/example/example.rb +++ b/example/example.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "../lib/absmartly" # config file diff --git a/lib/context.rb b/lib/context.rb index f4fefa3..46f61f8 100644 --- a/lib/context.rb +++ b/lib/context.rb @@ -294,14 +294,14 @@ def flush event.attributes = @attributes unless @attributes.empty? event.goals = achievements unless achievements.nil? log_event(ContextEventLogger::EVENT_TYPE::PUBLISH, event) - return @event_handler.publish(self, event) + @event_handler.publish(self, event) end end else @exposures = [] @achievements = [] @pending_count = 0 - return @data_failed + @data_failed end end @@ -481,13 +481,13 @@ def set_data_failed(exception) def log_event(event, data) unless @event_logger.nil? - @event_logger.handle_event(event, data); + @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); + @event_logger.handle_event(ContextEventLogger::EVENT_TYPE::ERROR, error.message) end end diff --git a/lib/context_config.rb b/lib/context_config.rb index 14169a6..cbe5c64 100644 --- a/lib/context_config.rb +++ b/lib/context_config.rb @@ -74,7 +74,5 @@ def set_event_logger(event_logger) self end - def event_logger - @event_logger - end + attr_reader :event_logger end diff --git a/spec/context_spec.rb b/spec/context_spec.rb index 55e734d..8897341 100644 --- a/spec/context_spec.rb +++ b/spec/context_spec.rb @@ -205,13 +205,13 @@ def faraday_response(content) end it "calls event logger when ready" do - context = create_ready_context + create_ready_context expect(event_logger).to have_received(:handle_event).with(ContextEventLogger::EVENT_TYPE::READY, data).once end it "callsEventLoggerWithException" do - context = create_context(data_future_failed) + create_context(data_future_failed) expect(event_logger).to have_received(:handle_event).with(ContextEventLogger::EVENT_TYPE::ERROR, "FAILED").once end @@ -1091,7 +1091,7 @@ def initialize def handle_event(event, data) @called += 1 - @events << {event: event, data: data} + @events << { event: event, data: data } end def clear From 8b2eea9b99443dc1e8494be8fe8a3865835d8c2f Mon Sep 17 00:00:00 2001 From: Daniel Ansari Date: Wed, 14 Dec 2022 02:29:49 +0700 Subject: [PATCH 6/7] remove byebug --- spec/client_spec.rb | 1 - spec/context_spec.rb | 1 - 2 files changed, 2 deletions(-) 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 8897341..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" From 4246027afded81c00079d9aee5de6d8e465e76b0 Mon Sep 17 00:00:00 2001 From: Daniel Ansari Date: Wed, 14 Dec 2022 02:39:48 +0700 Subject: [PATCH 7/7] improve example --- example/example.rb | 2 +- lib/a_b_smartly.rb | 2 +- lib/context.rb | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/example/example.rb b/example/example.rb index 0ad775b..5fd6f18 100644 --- a/example/example.rb +++ b/example/example.rb @@ -27,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 46f61f8..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"