From d2c1da57e836f435653bc06f60d0f08fe8c2b508 Mon Sep 17 00:00:00 2001
From: Mike Chu <104384559+mikechu-optimizely@users.noreply.github.com>
Date: Tue, 8 Aug 2023 17:06:35 -0400
Subject: [PATCH 01/18] [FSSDK-9555] GitHub Issue Templates (#337)
* Add devcontainer config
* Add Ruby flavor GH Issue templates
* Add gitlens to devcontainer
* Add PR #298 doc fix
* Add GitLense to devcontainer
* Testing updates to ruby CI
* Fix linting item
* Rollback CI changes
* Add suggested setup from @andrewleap-optimizely
* fix for solargraph extension
---------
Co-authored-by: Andy Leap <104936100+andrewleap-optimizely@users.noreply.github.com>
---
.devcontainer/devcontainer.json | 17 ++++
.github/ISSUE_TEMPLATE/BUG-REPORT.yml | 94 +++++++++++++++++++++++
.github/ISSUE_TEMPLATE/ENHANCEMENT.yml | 45 +++++++++++
.github/ISSUE_TEMPLATE/FEATURE-REQUEST.md | 4 +
.github/ISSUE_TEMPLATE/config.yml | 5 ++
lib/optimizely/bucketer.rb | 4 +-
spec/audience_spec.rb | 2 +-
7 files changed, 168 insertions(+), 3 deletions(-)
create mode 100644 .devcontainer/devcontainer.json
create mode 100644 .github/ISSUE_TEMPLATE/BUG-REPORT.yml
create mode 100644 .github/ISSUE_TEMPLATE/ENHANCEMENT.yml
create mode 100644 .github/ISSUE_TEMPLATE/FEATURE-REQUEST.md
create mode 100644 .github/ISSUE_TEMPLATE/config.yml
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 00000000..756e7ae0
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,17 @@
+{
+ "name": "Ruby SDK",
+
+ "image": "mcr.microsoft.com/devcontainers/ruby:1-3.2-bullseye",
+
+ "postCreateCommand": "set -e && bundle install && gem install optimizely-sdk && rake build && gem install pkg/* && gem install solargraph",
+
+ "customizations": {
+ "vscode": {
+ "extensions": [
+ "eamodio.gitlens",
+ "github.vscode-github-actions",
+ "castwide.solargraph"
+ ]
+ }
+ }
+}
diff --git a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml
new file mode 100644
index 00000000..d4b638dc
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml
@@ -0,0 +1,94 @@
+name: 🐞 Bug
+description: File a bug/issue
+title: "[BUG]
"
+labels: ["bug", "needs-triage"]
+body:
+- type: checkboxes
+ attributes:
+ label: Is there an existing issue for this?
+ description: Please search to see if an issue already exists for the bug you encountered.
+ options:
+ - label: I have searched the existing issues
+ required: true
+- type: textarea
+ attributes:
+ label: SDK Version
+ description: Version of the SDK in use?
+ validations:
+ required: true
+- type: textarea
+ attributes:
+ label: Current Behavior
+ description: A concise description of what you're experiencing.
+ validations:
+ required: true
+- type: textarea
+ attributes:
+ label: Expected Behavior
+ description: A concise description of what you expected to happen.
+ validations:
+ required: true
+- type: textarea
+ attributes:
+ label: Steps To Reproduce
+ description: Steps to reproduce the behavior.
+ placeholder: |
+ 1. In this environment...
+ 1. With this config...
+ 1. Run '...'
+ 1. See error...
+ validations:
+ required: true
+- type: textarea
+ attributes:
+ label: Ruby Version
+ description: What version of Ruby are you using?
+ validations:
+ required: false
+- type: textarea
+ attributes:
+ label: Rails
+ description: If you're using Rail, what version?
+ validations:
+ required: false
+- type: textarea
+ attributes:
+ label: Link
+ description: Link to code demonstrating the problem.
+ validations:
+ required: false
+- type: textarea
+ attributes:
+ label: Logs
+ description: Logs/stack traces related to the problem (⚠️do not include sensitive information).
+ validations:
+ required: false
+- type: dropdown
+ attributes:
+ label: Severity
+ description: What is the severity of the problem?
+ multiple: true
+ options:
+ - Blocking development
+ - Affecting users
+ - Minor issue
+ validations:
+ required: false
+- type: textarea
+ attributes:
+ label: Workaround/Solution
+ description: Do you have any workaround or solution in mind for the problem?
+ validations:
+ required: false
+- type: textarea
+ attributes:
+ label: Recent Change
+ description: Has this issue started happening after an update or experiment change?
+ validations:
+ required: false
+- type: textarea
+ attributes:
+ label: Conflicts
+ description: Are there other libraries/dependencies potentially in conflict?
+ validations:
+ required: false
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/ENHANCEMENT.yml b/.github/ISSUE_TEMPLATE/ENHANCEMENT.yml
new file mode 100644
index 00000000..42d8a302
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/ENHANCEMENT.yml
@@ -0,0 +1,45 @@
+name: ✨Enhancement
+description: Create a new ticket for a Enhancement/Tech-initiative for the benefit of the SDK which would be considered for a minor version update.
+title: "[ENHANCEMENT] "
+labels: ["enhancement"]
+body:
+ - type: textarea
+ id: description
+ attributes:
+ label: Description
+ description: Briefly describe the enhancement in a few sentences.
+ placeholder: Short description...
+ validations:
+ required: true
+ - type: textarea
+ id: benefits
+ attributes:
+ label: Benefits
+ description: How would the enhancement benefit to your product or usage?
+ placeholder: Benefits...
+ validations:
+ required: true
+ - type: textarea
+ id: detail
+ attributes:
+ label: Detail
+ description: How would you like the enhancement to work? Please provide as much detail as possible
+ placeholder: Detailed description...
+ validations:
+ required: false
+ - type: textarea
+ id: examples
+ attributes:
+ label: Examples
+ description: Are there any examples of this enhancement in other products/services? If so, please provide links or references.
+ placeholder: Links/References...
+ validations:
+ required: false
+ - type: textarea
+ id: risks
+ attributes:
+ label: Risks/Downsides
+ description: Do you think this enhancement could have any potential downsides or risks?
+ placeholder: Risks/Downsides...
+ validations:
+ required: false
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.md b/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.md
new file mode 100644
index 00000000..a061f335
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.md
@@ -0,0 +1,4 @@
+
+## Feedback requesting a new feature can be shared [here.](https://feedback.optimizely.com/)
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 00000000..d28ef3dd
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,5 @@
+blank_issues_enabled: false
+contact_links:
+ - name: 💡Feature Requests
+ url: https://feedback.optimizely.com/
+ about: Feedback requesting a new feature can be shared here.
\ No newline at end of file
diff --git a/lib/optimizely/bucketer.rb b/lib/optimizely/bucketer.rb
index ba502833..15f711cb 100644
--- a/lib/optimizely/bucketer.rb
+++ b/lib/optimizely/bucketer.rb
@@ -110,8 +110,8 @@ def find_bucket(bucketing_id, user_id, parent_id, traffic_allocations)
# parent_id - String entity ID to use for bucketing ID
# traffic_allocations - Array of traffic allocations
#
- # Returns and array of two values where first value is the entity ID corresponding to the provided bucket value
- # or nil if no match is found. The second value contains the array of reasons stating how the deicision was taken
+ # Returns an array of two values where first value is the entity ID corresponding to the provided bucket value
+ # or nil if no match is found. The second value contains the array of reasons stating how the decision was taken
decide_reasons = []
bucketing_key = format(BUCKETING_ID_TEMPLATE, bucketing_id: bucketing_id, entity_id: parent_id)
bucket_value = generate_bucket_value(bucketing_key)
diff --git a/spec/audience_spec.rb b/spec/audience_spec.rb
index 73560aff..7777e804 100644
--- a/spec/audience_spec.rb
+++ b/spec/audience_spec.rb
@@ -47,7 +47,7 @@
user_meets_audience_conditions, reasons = Optimizely::Audience.user_meets_audience_conditions?(config, experiment, user_context, spy_logger)
expect(user_meets_audience_conditions).to be true
- expect(reasons).to eq(["Audiences for experiment 'test_experiment' collectively evaluated to TRUE."])
+ expect(reasons).to eq(["Audiences for experiment 'test_experiment' collectively evaluated to TRUE."])
# Audience Ids is Empty and Audience Conditions is nil
experiment = config.experiment_key_map['test_experiment']
From 85512a248d62fed1dbca6a04ef5c6ccd576cac23 Mon Sep 17 00:00:00 2001
From: Matjaz Pirnovar
Date: Mon, 25 Sep 2023 13:47:39 -0700
Subject: [PATCH 02/18] [FSSDK-9509] Update http_project_config_manager.rb with
soft log warning about polling interval < 30s (#338)
* Update http_project_config_manager.rb
Add soft warning that polling interval under 30 s is not recommended.
* Update w single quotes
* Update http_project_config_manager.rb
* Update WARNING to WARN
* Update lib/optimizely/config_manager/http_project_config_manager.rb
Co-authored-by: Andy Leap <104936100+andrewleap-optimizely@users.noreply.github.com>
---------
Co-authored-by: Andy Leap <104936100+andrewleap-optimizely@users.noreply.github.com>
---
.../config_manager/http_project_config_manager.rb | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/lib/optimizely/config_manager/http_project_config_manager.rb b/lib/optimizely/config_manager/http_project_config_manager.rb
index 0da73c1f..91610455 100644
--- a/lib/optimizely/config_manager/http_project_config_manager.rb
+++ b/lib/optimizely/config_manager/http_project_config_manager.rb
@@ -268,6 +268,13 @@ def polling_interval(polling_interval)
return
end
+ if polling_interval < 30
+ @logger.log(
+ Logger::WARN,
+ 'Polling intervals below 30 seconds are not recommended.'
+ )
+ end
+
@polling_interval = polling_interval
end
From 9b77a5b3bb051e953d5ab6c44008bb7b684448b1 Mon Sep 17 00:00:00 2001
From: stoneman <741710+stoneman@users.noreply.github.com>
Date: Tue, 10 Oct 2023 18:10:30 +0100
Subject: [PATCH 03/18] feat: include object id/key in invalid object errors
(#301)
* feat: include object id/key in invalid object errors
- Include object `id`/`key` in errors when objects not found in datafile
- Modifies invalid object `id`/`key` log messages to make them consistent
Include object `id`/`key` in errors when an object is not found makes them available to the user or the custom error handler.
One example of why this is useful is that the `key` of an experiment could be used within a custom error handler to fetch the details of the experiment. This would indicate whether the experiment has been paused (in which case the error could be ignored) or archived (in which case the code referencing the experiment should be removed from the application).
* feat: expose object identifiers as error properties
- Expose the object identifiers as error properties so that they are easier to use in error handlers.
- Encapsulate the error messages within the error objects to enforce consistency and to simplify initialization
- Use the messages of the error objects as log messages to enforce consistency and to simplify logging
* Update lib/optimizely/exceptions.rb typo identifier
* identifier typo
* identifier typo
* identifier typo
* identifier typo
* identifier typo
---------
Co-authored-by: Matjaz Pirnovar
---
.../config/datafile_project_config.rb | 80 +++++++++++--------
lib/optimizely/exceptions.rb | 64 ++++++++++++---
spec/config/datafile_project_config_spec.rb | 6 +-
3 files changed, 105 insertions(+), 45 deletions(-)
diff --git a/lib/optimizely/config/datafile_project_config.rb b/lib/optimizely/config/datafile_project_config.rb
index d8d78975..25357133 100644
--- a/lib/optimizely/config/datafile_project_config.rb
+++ b/lib/optimizely/config/datafile_project_config.rb
@@ -223,8 +223,9 @@ def get_experiment_from_key(experiment_key)
experiment = @experiment_key_map[experiment_key]
return experiment if experiment
- @logger.log Logger::ERROR, "Experiment key '#{experiment_key}' is not in datafile."
- @error_handler.handle_error InvalidExperimentError
+ invalid_experiment_error = InvalidExperimentError.new(experiment_key: experiment_key)
+ @logger.log Logger::ERROR, invalid_experiment_error.message
+ @error_handler.handle_error invalid_experiment_error
nil
end
@@ -238,8 +239,9 @@ def get_experiment_from_id(experiment_id)
experiment = @experiment_id_map[experiment_id]
return experiment if experiment
- @logger.log Logger::ERROR, "Experiment id '#{experiment_id}' is not in datafile."
- @error_handler.handle_error InvalidExperimentError
+ invalid_experiment_error = InvalidExperimentError.new(experiment_id: experiment_id)
+ @logger.log Logger::ERROR, invalid_experiment_error.message
+ @error_handler.handle_error invalid_experiment_error
nil
end
@@ -253,8 +255,9 @@ def get_experiment_key(experiment_id)
experiment = @experiment_id_map[experiment_id]
return experiment['key'] unless experiment.nil?
- @logger.log Logger::ERROR, "Experiment id '#{experiment_id}' is not in datafile."
- @error_handler.handle_error InvalidExperimentError
+ invalid_experiment_error = InvalidExperimentError.new(experiment_id: experiment_id)
+ @logger.log Logger::ERROR, invalid_experiment_error.message
+ @error_handler.handle_error invalid_experiment_error
nil
end
@@ -268,8 +271,9 @@ def get_event_from_key(event_key)
event = @event_key_map[event_key]
return event if event
- @logger.log Logger::ERROR, "Event '#{event_key}' is not in datafile."
- @error_handler.handle_error InvalidEventError
+ invalid_event_error = InvalidEventError.new(event_key)
+ @logger.log Logger::ERROR, invalid_event_error.message
+ @error_handler.handle_error invalid_event_error
nil
end
@@ -283,8 +287,9 @@ def get_audience_from_id(audience_id)
audience = @audience_id_map[audience_id]
return audience if audience
- @logger.log Logger::ERROR, "Audience '#{audience_id}' is not in datafile."
- @error_handler.handle_error InvalidAudienceError
+ invalid_audience_error = InvalidAudienceError.new(audience_id)
+ @logger.log Logger::ERROR, invalid_audience_error.message
+ @error_handler.handle_error invalid_audience_error
nil
end
@@ -308,13 +313,15 @@ def get_variation_from_id(experiment_key, variation_id)
variation = variation_id_map[variation_id]
return variation if variation
- @logger.log Logger::ERROR, "Variation id '#{variation_id}' is not in datafile."
- @error_handler.handle_error InvalidVariationError
+ invalid_variation_error = InvalidVariationError.new(variation_id: variation_id)
+ @logger.log Logger::ERROR, invalid_variation_error.message
+ @error_handler.handle_error invalid_variation_error
return nil
end
- @logger.log Logger::ERROR, "Experiment key '#{experiment_key}' is not in datafile."
- @error_handler.handle_error InvalidExperimentError
+ invalid_experiment_error = InvalidExperimentError.new(experiment_key: experiment_key)
+ @logger.log Logger::ERROR, invalid_experiment_error.message
+ @error_handler.handle_error invalid_experiment_error
nil
end
@@ -331,13 +338,15 @@ def get_variation_from_id_by_experiment_id(experiment_id, variation_id)
variation = variation_id_map_by_experiment_id[variation_id]
return variation if variation
- @logger.log Logger::ERROR, "Variation id '#{variation_id}' is not in datafile."
- @error_handler.handle_error InvalidVariationError
+ invalid_variation_error = InvalidVariationError.new(variation_id: variation_id)
+ @logger.log Logger::ERROR, invalid_variation_error.message
+ @error_handler.handle_error invalid_variation_error
return nil
end
- @logger.log Logger::ERROR, "Experiment id '#{experiment_id}' is not in datafile."
- @error_handler.handle_error InvalidExperimentError
+ invalid_experiment_error = InvalidExperimentError.new(experiment_id: experiment_id)
+ @logger.log Logger::ERROR, invalid_experiment_error.message
+ @error_handler.handle_error invalid_experiment_error
nil
end
@@ -354,13 +363,15 @@ def get_variation_id_from_key_by_experiment_id(experiment_id, variation_key)
variation = variation_key_map[variation_key]
return variation['id'] if variation
- @logger.log Logger::ERROR, "Variation key '#{variation_key}' is not in datafile."
- @error_handler.handle_error InvalidVariationError
+ invalid_variation_error = InvalidVariationError.new(variation_key: variation_key)
+ @logger.log Logger::ERROR, invalid_variation_error.message
+ @error_handler.handle_error invalid_variation_error
return nil
end
- @logger.log Logger::ERROR, "Experiment id '#{experiment_id}' is not in datafile."
- @error_handler.handle_error InvalidExperimentError
+ invalid_experiment_error = InvalidExperimentError.new(experiment_id: experiment_id)
+ @logger.log Logger::ERROR, invalid_experiment_error.message
+ @error_handler.handle_error invalid_experiment_error
nil
end
@@ -377,13 +388,15 @@ def get_variation_id_from_key(experiment_key, variation_key)
variation = variation_key_map[variation_key]
return variation['id'] if variation
- @logger.log Logger::ERROR, "Variation key '#{variation_key}' is not in datafile."
- @error_handler.handle_error InvalidVariationError
+ invalid_variation_error = InvalidVariationError.new(variation_key: variation_key)
+ @logger.log Logger::ERROR, invalid_variation_error.message
+ @error_handler.handle_error invalid_variation_error
return nil
end
- @logger.log Logger::ERROR, "Experiment key '#{experiment_key}' is not in datafile."
- @error_handler.handle_error InvalidExperimentError
+ invalid_experiment_error = InvalidExperimentError.new(experiment_key: experiment_key)
+ @logger.log Logger::ERROR, invalid_experiment_error.message
+ @error_handler.handle_error invalid_experiment_error
nil
end
@@ -397,8 +410,9 @@ def get_whitelisted_variations(experiment_id)
experiment = @experiment_id_map[experiment_id]
return experiment['forcedVariations'] if experiment
- @logger.log Logger::ERROR, "Experiment ID '#{experiment_id}' is not in datafile."
- @error_handler.handle_error InvalidExperimentError
+ invalid_experiment_error = InvalidExperimentError.new(experiment_id: experiment_id)
+ @logger.log Logger::ERROR, invalid_experiment_error.message
+ @error_handler.handle_error invalid_experiment_error
end
def get_attribute_id(attribute_key)
@@ -420,8 +434,9 @@ def get_attribute_id(attribute_key)
end
return attribute_key if has_reserved_prefix
- @logger.log Logger::ERROR, "Attribute key '#{attribute_key}' is not in datafile."
- @error_handler.handle_error InvalidAttributeError
+ invalid_attribute_error = InvalidAttributeError.new(attribute_key)
+ @logger.log Logger::ERROR, invalid_attribute_error.message
+ @error_handler.handle_error invalid_attribute_error
nil
end
@@ -439,8 +454,9 @@ def variation_id_exists?(experiment_id, variation_id)
variation = variation_id_map[variation_id]
return true if variation
- @logger.log Logger::ERROR, "Variation ID '#{variation_id}' is not in datafile."
- @error_handler.handle_error InvalidVariationError
+ invalid_variation_error = InvalidVariationError.new(variation_id: variation_id)
+ @logger.log Logger::ERROR, invalid_variation_error.message
+ @error_handler.handle_error invalid_variation_error
end
false
diff --git a/lib/optimizely/exceptions.rb b/lib/optimizely/exceptions.rb
index 50ef62c0..5d608b2f 100644
--- a/lib/optimizely/exceptions.rb
+++ b/lib/optimizely/exceptions.rb
@@ -42,16 +42,28 @@ def initialize(msg = 'SDK key not provided/cannot be found in the datafile.')
class InvalidAudienceError < Error
# Raised when an invalid audience is provided
- def initialize(msg = 'Provided audience is not in datafile.')
- super
+ attr_reader :audience_id
+
+ def initialize(audience_id)
+ raise ArgumentError, 'audience_id must be provided' if audience_id.nil?
+
+ super("Audience id '#{audience_id}' is not in datafile.")
+
+ @audience_id = audience_id
end
end
class InvalidAttributeError < Error
# Raised when an invalid attribute is provided
- def initialize(msg = 'Provided attribute is not in datafile.')
- super
+ attr_reader :attribute_key
+
+ def initialize(attribute_key)
+ raise ArgumentError, 'attribute_key must be provided' if attribute_key.nil?
+
+ super("Attribute key '#{attribute_key}' is not in datafile.")
+
+ @attribute_key = attribute_key
end
end
@@ -74,24 +86,56 @@ def initialize(msg = 'Event tags provided are in an invalid format.')
class InvalidExperimentError < Error
# Raised when an invalid experiment key is provided
- def initialize(msg = 'Provided experiment is not in datafile.')
- super
+ attr_reader :experiment_id, :experiment_key
+
+ def initialize(experiment_id: nil, experiment_key: nil)
+ raise ArgumentError, 'Either experiment_id or experiment_key must be provided.' if experiment_id.nil? && experiment_key.nil?
+ raise ArgumentError, 'Cannot provide both experiment_id and experiment_key.' if !experiment_id.nil? && !experiment_key.nil?
+
+ if experiment_id.nil?
+ @experiment_key = experiment_key
+ identifier = "key '#{@experiment_key}'"
+ else
+ @experiment_id = experiment_id
+ identifier = "id '#{@experiment_id}'"
+ end
+
+ super("Experiment #{identifier} is not in datafile.")
end
end
class InvalidEventError < Error
# Raised when an invalid event key is provided
- def initialize(msg = 'Provided event is not in datafile.')
- super
+ attr_reader :event_key
+
+ def initialize(event_key)
+ raise ArgumentError, 'event_key must be provided.' if event_key.nil?
+
+ super("Event key '#{event_key}' is not in datafile.")
+
+ @event_key = event_key
end
end
class InvalidVariationError < Error
# Raised when an invalid variation key or ID is provided
- def initialize(msg = 'Provided variation is not in datafile.')
- super
+ attr_reader :variation_id, :variation_key
+
+ def initialize(variation_id: nil, variation_key: nil)
+ raise ArgumentError, 'Either variation_id or variation_key must be provided.' if variation_id.nil? && variation_key.nil?
+ raise ArgumentError, 'Cannot provide both variation_id and variation_key.' if !variation_id.nil? && !variation_key.nil?
+
+ if variation_id.nil?
+ identifier = "key '#{variation_key}'"
+ @variation_key = variation_key
+ else
+ identifier = "id '#{variation_id}'"
+ @variation_id = variation_id
+ end
+
+ super("Variation #{identifier} is not in datafile.")
end
end
diff --git a/spec/config/datafile_project_config_spec.rb b/spec/config/datafile_project_config_spec.rb
index 3cf2bd31..e30d07e1 100644
--- a/spec/config/datafile_project_config_spec.rb
+++ b/spec/config/datafile_project_config_spec.rb
@@ -837,14 +837,14 @@
describe 'get_event_from_key' do
it 'should log a message when provided event key is invalid' do
config.get_event_from_key('invalid_key')
- expect(spy_logger).to have_received(:log).with(Logger::ERROR, "Event 'invalid_key' is not in datafile.")
+ expect(spy_logger).to have_received(:log).with(Logger::ERROR, "Event key 'invalid_key' is not in datafile.")
end
end
describe 'get_audience_from_id' do
it 'should log a message when provided audience ID is invalid' do
config.get_audience_from_id('invalid_id')
- expect(spy_logger).to have_received(:log).with(Logger::ERROR, "Audience 'invalid_id' is not in datafile.")
+ expect(spy_logger).to have_received(:log).with(Logger::ERROR, "Audience id 'invalid_id' is not in datafile.")
end
end
@@ -948,7 +948,7 @@
it 'should log a message when there is no experiment key map for the experiment' do
config.get_whitelisted_variations('invalid_key')
expect(spy_logger).to have_received(:log).with(Logger::ERROR,
- "Experiment ID 'invalid_key' is not in datafile.")
+ "Experiment id 'invalid_key' is not in datafile.")
end
end
From a269a52022896327bdb75d6729c7ac8a457f142a Mon Sep 17 00:00:00 2001
From: Matjaz Pirnovar
Date: Thu, 9 Nov 2023 13:39:02 -0800
Subject: [PATCH 04/18] updated to ruby version (#341)
---
.github/workflows/ruby.yml | 2 +-
.rubocop.yml | 2 +-
README.md | 2 +-
optimizely-sdk.gemspec | 4 ++--
4 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml
index 1e22e74e..a3fa180a 100644
--- a/.github/workflows/ruby.yml
+++ b/.github/workflows/ruby.yml
@@ -25,7 +25,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- ruby: [ '2.7.0', '3.0.0', '3.1.0' ]
+ ruby: [ '3.0.0', '3.1.0', '3.2.0' ]
steps:
- uses: actions/checkout@v3
- name: Set up Ruby ${{ matrix.ruby }}
diff --git a/.rubocop.yml b/.rubocop.yml
index ea105dd6..1bbc4a4a 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -1,7 +1,7 @@
inherit_from: .rubocop_todo.yml
AllCops:
- TargetRubyVersion: 2.7
+ TargetRubyVersion: 3.0
Layout/SpaceInsideHashLiteralBraces:
EnforcedStyle: no_space
diff --git a/README.md b/README.md
index a10ba8d9..cbeb71ae 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@ Refer to the [Ruby SDK's developer documentation](https://docs.developers.optimi
### Requirements
-* Ruby 2.7+
+* Ruby 3.0+
### Install the SDK
diff --git a/optimizely-sdk.gemspec b/optimizely-sdk.gemspec
index c1c5b881..a34450b1 100644
--- a/optimizely-sdk.gemspec
+++ b/optimizely-sdk.gemspec
@@ -7,7 +7,7 @@ Gem::Specification.new do |spec|
spec.version = Optimizely::VERSION
spec.authors = ['Optimizely']
spec.email = ['developers@optimizely.com']
- spec.required_ruby_version = '>= 2.7'
+ spec.required_ruby_version = '>= 3.0'
spec.summary = "Ruby SDK for Optimizely's testing framework"
spec.description = 'A Ruby SDK for use with Optimizely Feature Experimentation, Optimizely Full Stack (legacy), and Optimizely Rollouts'
@@ -24,6 +24,6 @@ Gem::Specification.new do |spec|
spec.add_development_dependency 'rubocop'
spec.add_development_dependency 'webmock'
- spec.add_runtime_dependency 'json-schema', '~> 2.6'
+ spec.add_runtime_dependency 'json-schema', '>= 2.6'
spec.add_runtime_dependency 'murmurhash3', '~> 0.1'
end
From 9487c0ac25c3e7e3f860a833023656f5b20218ff Mon Sep 17 00:00:00 2001
From: Andy Leap <104936100+andrewleap-optimizely@users.noreply.github.com>
Date: Tue, 14 Nov 2023 09:18:48 -0500
Subject: [PATCH 05/18] remove config manager stop restriction (#340)
---
lib/optimizely/config_manager/http_project_config_manager.rb | 5 -----
1 file changed, 5 deletions(-)
diff --git a/lib/optimizely/config_manager/http_project_config_manager.rb b/lib/optimizely/config_manager/http_project_config_manager.rb
index 91610455..f5f33cbf 100644
--- a/lib/optimizely/config_manager/http_project_config_manager.rb
+++ b/lib/optimizely/config_manager/http_project_config_manager.rb
@@ -102,11 +102,6 @@ def ready?
end
def start!
- if @stopped
- @logger.log(Logger::WARN, 'Not starting. Already stopped.')
- return
- end
-
@async_scheduler.start!
@stopped = false
end
From ca43f1e1fd5000218f0d2693c245605fe2ba59e6 Mon Sep 17 00:00:00 2001
From: Andy Leap <104936100+andrewleap-optimizely@users.noreply.github.com>
Date: Fri, 8 Dec 2023 11:16:02 -0500
Subject: [PATCH 06/18] [FSSDK-9382] switch client init args from positional to
keyword (#342)
---
README.md | 2 +-
lib/optimizely.rb | 28 ++--
lib/optimizely/audience.rb | 4 +-
lib/optimizely/event/event_factory.rb | 4 +-
lib/optimizely/event_builder.rb | 4 +-
lib/optimizely/helpers/validator.rb | 12 +-
lib/optimizely/optimizely_factory.rb | 30 ++--
spec/audience_spec.rb | 4 +-
spec/condition_tree_evaluator_spec.rb | 30 ++--
spec/decision_service_spec.rb | 10 +-
spec/notification_center_registry_spec.rb | 6 +-
spec/optimizely_config_spec.rb | 10 +-
spec/optimizely_user_context_spec.rb | 8 +-
spec/project_spec.rb | 188 +++++++++++-----------
spec/user_condition_evaluator_spec.rb | 4 +-
15 files changed, 170 insertions(+), 174 deletions(-)
diff --git a/README.md b/README.md
index cbeb71ae..a9899291 100644
--- a/README.md
+++ b/README.md
@@ -41,7 +41,7 @@ You can initialize the Optimizely instance in two ways: directly with a datafile
Initialize Optimizely with a datafile. This datafile will be used as ProjectConfig throughout the life of the Optimizely instance.
```ruby
- optimizely_instance = Optimizely::Project.new(datafile)
+ optimizely_instance = Optimizely::Project.new(datafile: datafile)
```
#### Initialization by OptimizelyFactory
diff --git a/lib/optimizely.rb b/lib/optimizely.rb
index 93f4fc3c..da6cbbf5 100644
--- a/lib/optimizely.rb
+++ b/lib/optimizely.rb
@@ -70,20 +70,20 @@ class Project
# @param event_processor_options: Optional hash of options to be passed to the default batch event processor.
# @param settings: Optional instance of OptimizelySdkSettings for sdk configuration.
- def initialize( # rubocop:disable Metrics/ParameterLists
- datafile = nil,
- event_dispatcher = nil,
- logger = nil,
- error_handler = nil,
- skip_json_validation = false, # rubocop:disable Style/OptionalBooleanParameter
- user_profile_service = nil,
- sdk_key = nil,
- config_manager = nil,
- notification_center = nil,
- event_processor = nil,
- default_decide_options = [],
- event_processor_options = {},
- settings = nil
+ def initialize(
+ datafile: nil,
+ event_dispatcher: nil,
+ logger: nil,
+ error_handler: nil,
+ skip_json_validation: false,
+ user_profile_service: nil,
+ sdk_key: nil,
+ config_manager: nil,
+ notification_center: nil,
+ event_processor: nil,
+ default_decide_options: [],
+ event_processor_options: {},
+ settings: nil
)
@logger = logger || NoOpLogger.new
@error_handler = error_handler || NoOpErrorHandler.new
diff --git a/lib/optimizely/audience.rb b/lib/optimizely/audience.rb
index 4c57261a..3e919ad8 100644
--- a/lib/optimizely/audience.rb
+++ b/lib/optimizely/audience.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
#
-# Copyright 2016-2017, 2019-2020, Optimizely and contributors
+# Copyright 2016-2017, 2019-2020, 2023, Optimizely and contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -59,7 +59,7 @@ def user_meets_audience_conditions?(config, experiment, user_context, logger, lo
user_condition_evaluator = UserConditionEvaluator.new(user_context, logger)
evaluate_user_conditions = lambda do |condition|
- return user_condition_evaluator.evaluate(condition)
+ user_condition_evaluator.evaluate(condition)
end
evaluate_audience = lambda do |audience_id|
diff --git a/lib/optimizely/event/event_factory.rb b/lib/optimizely/event/event_factory.rb
index d8d5062e..9ac8a937 100644
--- a/lib/optimizely/event/event_factory.rb
+++ b/lib/optimizely/event/event_factory.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
#
-# Copyright 2019-2020, 2022, Optimizely and contributors
+# Copyright 2019-2020, 2022-2023, Optimizely and contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -72,7 +72,7 @@ def create_log_event(user_events, logger)
def build_attribute_list(user_attributes, project_config)
visitor_attributes = []
- user_attributes&.keys&.each do |attribute_key|
+ user_attributes&.each_key do |attribute_key|
# Omit attribute values that are not supported by the log endpoint.
attribute_value = user_attributes[attribute_key]
next unless Helpers::Validator.attribute_valid?(attribute_key, attribute_value)
diff --git a/lib/optimizely/event_builder.rb b/lib/optimizely/event_builder.rb
index 9b87413e..7b1ba51d 100644
--- a/lib/optimizely/event_builder.rb
+++ b/lib/optimizely/event_builder.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
#
-# Copyright 2016-2019, 2022, Optimizely and contributors
+# Copyright 2016-2019, 2022-2023, Optimizely and contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -62,7 +62,7 @@ def get_common_params(project_config, user_id, attributes)
visitor_attributes = []
- attributes&.keys&.each do |attribute_key|
+ attributes&.each_key do |attribute_key|
# Omit attribute values that are not supported by the log endpoint.
attribute_value = attributes[attribute_key]
if Helpers::Validator.attribute_valid?(attribute_key, attribute_value)
diff --git a/lib/optimizely/helpers/validator.rb b/lib/optimizely/helpers/validator.rb
index 3ae2350a..d3baa447 100644
--- a/lib/optimizely/helpers/validator.rb
+++ b/lib/optimizely/helpers/validator.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
#
-# Copyright 2016-2019, 2022, Optimizely and contributors
+# Copyright 2016-2019, 2022-2023, Optimizely and contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -190,14 +190,13 @@ def segments_cache_valid?(segments_cache)
# segments_cache - custom cache to be validated.
#
# Returns boolean depending on whether cache has required methods.
- (
- segments_cache.respond_to?(:reset) &&
+
+ segments_cache.respond_to?(:reset) &&
segments_cache.method(:reset)&.parameters&.empty? &&
segments_cache.respond_to?(:lookup) &&
segments_cache.method(:lookup)&.parameters&.length&.positive? &&
segments_cache.respond_to?(:save) &&
segments_cache.method(:save)&.parameters&.length&.positive?
- )
end
def segment_manager_valid?(segment_manager)
@@ -206,13 +205,12 @@ def segment_manager_valid?(segment_manager)
# segment_manager - custom manager to be validated.
#
# Returns boolean depending on whether manager has required methods.
- (
- segment_manager.respond_to?(:odp_config) &&
+
+ segment_manager.respond_to?(:odp_config) &&
segment_manager.respond_to?(:reset) &&
segment_manager.method(:reset)&.parameters&.empty? &&
segment_manager.respond_to?(:fetch_qualified_segments) &&
(segment_manager.method(:fetch_qualified_segments)&.parameters&.length || 0) >= 3
- )
end
def event_manager_valid?(event_manager)
diff --git a/lib/optimizely/optimizely_factory.rb b/lib/optimizely/optimizely_factory.rb
index b6734872..04c7ecdd 100644
--- a/lib/optimizely/optimizely_factory.rb
+++ b/lib/optimizely/optimizely_factory.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
#
-# Copyright 2019, 2022, Optimizely and contributors
+# Copyright 2019, 2022-2023, Optimizely and contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -103,7 +103,7 @@ def self.default_instance(sdk_key, datafile = nil)
)
Optimizely::Project.new(
- datafile, nil, logger, error_handler, nil, nil, sdk_key, config_manager, notification_center
+ datafile: datafile, logger: logger, error_handler: error_handler, sdk_key: sdk_key, config_manager: config_manager, notification_center: notification_center
)
end
@@ -111,7 +111,7 @@ def self.default_instance(sdk_key, datafile = nil)
#
# @param config_manager - Required ConfigManagerInterface Responds to 'config' method.
def self.default_instance_with_config_manager(config_manager)
- Optimizely::Project.new(nil, nil, nil, nil, nil, nil, nil, config_manager)
+ Optimizely::Project.new(config_manager: config_manager)
end
# Returns a new optimizely instance.
@@ -167,19 +167,17 @@ def self.custom_instance( # rubocop:disable Metrics/ParameterLists
)
Optimizely::Project.new(
- datafile,
- event_dispatcher,
- logger,
- error_handler,
- skip_json_validation,
- user_profile_service,
- sdk_key,
- config_manager,
- notification_center,
- event_processor,
- [],
- {},
- settings
+ datafile: datafile,
+ event_dispatcher: event_dispatcher,
+ logger: logger,
+ error_handler: error_handler,
+ skip_json_validation: skip_json_validation,
+ user_profile_service: user_profile_service,
+ sdk_key: sdk_key,
+ config_manager: config_manager,
+ notification_center: notification_center,
+ event_processor: event_processor,
+ settings: settings
)
end
end
diff --git a/spec/audience_spec.rb b/spec/audience_spec.rb
index 7777e804..eb997f0e 100644
--- a/spec/audience_spec.rb
+++ b/spec/audience_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
#
-# Copyright 2016-2017, 2019-2020, 2022, Optimizely and contributors
+# Copyright 2016-2017, 2019-2020, 2022-2023, Optimizely and contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -25,7 +25,7 @@
let(:config) { Optimizely::DatafileProjectConfig.new(config_body_JSON, spy_logger, error_handler) }
let(:typed_audience_config) { Optimizely::DatafileProjectConfig.new(config_typed_audience_JSON, spy_logger, error_handler) }
let(:integration_config) { Optimizely::DatafileProjectConfig.new(config_integration_JSON, spy_logger, error_handler) }
- let(:project_instance) { Optimizely::Project.new(config_body_JSON, nil, spy_logger, error_handler) }
+ let(:project_instance) { Optimizely::Project.new(datafile: config_body_JSON, logger: spy_logger, error_handler: error_handler) }
let(:user_context) { project_instance.create_user_context('some-user', {}) }
after(:example) { project_instance.close }
diff --git a/spec/condition_tree_evaluator_spec.rb b/spec/condition_tree_evaluator_spec.rb
index 68e99844..28dda143 100644
--- a/spec/condition_tree_evaluator_spec.rb
+++ b/spec/condition_tree_evaluator_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
#
-# Copyright 2019, Optimizely and contributors
+# Copyright 2019, 2023, Optimizely and contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -27,19 +27,19 @@
describe 'evaluate' do
it 'should return true for a leaf condition when the leaf condition evaluator returns true' do
- leaf_callback = ->(_condition) { return true }
+ leaf_callback = ->(_condition) { true }
expect(Optimizely::ConditionTreeEvaluator.evaluate(@browser_condition, leaf_callback)).to be true
end
it 'should return false for a leaf condition when the leaf condition evaluator returns false' do
- leaf_callback = ->(_condition) { return false }
+ leaf_callback = ->(_condition) { false }
expect(Optimizely::ConditionTreeEvaluator.evaluate(@browser_condition, leaf_callback)).to be false
end
end
describe 'and evaluation' do
it 'should return true when ALL conditions evaluate to true' do
- leaf_callback = ->(_condition) { return true }
+ leaf_callback = ->(_condition) { true }
expect(Optimizely::ConditionTreeEvaluator.evaluate(['and', @browser_condition, @device_condition], leaf_callback)).to be true
end
@@ -51,7 +51,7 @@
describe 'nil handling' do
it 'should return nil when all operands evaluate to nil' do
- leaf_callback = ->(_condition) { return nil }
+ leaf_callback = ->(_condition) { nil }
expect(Optimizely::ConditionTreeEvaluator.evaluate(['and', @browser_condition, @device_condition], leaf_callback)).to eq(nil)
end
@@ -83,7 +83,7 @@
describe 'or evaluation' do
it 'should return false if all conditions evaluate to false' do
- leaf_callback = ->(_condition) { return false }
+ leaf_callback = ->(_condition) { false }
expect(Optimizely::ConditionTreeEvaluator.evaluate(['or', @browser_condition, @device_condition], leaf_callback)).to be false
end
@@ -95,7 +95,7 @@
describe 'nil handling' do
it 'should return nil when all operands evaluate to nil' do
- leaf_callback = ->(_condition) { return nil }
+ leaf_callback = ->(_condition) { nil }
expect(Optimizely::ConditionTreeEvaluator.evaluate(['or', @browser_condition, @device_condition], leaf_callback)).to eq(nil)
end
@@ -127,34 +127,34 @@
describe 'not evaluation' do
it 'should return true if the condition evaluates to false' do
- leaf_callback = ->(_condition) { return false }
+ leaf_callback = ->(_condition) { false }
expect(Optimizely::ConditionTreeEvaluator.evaluate(['not', @browser_condition], leaf_callback)).to be true
end
it 'should return false if the condition evaluates to true' do
- leaf_callback = ->(_condition) { return true }
+ leaf_callback = ->(_condition) { true }
expect(Optimizely::ConditionTreeEvaluator.evaluate(['not', @browser_condition], leaf_callback)).to be false
end
it 'should return the result of negating the first condition, and ignore any additional conditions' do
- leaf_callback = ->(id) { return id == '1' }
+ leaf_callback = ->(id) { id == '1' }
expect(Optimizely::ConditionTreeEvaluator.evaluate(%w[not 1 2 1], leaf_callback)).to be false
- leaf_callback2 = ->(id) { return id == '2' }
+ leaf_callback2 = ->(id) { id == '2' }
expect(Optimizely::ConditionTreeEvaluator.evaluate(%w[not 1 2 1], leaf_callback2)).to be true
- leaf_callback3 = ->(id) { return id == '1' ? nil : id == '3' }
+ leaf_callback3 = ->(id) { id == '1' ? nil : id == '3' }
expect(Optimizely::ConditionTreeEvaluator.evaluate(%w[not 1 2 3], leaf_callback3)).to eq(nil)
end
describe 'nil handling' do
it 'should return nil when operand evaluates to nil' do
- leaf_callback = ->(_condition) { return nil }
+ leaf_callback = ->(_condition) { nil }
expect(Optimizely::ConditionTreeEvaluator.evaluate(['not', @browser_condition, @device_condition], leaf_callback)).to eq(nil)
end
it 'should return nil when there are no operands' do
- leaf_callback = ->(_condition) { return nil }
+ leaf_callback = ->(_condition) { nil }
expect(Optimizely::ConditionTreeEvaluator.evaluate(['not'], leaf_callback)).to eq(nil)
end
end
@@ -166,7 +166,7 @@
allow(leaf_callback).to receive(:call).and_return(true, false)
expect(Optimizely::ConditionTreeEvaluator.evaluate([@browser_condition, @device_condition], leaf_callback)).to be true
- leaf_callback = ->(_condition) { return false }
+ leaf_callback = ->(_condition) { false }
allow(leaf_callback).to receive(:call).and_return(false, true)
expect(Optimizely::ConditionTreeEvaluator.evaluate([@browser_condition, @device_condition], leaf_callback)).to be true
end
diff --git a/spec/decision_service_spec.rb b/spec/decision_service_spec.rb
index 7646c032..10f58792 100644
--- a/spec/decision_service_spec.rb
+++ b/spec/decision_service_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
#
-# Copyright 2017-2020, Optimizely and contributors
+# Copyright 2017-2020, 2023, Optimizely and contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -28,7 +28,7 @@
let(:spy_user_profile_service) { spy('user_profile_service') }
let(:config) { Optimizely::DatafileProjectConfig.new(config_body_JSON, spy_logger, error_handler) }
let(:decision_service) { Optimizely::DecisionService.new(spy_logger, spy_user_profile_service) }
- let(:project_instance) { Optimizely::Project.new(config_body_JSON, nil, spy_logger, error_handler) }
+ let(:project_instance) { Optimizely::Project.new(datafile: config_body_JSON, logger: spy_logger, error_handler: error_handler) }
let(:user_context) { project_instance.create_user_context('some-user', {}) }
after(:example) { project_instance.close }
@@ -497,7 +497,7 @@
describe '#get_variation_for_feature_experiment' do
config_body_json = OptimizelySpec::VALID_CONFIG_BODY_JSON
- project_instance = Optimizely::Project.new(config_body_json, nil, nil, nil)
+ project_instance = Optimizely::Project.new(datafile: config_body_json)
user_context = project_instance.create_user_context('user_1', {})
describe 'when the feature flag\'s experiment ids array is empty' do
@@ -619,7 +619,7 @@
describe '#get_variation_for_feature_rollout' do
config_body_json = OptimizelySpec::VALID_CONFIG_BODY_JSON
- project_instance = Optimizely::Project.new(config_body_json, nil, nil, nil)
+ project_instance = Optimizely::Project.new(datafile: config_body_json)
user_context = project_instance.create_user_context('user_1', {})
user_id = 'user_1'
@@ -816,7 +816,7 @@
describe '#get_variation_for_feature' do
config_body_json = OptimizelySpec::VALID_CONFIG_BODY_JSON
- project_instance = Optimizely::Project.new(config_body_json, nil, nil, nil)
+ project_instance = Optimizely::Project.new(datafile: config_body_json)
user_context = project_instance.create_user_context('user_1', {})
describe 'when the user is bucketed into the feature experiment' do
diff --git a/spec/notification_center_registry_spec.rb b/spec/notification_center_registry_spec.rb
index ab783ef5..2a4521c7 100644
--- a/spec/notification_center_registry_spec.rb
+++ b/spec/notification_center_registry_spec.rb
@@ -42,7 +42,7 @@
stub_request(:get, "https://cdn.optimizely.com/datafiles/#{sdk_key}.json")
.to_return(status: 200, body: config_body_JSON)
- project = Optimizely::Project.new(nil, nil, spy_logger, nil, false, nil, sdk_key)
+ project = Optimizely::Project.new(logger: spy_logger, sdk_key: sdk_key)
notification_center = Optimizely::NotificationCenterRegistry.get_notification_center(sdk_key, spy_logger)
expect(notification_center).to be_a Optimizely::NotificationCenter
@@ -60,7 +60,7 @@
.to_return(status: 200, body: config_body_JSON)
notification_center = Optimizely::NotificationCenterRegistry.get_notification_center(sdk_key, spy_logger)
- project = Optimizely::Project.new(nil, nil, spy_logger, nil, false, nil, sdk_key)
+ project = Optimizely::Project.new(logger: spy_logger, sdk_key: sdk_key)
expect(notification_center).to eq(Optimizely::NotificationCenterRegistry.get_notification_center(sdk_key, spy_logger))
expect(spy_logger).not_to have_received(:log).with(Logger::ERROR, anything)
@@ -78,7 +78,7 @@
notification_center = Optimizely::NotificationCenterRegistry.get_notification_center(sdk_key, spy_logger)
expect(notification_center).to receive(:send_notifications).once
- project = Optimizely::Project.new(nil, nil, spy_logger, nil, false, nil, sdk_key)
+ project = Optimizely::Project.new(logger: spy_logger, sdk_key: sdk_key)
project.config_manager.config
Optimizely::NotificationCenterRegistry.remove_notification_center(sdk_key)
diff --git a/spec/optimizely_config_spec.rb b/spec/optimizely_config_spec.rb
index 8d364e1d..cfac6ba7 100644
--- a/spec/optimizely_config_spec.rb
+++ b/spec/optimizely_config_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
#
-# Copyright 2019-2021, Optimizely and contributors
+# Copyright 2019-2021, 2023, Optimizely and contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -26,16 +26,16 @@
let(:error_handler) { Optimizely::NoOpErrorHandler.new }
let(:spy_logger) { spy('logger') }
let(:project_config) { Optimizely::DatafileProjectConfig.new(config_body_JSON, spy_logger, error_handler) }
- let(:project_instance) { Optimizely::Project.new(config_body_JSON, nil, spy_logger, error_handler) }
+ let(:project_instance) { Optimizely::Project.new(datafile: config_body_JSON, logger: spy_logger, error_handler: error_handler) }
let(:optimizely_config) { project_instance.get_optimizely_config }
let(:project_config_sim_keys) { Optimizely::DatafileProjectConfig.new(similar_exp_keys_JSON, spy_logger, error_handler) }
- let(:project_instance_sim_keys) { Optimizely::Project.new(similar_exp_keys_JSON, nil, spy_logger, error_handler) }
+ let(:project_instance_sim_keys) { Optimizely::Project.new(datafile: similar_exp_keys_JSON, logger: spy_logger, error_handler: error_handler) }
let(:optimizely_config_sim_keys) { project_instance_sim_keys.get_optimizely_config }
let(:project_config_typed_audiences) { Optimizely::DatafileProjectConfig.new(typed_audiences_JSON, spy_logger, error_handler) }
- let(:project_instance_typed_audiences) { Optimizely::Project.new(typed_audiences_JSON, nil, spy_logger, error_handler) }
+ let(:project_instance_typed_audiences) { Optimizely::Project.new(datafile: typed_audiences_JSON, logger: spy_logger, error_handler: error_handler) }
let(:optimizely_config_typed_audiences) { project_instance_typed_audiences.get_optimizely_config }
let(:project_config_similar_rule_keys) { Optimizely::DatafileProjectConfig.new(similar_rule_key_JSON, spy_logger, error_handler) }
- let(:project_instance_similar_rule_keys) { Optimizely::Project.new(similar_rule_key_JSON, nil, spy_logger, error_handler) }
+ let(:project_instance_similar_rule_keys) { Optimizely::Project.new(datafile: similar_rule_key_JSON, logger: spy_logger, error_handler: error_handler) }
let(:optimizely_config_similar_rule_keys) { project_instance_similar_rule_keys.get_optimizely_config }
after(:example) do
project_instance.close
diff --git a/spec/optimizely_user_context_spec.rb b/spec/optimizely_user_context_spec.rb
index 6a99c57b..c968c336 100644
--- a/spec/optimizely_user_context_spec.rb
+++ b/spec/optimizely_user_context_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
#
-# Copyright 2020, 2022, Optimizely and contributors
+# Copyright 2020, 2022-2023, Optimizely and contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -27,9 +27,9 @@
let(:integration_JSON) { OptimizelySpec::CONFIG_DICT_WITH_INTEGRATIONS_JSON }
let(:error_handler) { Optimizely::RaiseErrorHandler.new }
let(:spy_logger) { spy('logger') }
- let(:project_instance) { Optimizely::Project.new(config_body_JSON, nil, spy_logger, error_handler) }
- let(:forced_decision_project_instance) { Optimizely::Project.new(forced_decision_JSON, nil, spy_logger, error_handler, false, nil, nil, nil, nil, nil, [], {batch_size: 1}) }
- let(:integration_project_instance) { Optimizely::Project.new(integration_JSON, nil, spy_logger, error_handler) }
+ let(:project_instance) { Optimizely::Project.new(datafile: config_body_JSON, logger: spy_logger, error_handler: error_handler) }
+ let(:forced_decision_project_instance) { Optimizely::Project.new(datafile: forced_decision_JSON, logger: spy_logger, error_handler: error_handler, event_processor_options: {batch_size: 1}) }
+ let(:integration_project_instance) { Optimizely::Project.new(datafile: integration_JSON, logger: spy_logger, error_handler: error_handler) }
let(:impression_log_url) { 'https://logx.optimizely.com/v1/events' }
let(:good_response_data) do
{
diff --git a/spec/project_spec.rb b/spec/project_spec.rb
index d00f93c1..2c1aeaca 100644
--- a/spec/project_spec.rb
+++ b/spec/project_spec.rb
@@ -48,7 +48,7 @@
let(:version) { Optimizely::VERSION }
let(:impression_log_url) { 'https://logx.optimizely.com/v1/events' }
let(:conversion_log_url) { 'https://logx.optimizely.com/v1/events' }
- let(:project_instance) { Optimizely::Project.new(config_body_JSON, nil, spy_logger, error_handler, false, nil, nil, nil, nil, nil, [], {batch_size: 1}) }
+ let(:project_instance) { Optimizely::Project.new(datafile: config_body_JSON, logger: spy_logger, error_handler: error_handler, event_processor_options: {batch_size: 1}) }
let(:project_config) { project_instance.config_manager.config }
let(:time_now) { Time.now }
let(:post_headers) { {'Content-Type' => 'application/json'} }
@@ -71,7 +71,7 @@ def log(_level, log_message)
end
logger = CustomLogger.new
- instance_with_logger = Optimizely::Project.new(config_body_JSON, nil, logger)
+ instance_with_logger = Optimizely::Project.new(datafile: config_body_JSON, logger: logger)
expect(instance_with_logger.logger.log(Logger::INFO, 'test_message')).to eq('test_message')
instance_with_logger.close
end
@@ -84,19 +84,19 @@ def handle_error(error)
end
error_handler = CustomErrorHandler.new
- instance_with_error_handler = Optimizely::Project.new(config_body_JSON, nil, nil, error_handler)
+ instance_with_error_handler = Optimizely::Project.new(datafile: config_body_JSON, error_handler: error_handler)
expect(instance_with_error_handler.error_handler.handle_error('test_message')).to eq('test_message')
instance_with_error_handler.close
end
it 'should log an error when datafile is null' do
expect(spy_logger).to receive(:log).once.with(Logger::ERROR, 'Provided datafile is in an invalid format.')
- Optimizely::Project.new(nil, nil, spy_logger).close
+ Optimizely::Project.new(logger: spy_logger).close
end
it 'should log an error when datafile is empty' do
expect(spy_logger).to receive(:log).once.with(Logger::ERROR, 'Provided datafile is in an invalid format.')
- Optimizely::Project.new('', nil, spy_logger).close
+ Optimizely::Project.new(datafile: '', logger: spy_logger).close
end
it 'should log an error when given a datafile that does not conform to the schema' do
@@ -104,7 +104,7 @@ def handle_error(error)
allow(spy_logger).to receive(:log).with(Logger::DEBUG, anything)
expect(spy_logger).to receive(:log).once.with(Logger::ERROR, 'Provided datafile is in an invalid format.')
expect(spy_logger).to receive(:log).once.with(Logger::ERROR, 'SDK key not provided/cannot be found in the datafile. ODP may not work properly without it.')
- Optimizely::Project.new('{"foo": "bar"}', nil, spy_logger).close
+ Optimizely::Project.new(datafile: '{"foo": "bar"}', logger: spy_logger).close
end
it 'should log an error when given an invalid logger' do
@@ -114,7 +114,7 @@ def handle_error(error)
expect(spy_logger).to receive(:log).once.with(Logger::ERROR, 'Provided logger is in an invalid format.')
class InvalidLogger; end # rubocop:disable Lint/ConstantDefinitionInBlock
- Optimizely::Project.new(config_body_JSON, nil, InvalidLogger.new).close
+ Optimizely::Project.new(datafile: config_body_JSON, logger: InvalidLogger.new).close
end
it 'should log an error when given an invalid event_dispatcher' do
@@ -123,7 +123,7 @@ class InvalidLogger; end # rubocop:disable Lint/ConstantDefinitionInBlock
expect_any_instance_of(Optimizely::SimpleLogger).to receive(:log).once.with(Logger::ERROR, 'Provided event_dispatcher is in an invalid format.')
class InvalidEventDispatcher; end # rubocop:disable Lint/ConstantDefinitionInBlock
- Optimizely::Project.new(config_body_JSON, InvalidEventDispatcher.new).close
+ Optimizely::Project.new(datafile: config_body_JSON, event_dispatcher: InvalidEventDispatcher.new).close
end
it 'should log an error when given an invalid error_handler' do
@@ -132,14 +132,14 @@ class InvalidEventDispatcher; end # rubocop:disable Lint/ConstantDefinitionInBlo
expect_any_instance_of(Optimizely::SimpleLogger).to receive(:log).once.with(Logger::ERROR, 'Provided error_handler is in an invalid format.')
class InvalidErrorHandler; end # rubocop:disable Lint/ConstantDefinitionInBlock
- Optimizely::Project.new(config_body_JSON, nil, nil, InvalidErrorHandler.new).close
+ Optimizely::Project.new(datafile: config_body_JSON, error_handler: InvalidErrorHandler.new).close
end
it 'should not validate the JSON schema of the datafile when skip_json_validation is true' do
project_instance.close
expect(Optimizely::Helpers::Validator).not_to receive(:datafile_valid?)
- Optimizely::Project.new(config_body_JSON, nil, nil, nil, true).close
+ Optimizely::Project.new(datafile: config_body_JSON, skip_json_validation: true).close
end
it 'should be invalid when datafile contains integrations missing key' do
@@ -152,7 +152,7 @@ class InvalidErrorHandler; end # rubocop:disable Lint/ConstantDefinitionInBlock
config['integrations'][0].delete('key')
integrations_json = JSON.dump(config)
- Optimizely::Project.new(integrations_json, nil, spy_logger)
+ Optimizely::Project.new(datafile: integrations_json, logger: spy_logger)
end
it 'should be valid when datafile contains integrations with only key' do
@@ -161,7 +161,7 @@ class InvalidErrorHandler; end # rubocop:disable Lint/ConstantDefinitionInBlock
config['integrations'].push('key' => '123')
integrations_json = JSON.dump(config)
- project_instance = Optimizely::Project.new(integrations_json)
+ project_instance = Optimizely::Project.new(datafile: integrations_json)
expect(project_instance.is_valid).to be true
end
@@ -171,7 +171,7 @@ class InvalidErrorHandler; end # rubocop:disable Lint/ConstantDefinitionInBlock
config['integrations'].push('key' => 'future', 'any-key-1' => 1, 'any-key-2' => 'any-value-2')
integrations_json = JSON.dump(config)
- project_instance = Optimizely::Project.new(integrations_json)
+ project_instance = Optimizely::Project.new(datafile: integrations_json)
expect(project_instance.is_valid).to be true
end
@@ -179,20 +179,20 @@ class InvalidErrorHandler; end # rubocop:disable Lint/ConstantDefinitionInBlock
expect(spy_logger).to receive(:log).once.with(Logger::ERROR, 'Provided datafile is in an invalid format.')
expect_any_instance_of(Optimizely::RaiseErrorHandler).to receive(:handle_error).once.with(Optimizely::InvalidInputError)
- Optimizely::Project.new('this is not JSON', nil, spy_logger, Optimizely::RaiseErrorHandler.new, true)
+ Optimizely::Project.new(datafile: 'this is not JSON', logger: spy_logger, error_handler: Optimizely::RaiseErrorHandler.new, skip_json_validation: true)
end
it 'should log an error when provided an invalid JSON datafile and skip_json_validation is true' do
expect(spy_logger).to receive(:log).once.with(Logger::ERROR, 'Provided datafile is in an invalid format.')
- Optimizely::Project.new('{"version": "2", "foo": "bar"}', nil, spy_logger, nil, true)
+ Optimizely::Project.new(datafile: '{"version": "2", "foo": "bar"}', logger: spy_logger, skip_json_validation: true)
end
it 'should log and raise an error when provided a datafile of unsupported version' do
config_body_invalid_json = JSON.parse(config_body_invalid_JSON)
expect(spy_logger).to receive(:log).once.with(Logger::ERROR, "This version of the Ruby SDK does not support the given datafile version: #{config_body_invalid_json['version']}.")
- expect { Optimizely::Project.new(config_body_invalid_JSON, nil, spy_logger, Optimizely::RaiseErrorHandler.new, true) }.to raise_error(Optimizely::InvalidDatafileVersionError, 'This version of the Ruby SDK does not support the given datafile version: 5.')
+ expect { Optimizely::Project.new(datafile: config_body_invalid_JSON, logger: spy_logger, error_handler: Optimizely::RaiseErrorHandler.new, skip_json_validation: true) }.to raise_error(Optimizely::InvalidDatafileVersionError, 'This version of the Ruby SDK does not support the given datafile version: 5.')
end
end
@@ -225,7 +225,7 @@ class InvalidErrorHandler; end # rubocop:disable Lint/ConstantDefinitionInBlock
end
it 'should send identify event when called with odp enabled' do
- project = Optimizely::Project.new(config_body_integrations_JSON, nil, spy_logger)
+ project = Optimizely::Project.new(datafile: config_body_integrations_JSON, logger: spy_logger)
expect(project.odp_manager).to receive(:identify_user).with({user_id: 'tester'})
project.create_user_context('tester')
@@ -359,7 +359,7 @@ class InvalidErrorHandler; end # rubocop:disable Lint/ConstantDefinitionInBlock
describe '.typed audiences' do
before(:example) do
- @project_typed_audience_instance = Optimizely::Project.new(JSON.dump(OptimizelySpec::CONFIG_DICT_WITH_TYPED_AUDIENCES), nil, spy_logger, error_handler, false, nil, nil, nil, nil, nil, [], {batch_size: 1})
+ @project_typed_audience_instance = Optimizely::Project.new(datafile: JSON.dump(OptimizelySpec::CONFIG_DICT_WITH_TYPED_AUDIENCES), logger: spy_logger, error_handler: error_handler, event_processor_options: {batch_size: 1})
@project_config = @project_typed_audience_instance.config_manager.config
@expected_activate_params = {
account_id: '4879520872',
@@ -900,7 +900,7 @@ def callback(_args); end
end
it 'should log an error when called with an invalid Project object' do
- invalid_project = Optimizely::Project.new('invalid', nil, spy_logger)
+ invalid_project = Optimizely::Project.new(datafile: 'invalid', logger: spy_logger)
invalid_project.activate('test_exp', 'test_user')
expect(spy_logger).to have_received(:log).with(Logger::ERROR, 'Provided datafile is in an invalid format.')
expect(spy_logger).to have_received(:log).with(Logger::ERROR, "Optimizely instance is not valid. Failing 'activate'.")
@@ -976,8 +976,8 @@ def callback(_args); end
)
custom_project_instance = Optimizely::Project.new(
- nil, nil, spy_logger, error_handler,
- false, nil, nil, http_project_config_manager, notification_center
+ logger: spy_logger, error_handler: error_handler,
+ config_manager: http_project_config_manager, notification_center: notification_center
)
sleep 0.1 until http_project_config_manager.ready?
@@ -1003,8 +1003,8 @@ def callback(_args); end
)
custom_project_instance = Optimizely::Project.new(
- nil, nil, spy_logger, error_handler,
- false, nil, nil, http_project_config_manager, notification_center
+ logger: spy_logger, error_handler: error_handler,
+ config_manager: http_project_config_manager, notification_center: notification_center
)
sleep 0.1 until http_project_config_manager.ready?
@@ -1037,8 +1037,8 @@ def callback(_args); end
expect(notification_center).to receive(:send_notifications).ordered
custom_project_instance = Optimizely::Project.new(
- nil, nil, spy_logger, error_handler,
- false, nil, sdk_key, nil, notification_center
+ logger: spy_logger, error_handler: error_handler,
+ sdk_key: sdk_key, notification_center: notification_center
)
sleep 0.1 until custom_project_instance.config_manager.ready?
@@ -1132,7 +1132,7 @@ def callback(_args); end
end
it 'should properly track an event with tags even when the project does not have a custom logger' do
- custom_project_instance = Optimizely::Project.new(config_body_JSON, nil, spy_logger, error_handler, false, nil, nil, nil, nil, nil, [], {batch_size: 1})
+ custom_project_instance = Optimizely::Project.new(datafile: config_body_JSON, logger: spy_logger, error_handler: error_handler, event_processor_options: {batch_size: 1})
params = @expected_track_event_params
params[:visitors][0][:snapshots][0][:events][0][:tags] = {revenue: 42}
@@ -1217,7 +1217,7 @@ def callback(_args); end
describe '.typed audiences' do
before(:example) do
- @project_typed_audience_instance = Optimizely::Project.new(JSON.dump(OptimizelySpec::CONFIG_DICT_WITH_TYPED_AUDIENCES), nil, spy_logger, error_handler, false, nil, nil, nil, nil, nil, [], {batch_size: 1})
+ @project_typed_audience_instance = Optimizely::Project.new(datafile: JSON.dump(OptimizelySpec::CONFIG_DICT_WITH_TYPED_AUDIENCES), logger: spy_logger, error_handler: error_handler, event_processor_options: {batch_size: 1})
@expected_event_params = {
account_id: '4879520872',
project_id: '11624721371',
@@ -1424,7 +1424,7 @@ def callback(_args); end
end
it 'should log an error when called with an invalid Project object' do
- invalid_project = Optimizely::Project.new('invalid', nil, spy_logger)
+ invalid_project = Optimizely::Project.new(datafile: 'invalid', logger: spy_logger)
invalid_project.track('test_event', 'test_user')
expect(spy_logger).to have_received(:log).with(Logger::ERROR, 'Provided datafile is in an invalid format.')
expect(spy_logger).to have_received(:log).with(Logger::ERROR, "Optimizely instance is not valid. Failing 'track'.")
@@ -1532,7 +1532,7 @@ def callback(_args); end
end
it 'should log an error when called with an invalid Project object' do
- invalid_project = Optimizely::Project.new('invalid', nil, spy_logger)
+ invalid_project = Optimizely::Project.new(datafile: 'invalid', logger: spy_logger)
invalid_project.get_variation('test_exp', 'test_user')
expect(spy_logger).to have_received(:log).with(Logger::ERROR, 'Provided datafile is in an invalid format.')
expect(spy_logger).to have_received(:log).with(Logger::ERROR, "Optimizely instance is not valid. Failing 'get_variation'.")
@@ -1620,7 +1620,7 @@ def callback(_args); end
end
it 'should return false when called with invalid project config' do
- invalid_project = Optimizely::Project.new('invalid', nil, spy_logger)
+ invalid_project = Optimizely::Project.new(datafile: 'invalid', logger: spy_logger)
expect(invalid_project.is_feature_enabled('totally_invalid_feature_key', 'test_user')).to be false
expect(spy_logger).to have_received(:log).once.with(Logger::ERROR, 'Provided datafile is in an invalid format.')
expect(spy_logger).to have_received(:log).once.with(Logger::ERROR, "Optimizely instance is not valid. Failing 'is_feature_enabled'.")
@@ -1758,7 +1758,7 @@ def callback(_args); end
describe '.typed audiences' do
before(:example) do
- @project_typed_audience_instance = Optimizely::Project.new(JSON.dump(OptimizelySpec::CONFIG_DICT_WITH_TYPED_AUDIENCES), nil, spy_logger, error_handler)
+ @project_typed_audience_instance = Optimizely::Project.new(datafile: JSON.dump(OptimizelySpec::CONFIG_DICT_WITH_TYPED_AUDIENCES), logger: spy_logger, error_handler: error_handler)
stub_request(:post, impression_log_url)
end
after(:example) do
@@ -2023,7 +2023,7 @@ def callback(_args); end
describe '#get_enabled_features' do
it 'should return empty when called with invalid project config' do
- invalid_project = Optimizely::Project.new('invalid', nil, spy_logger)
+ invalid_project = Optimizely::Project.new(datafile: 'invalid', logger: spy_logger)
expect(invalid_project.get_enabled_features('test_user')).to be_empty
expect(spy_logger).to have_received(:log).once.with(Logger::ERROR, 'Provided datafile is in an invalid format.')
expect(spy_logger).to have_received(:log).once.with(Logger::ERROR, "Optimizely instance is not valid. Failing 'get_enabled_features'.")
@@ -2249,7 +2249,7 @@ def callback(_args); end
user_attributes = {}
it 'should return nil when called with invalid project config' do
- invalid_project = Optimizely::Project.new('invalid', nil, spy_logger)
+ invalid_project = Optimizely::Project.new(datafile: 'invalid', logger: spy_logger)
expect(invalid_project.get_feature_variable_string('string_single_variable_feature', 'string_variable', user_id, user_attributes))
.to eq(nil)
expect(spy_logger).to have_received(:log).once.with(Logger::ERROR, 'Provided datafile is in an invalid format.')
@@ -2399,7 +2399,7 @@ def callback(_args); end
user_attributes = {}
it 'should return nil when called with invalid project config' do
- invalid_project = Optimizely::Project.new('invalid', nil, spy_logger)
+ invalid_project = Optimizely::Project.new(datafile: 'invalid', logger: spy_logger)
expect(invalid_project.get_feature_variable_json('json_single_variable_feature', 'json_variable', user_id, user_attributes))
.to eq(nil)
expect(spy_logger).to have_received(:log).once.with(Logger::ERROR, 'Provided datafile is in an invalid format.')
@@ -2582,7 +2582,7 @@ def callback(_args); end
user_attributes = {}
it 'should return nil when called with invalid project config' do
- invalid_project = Optimizely::Project.new('invalid', nil, spy_logger)
+ invalid_project = Optimizely::Project.new(datafile: 'invalid', logger: spy_logger)
expect(invalid_project.get_feature_variable_boolean('boolean_single_variable_feature', 'boolean_variable', user_id, user_attributes))
.to eq(nil)
expect(spy_logger).to have_received(:log).once.with(Logger::ERROR, 'Provided datafile is in an invalid format.')
@@ -2626,7 +2626,7 @@ def callback(_args); end
user_attributes = {}
it 'should return nil when called with invalid project config' do
- invalid_project = Optimizely::Project.new('invalid', nil, spy_logger)
+ invalid_project = Optimizely::Project.new(datafile: 'invalid', logger: spy_logger)
expect(invalid_project.get_feature_variable_double('double_single_variable_feature', 'double_variable', user_id, user_attributes))
.to eq(nil)
expect(spy_logger).to have_received(:log).once.with(Logger::ERROR, 'Provided datafile is in an invalid format.')
@@ -2672,7 +2672,7 @@ def callback(_args); end
user_attributes = {}
it 'should return nil when called with invalid project config' do
- invalid_project = Optimizely::Project.new('invalid', nil, spy_logger)
+ invalid_project = Optimizely::Project.new(datafile: 'invalid', logger: spy_logger)
expect(invalid_project.get_feature_variable_integer('integer_single_variable_feature', 'integer_variable', user_id, user_attributes))
.to eq(nil)
expect(spy_logger).to have_received(:log).once.with(Logger::ERROR, 'Provided datafile is in an invalid format.')
@@ -2718,7 +2718,7 @@ def callback(_args); end
user_attributes = {}
it 'should return nil when called with invalid project config' do
- invalid_project = Optimizely::Project.new('invalid', nil, spy_logger)
+ invalid_project = Optimizely::Project.new(datafile: 'invalid', logger: spy_logger)
expect(invalid_project.get_all_feature_variables('all_variables_feature', user_id, user_attributes))
.to eq(nil)
expect(spy_logger).to have_received(:log).once.with(Logger::ERROR, 'Provided datafile is in an invalid format.')
@@ -2951,7 +2951,7 @@ def callback(_args); end
user_attributes = {}
it 'should return nil when called with invalid project config' do
- invalid_project = Optimizely::Project.new('invalid', nil, spy_logger)
+ invalid_project = Optimizely::Project.new(datafile: 'invalid', logger: spy_logger)
expect(invalid_project.get_feature_variable('string_single_variable_feature', 'string_variable', user_id, user_attributes))
.to eq(nil)
expect(spy_logger).to have_received(:log).once.with(Logger::ERROR, 'Provided datafile is in an invalid format.')
@@ -3161,7 +3161,7 @@ def callback(_args); end
describe '.typed audiences' do
before(:example) do
- @project_typed_audience_instance = Optimizely::Project.new(JSON.dump(OptimizelySpec::CONFIG_DICT_WITH_TYPED_AUDIENCES), nil, spy_logger, error_handler)
+ @project_typed_audience_instance = Optimizely::Project.new(datafile: JSON.dump(OptimizelySpec::CONFIG_DICT_WITH_TYPED_AUDIENCES), logger: spy_logger, error_handler: error_handler)
end
after(:example) do
@project_typed_audience_instance.close
@@ -3466,7 +3466,7 @@ def callback(_args); end
valid_variation = {id: '111128', key: 'control'}
it 'should log an error when called with an invalid Project object' do
- invalid_project = Optimizely::Project.new('invalid', nil, spy_logger)
+ invalid_project = Optimizely::Project.new(datafile: 'invalid', logger: spy_logger)
invalid_project.set_forced_variation(valid_experiment[:key], user_id, valid_variation[:key])
expect(spy_logger).to have_received(:log).with(Logger::ERROR, 'Provided datafile is in an invalid format.')
expect(spy_logger).to have_received(:log).with(Logger::ERROR, "Optimizely instance is not valid. Failing 'set_forced_variation'.")
@@ -3523,7 +3523,7 @@ def callback(_args); end
valid_experiment = {id: '111127', key: 'test_experiment'}
it 'should log an error when called with an invalid Project object' do
- invalid_project = Optimizely::Project.new('invalid', nil, spy_logger)
+ invalid_project = Optimizely::Project.new(datafile: 'invalid', logger: spy_logger)
invalid_project.get_forced_variation(valid_experiment[:key], user_id)
expect(spy_logger).to have_received(:log).with(Logger::ERROR, 'Provided datafile is in an invalid format.')
expect(spy_logger).to have_received(:log).with(Logger::ERROR, "Optimizely instance is not valid. Failing 'get_forced_variation'.")
@@ -3569,7 +3569,7 @@ def callback(_args); end
describe '#is_valid' do
it 'should return false when called with an invalid datafile' do
- invalid_project = Optimizely::Project.new('invalid', nil, spy_logger)
+ invalid_project = Optimizely::Project.new(datafile: 'invalid', logger: spy_logger)
expect(invalid_project.is_valid).to be false
invalid_project.close
end
@@ -3595,9 +3595,9 @@ def callback(_args); end
event_processor = Optimizely::BatchEventProcessor.new(event_dispatcher: Optimizely::EventDispatcher.new)
- Optimizely::Project.new(config_body_JSON, nil, spy_logger, error_handler).close
+ Optimizely::Project.new(datafile: config_body_JSON, logger: spy_logger, error_handler: error_handler).close
- project_instance = Optimizely::Project.new(nil, nil, nil, nil, true, nil, nil, config_manager, nil, event_processor)
+ project_instance = Optimizely::Project.new(skip_json_validation: true, config_manager: config_manager, event_processor: event_processor)
expect(config_manager.stopped).to be false
expect(event_processor.started).to be false
@@ -3617,8 +3617,8 @@ def callback(_args); end
)
project_instance = Optimizely::Project.new(
- nil, nil, spy_logger, error_handler,
- false, nil, nil, http_project_config_manager
+ logger: spy_logger, error_handler: error_handler,
+ config_manager: http_project_config_manager
)
project_instance.close
@@ -3631,8 +3631,8 @@ def callback(_args); end
)
project_instance = Optimizely::Project.new(
- config_body_JSON, nil, spy_logger, error_handler,
- false, nil, nil, http_project_config_manager
+ datafile: config_body_JSON, logger: spy_logger, error_handler: error_handler,
+ config_manager: http_project_config_manager
)
sleep 0.1 until http_project_config_manager.ready?
@@ -3652,8 +3652,8 @@ def callback(_args); end
)
project_instance = Optimizely::Project.new(
- nil, nil, spy_logger, error_handler,
- false, nil, nil, static_project_config_manager
+ logger: spy_logger, error_handler: error_handler,
+ config_manager: static_project_config_manager
)
project_instance.close
@@ -3666,8 +3666,8 @@ def callback(_args); end
)
project_instance = Optimizely::Project.new(
- config_body_JSON, nil, spy_logger, error_handler,
- false, nil, nil, static_project_config_manager
+ datafile: config_body_JSON, logger: spy_logger, error_handler: error_handler,
+ config_manager: static_project_config_manager
)
project_instance.close
@@ -3696,7 +3696,7 @@ def callback(_args); end
describe '#decide' do
describe 'should return empty decision object with correct reason when sdk is not ready' do
it 'when sdk is not ready' do
- invalid_project = Optimizely::Project.new('invalid', nil, spy_logger)
+ invalid_project = Optimizely::Project.new(datafile: 'invalid', logger: spy_logger)
user_context = project_instance.create_user_context('user1')
decision = invalid_project.decide(user_context, 'dummy_flag')
expect(decision.as_json).to eq(
@@ -4216,7 +4216,7 @@ def callback(_args); end
describe '#decide_all' do
it 'should get empty object when sdk is not ready' do
- invalid_project = Optimizely::Project.new('invalid', nil, spy_logger)
+ invalid_project = Optimizely::Project.new(datafile: 'invalid', logger: spy_logger)
user_context = project_instance.create_user_context('user1')
decisions = invalid_project.decide_all(user_context)
expect(decisions).to eq({})
@@ -4283,7 +4283,7 @@ def callback(_args); end
boolean_feature
empty_feature
]
- invalid_project = Optimizely::Project.new('invalid', nil, spy_logger)
+ invalid_project = Optimizely::Project.new(datafile: 'invalid', logger: spy_logger)
user_context = project_instance.create_user_context('user1')
decisions = invalid_project.decide_for_keys(user_context, keys)
expect(decisions).to eq({})
@@ -4354,8 +4354,8 @@ def callback(_args); end
it 'should get only enabled decisions for keys when ENABLED_FLAGS_ONLY is true in default_decide_options' do
custom_project_instance = Optimizely::Project.new(
- config_body_JSON, nil, spy_logger, error_handler,
- false, nil, nil, nil, nil, nil, [Optimizely::Decide::OptimizelyDecideOption::ENABLED_FLAGS_ONLY]
+ datafile: config_body_JSON, logger: spy_logger, error_handler: error_handler,
+ default_decide_options: [Optimizely::Decide::OptimizelyDecideOption::ENABLED_FLAGS_ONLY]
)
keys = %w[
boolean_single_variable_feature
@@ -4392,7 +4392,7 @@ def callback(_args); end
describe 'default_decide_options' do
describe 'EXCLUDE_VARIABLES' do
it 'should include variables when the option is not set in default_decide_options' do
- custom_project_instance = Optimizely::Project.new(config_body_JSON, nil, spy_logger, error_handler)
+ custom_project_instance = Optimizely::Project.new(datafile: config_body_JSON, logger: spy_logger, error_handler: error_handler)
experiment_to_return = config_body['experiments'][3]
variation_to_return = experiment_to_return['variations'][0]
decision_to_return = Optimizely::DecisionService::Decision.new(
@@ -4418,8 +4418,8 @@ def callback(_args); end
it 'should exclude variables when the option is set in default_decide_options' do
custom_project_instance = Optimizely::Project.new(
- config_body_JSON, nil, spy_logger, error_handler,
- false, nil, nil, nil, nil, nil, [Optimizely::Decide::OptimizelyDecideOption::EXCLUDE_VARIABLES]
+ datafile: config_body_JSON, logger: spy_logger, error_handler: error_handler,
+ default_decide_options: [Optimizely::Decide::OptimizelyDecideOption::EXCLUDE_VARIABLES]
)
experiment_to_return = config_body['experiments'][3]
variation_to_return = experiment_to_return['variations'][0]
@@ -4448,8 +4448,8 @@ def callback(_args); end
describe 'INCLUDE_REASONS' do
it 'should include reasons when the option is set in default_decide_options' do
custom_project_instance = Optimizely::Project.new(
- config_body_JSON, nil, spy_logger, error_handler,
- false, nil, nil, nil, nil, nil, [Optimizely::Decide::OptimizelyDecideOption::INCLUDE_REASONS]
+ datafile: config_body_JSON, logger: spy_logger, error_handler: error_handler,
+ default_decide_options: [Optimizely::Decide::OptimizelyDecideOption::INCLUDE_REASONS]
)
expect(custom_project_instance.notification_center).to receive(:send_notifications)
.once.with(Optimizely::NotificationCenter::NOTIFICATION_TYPES[:LOG_EVENT], any_args)
@@ -4497,7 +4497,7 @@ def callback(_args); end
end
it 'should not include reasons when the option is not set in default_decide_options' do
- custom_project_instance = Optimizely::Project.new(config_body_JSON, nil, spy_logger, error_handler)
+ custom_project_instance = Optimizely::Project.new(datafile: config_body_JSON, logger: spy_logger, error_handler: error_handler)
expect(custom_project_instance.notification_center).to receive(:send_notifications)
.once.with(Optimizely::NotificationCenter::NOTIFICATION_TYPES[:LOG_EVENT], any_args)
expect(custom_project_instance.notification_center).to receive(:send_notifications)
@@ -4532,7 +4532,7 @@ def callback(_args); end
describe 'DISABLE_DECISION_EVENT' do
it 'should send event when option is not set in default_decide_options' do
- custom_project_instance = Optimizely::Project.new(config_body_JSON, nil, spy_logger, error_handler)
+ custom_project_instance = Optimizely::Project.new(datafile: config_body_JSON, logger: spy_logger, error_handler: error_handler)
experiment_to_return = config_body['experiments'][3]
variation_to_return = experiment_to_return['variations'][0]
expect(custom_project_instance.event_dispatcher).to receive(:dispatch_event).with(instance_of(Optimizely::Event))
@@ -4549,8 +4549,8 @@ def callback(_args); end
it 'should not send event when option is set in default_decide_options' do
custom_project_instance = Optimizely::Project.new(
- config_body_JSON, nil, spy_logger, error_handler,
- false, nil, nil, nil, nil, nil, [Optimizely::Decide::OptimizelyDecideOption::DISABLE_DECISION_EVENT]
+ datafile: config_body_JSON, logger: spy_logger, error_handler: error_handler,
+ default_decide_options: [Optimizely::Decide::OptimizelyDecideOption::DISABLE_DECISION_EVENT]
)
experiment_to_return = config_body['experiments'][3]
variation_to_return = experiment_to_return['variations'][0]
@@ -4574,7 +4574,7 @@ def callback(_args); end
stub_request(:get, "https://cdn.optimizely.com/datafiles/#{sdk_key}.json")
.to_return(status: 200, body: config_body_integrations_JSON)
sdk_settings = Optimizely::Helpers::OptimizelySdkSettings.new(disable_odp: true)
- project = Optimizely::Project.new(nil, nil, spy_logger, error_handler, false, nil, sdk_key, nil, nil, nil, [], {}, sdk_settings)
+ project = Optimizely::Project.new(logger: spy_logger, error_handler: error_handler, sdk_key: sdk_key, settings: sdk_settings)
expect(project.odp_manager.instance_variable_get('@event_manager')).to be_nil
expect(project.odp_manager.instance_variable_get('@segment_manager')).to be_nil
project.close
@@ -4587,7 +4587,7 @@ def callback(_args); end
stub_request(:get, "https://cdn.optimizely.com/datafiles/#{sdk_key}.json")
.to_return(status: 200, body: config_body_integrations_JSON)
sdk_settings = Optimizely::Helpers::OptimizelySdkSettings.new(odp_event_flush_interval: 0)
- project = Optimizely::Project.new(nil, nil, spy_logger, error_handler, false, nil, sdk_key, nil, nil, nil, [], {}, sdk_settings)
+ project = Optimizely::Project.new(logger: spy_logger, error_handler: error_handler, sdk_key: sdk_key, settings: sdk_settings)
event_manager = project.odp_manager.instance_variable_get('@event_manager')
expect(event_manager.instance_variable_get('@flush_interval')).to eq 0
project.close
@@ -4599,7 +4599,7 @@ def callback(_args); end
stub_request(:get, "https://cdn.optimizely.com/datafiles/#{sdk_key}.json")
.to_return(status: 200, body: config_body_integrations_JSON)
sdk_settings = Optimizely::Helpers::OptimizelySdkSettings.new(odp_event_flush_interval: nil)
- project = Optimizely::Project.new(nil, nil, spy_logger, error_handler, false, nil, sdk_key, nil, nil, nil, [], {}, sdk_settings)
+ project = Optimizely::Project.new(logger: spy_logger, error_handler: error_handler, sdk_key: sdk_key, settings: sdk_settings)
event_manager = project.odp_manager.instance_variable_get('@event_manager')
expect(event_manager.instance_variable_get('@flush_interval')).to eq 1
project.close
@@ -4612,7 +4612,7 @@ def callback(_args); end
.to_return(status: 200, body: config_body_integrations_JSON)
sdk_settings = Optimizely::Helpers::OptimizelySdkSettings.new(segments_cache_size: 5)
- project = Optimizely::Project.new(nil, nil, spy_logger, error_handler, false, nil, sdk_key, nil, nil, nil, [], {}, sdk_settings)
+ project = Optimizely::Project.new(logger: spy_logger, error_handler: error_handler, sdk_key: sdk_key, settings: sdk_settings)
segment_manager = project.odp_manager.instance_variable_get('@segment_manager')
expect(segment_manager.instance_variable_get('@segments_cache').capacity).to eq 5
project.close
@@ -4624,7 +4624,7 @@ def callback(_args); end
stub_request(:get, "https://cdn.optimizely.com/datafiles/#{sdk_key}.json")
.to_return(status: 200, body: config_body_integrations_JSON)
sdk_settings = Optimizely::Helpers::OptimizelySdkSettings.new(segments_cache_timeout_in_secs: 5)
- project = Optimizely::Project.new(nil, nil, spy_logger, error_handler, false, nil, sdk_key, nil, nil, nil, [], {}, sdk_settings)
+ project = Optimizely::Project.new(logger: spy_logger, error_handler: error_handler, sdk_key: sdk_key, settings: sdk_settings)
segment_manager = project.odp_manager.instance_variable_get('@segment_manager')
expect(segment_manager.instance_variable_get('@segments_cache').timeout).to eq 5
project.close
@@ -4636,7 +4636,7 @@ def callback(_args); end
stub_request(:get, "https://cdn.optimizely.com/datafiles/#{sdk_key}.json")
.to_return(status: 200, body: config_body_integrations_JSON)
sdk_settings = Optimizely::Helpers::OptimizelySdkSettings.new(segments_cache_size: 10, segments_cache_timeout_in_secs: 5)
- project = Optimizely::Project.new(nil, nil, spy_logger, error_handler, false, nil, sdk_key, nil, nil, nil, [], {}, sdk_settings)
+ project = Optimizely::Project.new(logger: spy_logger, error_handler: error_handler, sdk_key: sdk_key, settings: sdk_settings)
segment_manager = project.odp_manager.instance_variable_get('@segment_manager')
segments_cache = segment_manager.instance_variable_get('@segments_cache')
expect(segments_cache.capacity).to eq 10
@@ -4650,7 +4650,7 @@ def callback(_args); end
stub_request(:get, "https://cdn.optimizely.com/datafiles/#{sdk_key}.json")
.to_return(status: 200, body: config_body_integrations_JSON)
sdk_settings = Optimizely::Helpers::OptimizelySdkSettings.new
- project = Optimizely::Project.new(nil, nil, spy_logger, error_handler, false, nil, sdk_key, nil, nil, nil, [], {}, sdk_settings)
+ project = Optimizely::Project.new(logger: spy_logger, error_handler: error_handler, sdk_key: sdk_key, settings: sdk_settings)
segment_manager = project.odp_manager.instance_variable_get('@segment_manager')
segments_cache = segment_manager.instance_variable_get('@segments_cache')
expect(segments_cache.capacity).to eq 10_000
@@ -4664,7 +4664,7 @@ def callback(_args); end
stub_request(:get, "https://cdn.optimizely.com/datafiles/#{sdk_key}.json")
.to_return(status: 200, body: config_body_integrations_JSON)
sdk_settings = Optimizely::Helpers::OptimizelySdkSettings.new(segments_cache_size: 0, segments_cache_timeout_in_secs: 0)
- project = Optimizely::Project.new(nil, nil, spy_logger, error_handler, false, nil, sdk_key, nil, nil, nil, [], {}, sdk_settings)
+ project = Optimizely::Project.new(logger: spy_logger, error_handler: error_handler, sdk_key: sdk_key, settings: sdk_settings)
segment_manager = project.odp_manager.instance_variable_get('@segment_manager')
segments_cache = segment_manager.instance_variable_get('@segments_cache')
expect(segments_cache.capacity).to eq 0
@@ -4684,7 +4684,7 @@ def save(key, value); end
stub_request(:get, "https://cdn.optimizely.com/datafiles/#{sdk_key}.json")
.to_return(status: 200, body: config_body_integrations_JSON)
sdk_settings = Optimizely::Helpers::OptimizelySdkSettings.new(odp_segments_cache: CustomCache.new)
- project = Optimizely::Project.new(nil, nil, spy_logger, error_handler, false, nil, sdk_key, nil, nil, nil, [], {}, sdk_settings)
+ project = Optimizely::Project.new(logger: spy_logger, error_handler: error_handler, sdk_key: sdk_key, settings: sdk_settings)
segment_manager = project.odp_manager.instance_variable_get('@segment_manager')
expect(segment_manager.instance_variable_get('@segments_cache')).to be_a CustomCache
project.close
@@ -4698,7 +4698,7 @@ class InvalidCustomCache; end # rubocop:disable Lint/ConstantDefinitionInBlock
stub_request(:get, "https://cdn.optimizely.com/datafiles/#{sdk_key}.json")
.to_return(status: 200, body: config_body_integrations_JSON)
sdk_settings = Optimizely::Helpers::OptimizelySdkSettings.new(odp_segments_cache: InvalidCustomCache.new)
- project = Optimizely::Project.new(nil, nil, spy_logger, error_handler, false, nil, sdk_key, nil, nil, nil, [], {}, sdk_settings)
+ project = Optimizely::Project.new(logger: spy_logger, error_handler: error_handler, sdk_key: sdk_key, settings: sdk_settings)
segment_manager = project.odp_manager.instance_variable_get('@segment_manager')
expect(segment_manager.instance_variable_get('@segments_cache')).to be_a Optimizely::LRUCache
@@ -4722,7 +4722,7 @@ def fetch_qualified_segments(user_key, user_value, options); end
stub_request(:get, "https://cdn.optimizely.com/datafiles/#{sdk_key}.json")
.to_return(status: 200, body: config_body_integrations_JSON)
sdk_settings = Optimizely::Helpers::OptimizelySdkSettings.new(odp_segment_manager: CustomSegmentManager.new)
- project = Optimizely::Project.new(config_body_integrations_JSON, nil, spy_logger, error_handler, false, nil, nil, nil, nil, nil, [], {}, sdk_settings)
+ project = Optimizely::Project.new(datafile: config_body_integrations_JSON, logger: spy_logger, error_handler: error_handler, settings: sdk_settings)
segment_manager = project.odp_manager.instance_variable_get('@segment_manager')
expect(segment_manager).to be_a CustomSegmentManager
project.fetch_qualified_segments(user_id: 'test')
@@ -4738,7 +4738,7 @@ class InvalidSegmentManager; end # rubocop:disable Lint/ConstantDefinitionInBloc
stub_request(:get, "https://cdn.optimizely.com/datafiles/#{sdk_key}.json")
.to_return(status: 200, body: config_body_integrations_JSON)
sdk_settings = Optimizely::Helpers::OptimizelySdkSettings.new(odp_segment_manager: InvalidSegmentManager.new)
- project = Optimizely::Project.new(nil, nil, spy_logger, error_handler, false, nil, sdk_key, nil, nil, nil, [], {}, sdk_settings)
+ project = Optimizely::Project.new(logger: spy_logger, error_handler: error_handler, sdk_key: sdk_key, settings: sdk_settings)
segment_manager = project.odp_manager.instance_variable_get('@segment_manager')
expect(segment_manager).to be_a Optimizely::OdpSegmentManager
@@ -4761,7 +4761,7 @@ def running?; end
stub_request(:get, "https://cdn.optimizely.com/datafiles/#{sdk_key}.json")
.to_return(status: 200, body: config_body_integrations_JSON)
sdk_settings = Optimizely::Helpers::OptimizelySdkSettings.new(odp_event_manager: CustomEventManager.new)
- project = Optimizely::Project.new(nil, nil, spy_logger, error_handler, false, nil, sdk_key, nil, nil, nil, [], {}, sdk_settings)
+ project = Optimizely::Project.new(logger: spy_logger, error_handler: error_handler, sdk_key: sdk_key, settings: sdk_settings)
event_manager = project.odp_manager.instance_variable_get('@event_manager')
expect(event_manager).to be_a CustomEventManager
project.send_odp_event(action: 'test', identifiers: {wow: 'great'})
@@ -4776,7 +4776,7 @@ class InvalidEventManager; end # rubocop:disable Lint/ConstantDefinitionInBlock
stub_request(:get, "https://cdn.optimizely.com/datafiles/#{sdk_key}.json")
.to_return(status: 200, body: config_body_integrations_JSON)
sdk_settings = Optimizely::Helpers::OptimizelySdkSettings.new(odp_event_manager: InvalidEventManager.new)
- project = Optimizely::Project.new(nil, nil, spy_logger, error_handler, false, nil, sdk_key, nil, nil, nil, [], {}, sdk_settings)
+ project = Optimizely::Project.new(logger: spy_logger, error_handler: error_handler, sdk_key: sdk_key, settings: sdk_settings)
event_manager = project.odp_manager.instance_variable_get('@event_manager')
expect(event_manager).to be_a Optimizely::OdpEventManager
@@ -4791,7 +4791,7 @@ class InvalidEventManager; end # rubocop:disable Lint/ConstantDefinitionInBlock
stub_request(:post, 'https://api.zaius.com/v3/events').to_return(status: 200)
expect(spy_logger).to receive(:log).once.with(Logger::DEBUG, 'ODP event queue: flushing batch size 1.')
expect(spy_logger).not_to receive(:log).with(Logger::ERROR, anything)
- project = Optimizely::Project.new(config_body_integrations_JSON, nil, spy_logger)
+ project = Optimizely::Project.new(datafile: config_body_integrations_JSON, logger: spy_logger)
project.send_odp_event(type: 'wow', action: 'great', identifiers: {amazing: 'fantastic'}, data: {})
project.close
end
@@ -4803,7 +4803,7 @@ class InvalidEventManager; end # rubocop:disable Lint/ConstantDefinitionInBlock
stub_request(:post, 'https://api.zaius.com/v3/events').to_return(status: 200)
expect(spy_logger).to receive(:log).once.with(Logger::DEBUG, 'ODP event queue: flushing batch size 1.')
expect(spy_logger).not_to receive(:log).with(Logger::ERROR, anything)
- project = Optimizely::Project.new(nil, nil, spy_logger, nil, false, nil, sdk_key)
+ project = Optimizely::Project.new(logger: spy_logger, sdk_key: sdk_key)
sleep 0.1 until project.odp_manager.instance_variable_get('@event_manager').instance_variable_get('@event_queue').empty?
@@ -4814,7 +4814,7 @@ class InvalidEventManager; end # rubocop:disable Lint/ConstantDefinitionInBlock
it 'should log error when odp disabled' do
expect(spy_logger).to receive(:log).once.with(Logger::ERROR, 'ODP is not enabled.')
sdk_settings = Optimizely::Helpers::OptimizelySdkSettings.new(disable_odp: true)
- custom_project_instance = Optimizely::Project.new(config_body_integrations_JSON, nil, spy_logger, error_handler, false, nil, nil, nil, nil, nil, [], {}, sdk_settings)
+ custom_project_instance = Optimizely::Project.new(datafile: config_body_integrations_JSON, logger: spy_logger, error_handler: error_handler, settings: sdk_settings)
custom_project_instance.send_odp_event(type: 'wow', action: 'great', identifiers: {amazing: 'fantastic'}, data: {})
custom_project_instance.close
end
@@ -4823,7 +4823,7 @@ class InvalidEventManager; end # rubocop:disable Lint/ConstantDefinitionInBlock
stub_request(:get, "https://cdn.optimizely.com/datafiles/#{sdk_key}.json")
.to_return(status: 200, body: nil)
expect(spy_logger).to receive(:log).once.with(Logger::ERROR, "Optimizely instance is not valid. Failing 'send_odp_event'.")
- project = Optimizely::Project.new(nil, nil, spy_logger, nil, false, nil, sdk_key)
+ project = Optimizely::Project.new(logger: spy_logger, sdk_key: sdk_key)
project.send_odp_event(type: 'wow', action: 'great', identifiers: {amazing: 'fantastic'}, data: {})
project.close
end
@@ -4833,28 +4833,28 @@ class InvalidEventManager; end # rubocop:disable Lint/ConstantDefinitionInBlock
.to_return(status: 200, body: config_body_integrations_JSON)
expect(spy_logger).to receive(:log).once.with(Logger::ERROR, 'ODP is not enabled.')
sdk_settings = Optimizely::Helpers::OptimizelySdkSettings.new(disable_odp: true)
- project = Optimizely::Project.new(nil, nil, spy_logger, error_handler, false, nil, sdk_key, nil, nil, nil, [], {}, sdk_settings)
+ project = Optimizely::Project.new(logger: spy_logger, error_handler: error_handler, sdk_key: sdk_key, settings: sdk_settings)
project.send_odp_event(type: 'wow', action: 'great', identifiers: {amazing: 'fantastic'}, data: {})
project.close
end
it 'should log error with invalid data' do
expect(spy_logger).to receive(:log).once.with(Logger::ERROR, 'ODP data is not valid.')
- project = Optimizely::Project.new(config_body_integrations_JSON, nil, spy_logger)
+ project = Optimizely::Project.new(datafile: config_body_integrations_JSON, logger: spy_logger)
project.send_odp_event(type: 'wow', action: 'great', identifiers: {amazing: 'fantastic'}, data: {'wow': {}})
project.close
end
it 'should log error with empty identifiers' do
expect(spy_logger).to receive(:log).once.with(Logger::ERROR, 'ODP events must have at least one key-value pair in identifiers.')
- project = Optimizely::Project.new(config_body_integrations_JSON, nil, spy_logger)
+ project = Optimizely::Project.new(datafile: config_body_integrations_JSON, logger: spy_logger)
project.send_odp_event(type: 'wow', action: 'great', identifiers: {}, data: {'wow': {}})
project.close
end
it 'should log error with nil identifiers' do
expect(spy_logger).to receive(:log).once.with(Logger::ERROR, 'ODP events must have at least one key-value pair in identifiers.')
- project = Optimizely::Project.new(config_body_integrations_JSON, nil, spy_logger)
+ project = Optimizely::Project.new(datafile: config_body_integrations_JSON, logger: spy_logger)
project.send_odp_event(type: 'wow', action: 'great', identifiers: nil, data: {'wow': {}})
project.close
end
@@ -4864,7 +4864,7 @@ class InvalidEventManager; end # rubocop:disable Lint/ConstantDefinitionInBlock
feature_key = 'flag-segment'
user_id = 'test_user'
- project = Optimizely::Project.new(config_body_integrations_JSON, nil, spy_logger)
+ project = Optimizely::Project.new(datafile: config_body_integrations_JSON, logger: spy_logger)
allow(project.event_dispatcher).to receive(:dispatch_event).with(instance_of(Optimizely::Event))
expect(project.odp_manager).not_to receive(:send_event)
@@ -4881,20 +4881,20 @@ class InvalidEventManager; end # rubocop:disable Lint/ConstantDefinitionInBlock
it 'should log error with nil action' do
expect(spy_logger).to receive(:log).once.with(Logger::ERROR, 'ODP action is not valid (cannot be empty).')
- project = Optimizely::Project.new(config_body_integrations_JSON, nil, spy_logger)
+ project = Optimizely::Project.new(datafile: config_body_integrations_JSON, logger: spy_logger)
project.send_odp_event(type: 'wow', action: nil, identifiers: {amazing: 'fantastic'}, data: {})
project.close
end
it 'should log error with empty string action' do
expect(spy_logger).to receive(:log).once.with(Logger::ERROR, 'ODP action is not valid (cannot be empty).')
- project = Optimizely::Project.new(config_body_integrations_JSON, nil, spy_logger)
+ project = Optimizely::Project.new(datafile: config_body_integrations_JSON, logger: spy_logger)
project.send_odp_event(type: 'wow', action: '', identifiers: {amazing: 'fantastic'}, data: {})
project.close
end
it 'should use default with nil type' do
- project = Optimizely::Project.new(config_body_integrations_JSON, nil, spy_logger)
+ project = Optimizely::Project.new(datafile: config_body_integrations_JSON, logger: spy_logger)
expect(project.odp_manager).to receive('send_event').with(type: 'fullstack', action: 'great', identifiers: {amazing: 'fantastic'}, data: {})
project.send_odp_event(type: nil, action: 'great', identifiers: {amazing: 'fantastic'}, data: {})
@@ -4904,7 +4904,7 @@ class InvalidEventManager; end # rubocop:disable Lint/ConstantDefinitionInBlock
end
it 'should use default with empty string type' do
- project = Optimizely::Project.new(config_body_integrations_JSON, nil, spy_logger)
+ project = Optimizely::Project.new(datafile: config_body_integrations_JSON, logger: spy_logger)
expect(project.odp_manager).to receive('send_event').with(type: 'fullstack', action: 'great', identifiers: {amazing: 'fantastic'}, data: {})
project.send_odp_event(type: '', action: 'great', identifiers: {amazing: 'fantastic'}, data: {})
diff --git a/spec/user_condition_evaluator_spec.rb b/spec/user_condition_evaluator_spec.rb
index 7aef929e..0d74e514 100644
--- a/spec/user_condition_evaluator_spec.rb
+++ b/spec/user_condition_evaluator_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
#
-# Copyright 2019-2020, Optimizely and contributors
+# Copyright 2019-2020, 2023, Optimizely and contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -28,7 +28,7 @@
let(:error_handler) { Optimizely::NoOpErrorHandler.new }
let(:spy_logger) { spy('logger') }
let(:event_processor) { Optimizely::ForwardingEventProcessor.new(Optimizely::EventDispatcher.new) }
- let(:project_instance) { Optimizely::Project.new(config_body_JSON, nil, spy_logger, error_handler, false, nil, nil, nil, nil, event_processor) }
+ let(:project_instance) { Optimizely::Project.new(datafile: config_body_JSON, logger: spy_logger, error_handler: error_handler, event_processor: event_processor) }
let(:user_context) { project_instance.create_user_context('some-user', {}) }
after(:example) { project_instance.close }
From 1333d605610bc3065cbdf865695cfe190940b0e3 Mon Sep 17 00:00:00 2001
From: Andy Leap <104936100+andrewleap-optimizely@users.noreply.github.com>
Date: Fri, 8 Dec 2023 11:55:21 -0500
Subject: [PATCH 07/18] [FSSDK-9781] warn on duplicate experiment key (#343)
---
lib/optimizely.rb | 2 +-
.../http_project_config_manager.rb | 2 +-
.../static_project_config_manager.rb | 3 +-
lib/optimizely/optimizely_config.rb | 4 +-
spec/optimizely_config_spec.rb | 63 ++++++++++++++++++-
5 files changed, 69 insertions(+), 5 deletions(-)
diff --git a/lib/optimizely.rb b/lib/optimizely.rb
index da6cbbf5..1dbd54c4 100644
--- a/lib/optimizely.rb
+++ b/lib/optimizely.rb
@@ -889,7 +889,7 @@ def get_optimizely_config
if @config_manager.respond_to?(:optimizely_config)
@config_manager.optimizely_config
else
- OptimizelyConfig.new(project_config).config
+ OptimizelyConfig.new(project_config, @logger).config
end
end
diff --git a/lib/optimizely/config_manager/http_project_config_manager.rb b/lib/optimizely/config_manager/http_project_config_manager.rb
index f5f33cbf..03e177b5 100644
--- a/lib/optimizely/config_manager/http_project_config_manager.rb
+++ b/lib/optimizely/config_manager/http_project_config_manager.rb
@@ -141,7 +141,7 @@ def config
end
def optimizely_config
- @optimizely_config = OptimizelyConfig.new(@config).config if @optimizely_config.nil?
+ @optimizely_config = OptimizelyConfig.new(@config, @logger).config if @optimizely_config.nil?
@optimizely_config
end
diff --git a/lib/optimizely/config_manager/static_project_config_manager.rb b/lib/optimizely/config_manager/static_project_config_manager.rb
index 38829ce4..200126f8 100644
--- a/lib/optimizely/config_manager/static_project_config_manager.rb
+++ b/lib/optimizely/config_manager/static_project_config_manager.rb
@@ -41,12 +41,13 @@ def initialize(datafile, logger, error_handler, skip_json_validation)
error_handler,
skip_json_validation
)
+ @logger = logger
@sdk_key = @config&.sdk_key
@optimizely_config = nil
end
def optimizely_config
- @optimizely_config = OptimizelyConfig.new(@config).config if @optimizely_config.nil?
+ @optimizely_config = OptimizelyConfig.new(@config, @logger).config if @optimizely_config.nil?
@optimizely_config
end
diff --git a/lib/optimizely/optimizely_config.rb b/lib/optimizely/optimizely_config.rb
index 1ffbcd94..32a637ad 100644
--- a/lib/optimizely/optimizely_config.rb
+++ b/lib/optimizely/optimizely_config.rb
@@ -19,8 +19,9 @@ module Optimizely
require 'json'
class OptimizelyConfig
include Optimizely::ConditionTreeEvaluator
- def initialize(project_config)
+ def initialize(project_config, logger = nil)
@project_config = project_config
+ @logger = logger || NoOpLogger.new
@rollouts = @project_config.rollouts
@audiences = []
audience_id_lookup_dict = {}
@@ -91,6 +92,7 @@ def audiences_map
def experiments_map
experiments_id_map.values.reduce({}) do |experiments_key_map, experiment|
+ @logger.log(Logger::WARN, "Duplicate experiment keys found in datafile: #{experiment['key']}") if experiments_key_map.key? experiment['key']
experiments_key_map.update(experiment['key'] => experiment)
end
end
diff --git a/spec/optimizely_config_spec.rb b/spec/optimizely_config_spec.rb
index cfac6ba7..4164d3ca 100644
--- a/spec/optimizely_config_spec.rb
+++ b/spec/optimizely_config_spec.rb
@@ -19,6 +19,7 @@
require 'spec_helper'
describe Optimizely::OptimizelyConfig do
+ let(:config_body) { OptimizelySpec::VALID_CONFIG_BODY }
let(:config_body_JSON) { OptimizelySpec::VALID_CONFIG_BODY_JSON }
let(:similar_exp_keys_JSON) { OptimizelySpec::SIMILAR_EXP_KEYS_JSON }
let(:typed_audiences_JSON) { OptimizelySpec::CONFIG_DICT_WITH_TYPED_AUDIENCES_JSON }
@@ -768,7 +769,7 @@
'',
'"exactString" OR "999999999"'
]
- optimizely_config = Optimizely::OptimizelyConfig.new(project_instance_typed_audiences.send(:project_config))
+ optimizely_config = Optimizely::OptimizelyConfig.new(project_instance_typed_audiences.send(:project_config), spy_logger)
audiences_map = optimizely_config.send(:audiences_map)
audience_conditions.each_with_index do |audience_condition, index|
result = optimizely_config.send(:replace_ids_with_names, audience_condition, audiences_map)
@@ -796,4 +797,64 @@
expect(optimizely_config_similar_rule_keys['sdkKey']).to eq('')
expect(optimizely_config_similar_rule_keys['environmentKey']).to eq('')
end
+
+ it 'should use the newest of duplicate experiment keys' do
+ duplicate_experiment_key = 'test_experiment'
+ new_experiment = {
+ 'key': duplicate_experiment_key,
+ 'status': 'Running',
+ 'layerId': '8',
+ "audienceConditions": %w[
+ or
+ 11160
+ ],
+ 'audienceIds': ['11160'],
+ 'id': '111137',
+ 'forcedVariations': {},
+ 'trafficAllocation': [
+ {'entityId': '222242', 'endOfRange': 8000},
+ {'entityId': '', 'endOfRange': 10_000}
+ ],
+ 'variations': [
+ {
+ 'id': '222242',
+ 'key': 'control',
+ 'variables': []
+ }
+ ]
+ }
+
+ new_feature = {
+ 'id': '91117',
+ 'key': 'new_feature',
+ 'experimentIds': ['111137'],
+ 'rolloutId': '',
+ 'variables': [
+ {'id': '127', 'key': 'is_working', 'defaultValue': 'true', 'type': 'boolean'},
+ {'id': '128', 'key': 'environment', 'defaultValue': 'devel', 'type': 'string'},
+ {'id': '129', 'key': 'cost', 'defaultValue': '10.99', 'type': 'double'},
+ {'id': '130', 'key': 'count', 'defaultValue': '999', 'type': 'integer'},
+ {'id': '131', 'key': 'variable_without_usage', 'defaultValue': '45', 'type': 'integer'},
+ {'id': '132', 'key': 'object', 'defaultValue': '{"test": 12}', 'type': 'string', 'subType': 'json'},
+ {'id': '133', 'key': 'true_object', 'defaultValue': '{"true_test": 23.54}', 'type': 'json'}
+ ]
+ }
+
+ config = OptimizelySpec.deep_clone(config_body)
+
+ config['experiments'].push(new_experiment)
+ config['featureFlags'].push(new_feature)
+ project_config = Optimizely::DatafileProjectConfig.new(JSON.dump(config), spy_logger, error_handler)
+
+ opti_config = Optimizely::OptimizelyConfig.new(project_config, spy_logger)
+
+ key_map = opti_config.config['experimentsMap']
+ id_map = opti_config.send(:experiments_id_map)
+
+ expected_warning_message = "Duplicate experiment keys found in datafile: #{duplicate_experiment_key}"
+ expect(spy_logger).to have_received(:log).once.with(Logger::WARN, expected_warning_message)
+
+ expect(key_map[duplicate_experiment_key]['id']).to eq(new_experiment[:id])
+ expect(id_map.values.count { |exp| exp['key'] == duplicate_experiment_key }).to eq(2)
+ end
end
From 32d0d9963267244e7fde2284da3d6454c85f0181 Mon Sep 17 00:00:00 2001
From: Andy Leap <104936100+andrewleap-optimizely@users.noreply.github.com>
Date: Wed, 17 Jan 2024 17:12:15 -0500
Subject: [PATCH 08/18] [FSSDK-9951] add multi thread warning (#345)
---
README.md | 2 ++
lib/optimizely/event_builder.rb | 22 +++++++++++-----------
2 files changed, 13 insertions(+), 11 deletions(-)
diff --git a/README.md b/README.md
index a9899291..be5e0613 100644
--- a/README.md
+++ b/README.md
@@ -78,6 +78,8 @@ You can initialize the Optimizely instance in two ways: directly with a datafile
)
```
+**Note:** The SDK spawns multiple threads when initialized. These threads have infinite loops that are used for fetching the datafile, as well as batching and dispatching events in the background. When using in a web server that spawn multiple child processes, you need to initialize the SDK after those child processes or workers have been spawned.
+
#### HTTP Config Manager
The `HTTPConfigManager` asynchronously polls for datafiles from a specified URL at regular intervals by making HTTP requests.
diff --git a/lib/optimizely/event_builder.rb b/lib/optimizely/event_builder.rb
index 7b1ba51d..9b4ccd1b 100644
--- a/lib/optimizely/event_builder.rb
+++ b/lib/optimizely/event_builder.rb
@@ -65,17 +65,17 @@ def get_common_params(project_config, user_id, attributes)
attributes&.each_key do |attribute_key|
# Omit attribute values that are not supported by the log endpoint.
attribute_value = attributes[attribute_key]
- if Helpers::Validator.attribute_valid?(attribute_key, attribute_value)
- attribute_id = project_config.get_attribute_id attribute_key
- if attribute_id
- visitor_attributes.push(
- entity_id: attribute_id,
- key: attribute_key,
- type: CUSTOM_ATTRIBUTE_FEATURE_TYPE,
- value: attribute_value
- )
- end
- end
+ next unless Helpers::Validator.attribute_valid?(attribute_key, attribute_value)
+
+ attribute_id = project_config.get_attribute_id attribute_key
+ next unless attribute_id
+
+ visitor_attributes.push(
+ entity_id: attribute_id,
+ key: attribute_key,
+ type: CUSTOM_ATTRIBUTE_FEATURE_TYPE,
+ value: attribute_value
+ )
end
# Append Bot Filtering Attribute
if project_config.bot_filtering == true || project_config.bot_filtering == false
From 7a5261dfccdd31eb6e878dd74fac1d2980a5b5e1 Mon Sep 17 00:00:00 2001
From: Andy Leap <104936100+andrewleap-optimizely@users.noreply.github.com>
Date: Thu, 18 Jan 2024 13:24:37 -0500
Subject: [PATCH 09/18] [FSSDK-8582] chore: prepare for 5.0 release (#346)
* bump version and update changelog
* fix license
---
CHANGELOG.md | 62 +++++++++++++++++++++++++++++++++++++++
LICENSE | 2 +-
lib/optimizely/version.rb | 2 +-
3 files changed, 64 insertions(+), 2 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6c54b808..80701e78 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,67 @@
# Optimizely Ruby SDK Changelog
+## 5.0.0
+January 18th, 2024
+
+### New Features
+
+The 5.0.0 release introduces a new primary feature, [Advanced Audience Targeting]( https://docs.developers.optimizely.com/feature-experimentation/docs/optimizely-data-platform-advanced-audience-targeting) enabled through integration with [Optimizely Data Platform (ODP)](https://docs.developers.optimizely.com/optimizely-data-platform/docs)
+([#303](https://github.com/optimizely/ruby-sdk/pull/303),
+[#308](https://github.com/optimizely/ruby-sdk/pull/308),
+[#310](https://github.com/optimizely/ruby-sdk/pull/310),
+[#311](https://github.com/optimizely/ruby-sdk/pull/311),
+[#312](https://github.com/optimizely/ruby-sdk/pull/312),
+[#314](https://github.com/optimizely/ruby-sdk/pull/314),
+[#316](https://github.com/optimizely/ruby-sdk/pull/316)).
+You can use ODP, a high-performance [Customer Data Platform (CDP)]( https://www.optimizely.com/optimization-glossary/customer-data-platform/), to easily create complex real-time segments (RTS) using first-party and 50+ third-party data sources out of the box. You can create custom schemas that support the user attributes important for your business, and stitch together user behavior done on different devices to better understand and target your customers for personalized user experiences. ODP can be used as a single source of truth for these segments in any Optimizely or 3rd party tool.
+
+With ODP accounts integrated into Optimizely projects, you can build audiences using segments pre-defined in ODP. The SDK will fetch the segments for given users and make decisions using the segments. For access to ODP audience targeting in your Feature Experimentation account, please contact your Optimizely Customer Success Manager.
+
+This version includes the following changes:
+
+* New API added to `OptimizelyUserContext`:
+
+ * `fetch_qualified_segments()`: this API will retrieve user segments from the ODP server. The fetched segments will be used for audience evaluation. The fetched data will be stored in the local cache to avoid repeated network delays.
+
+ * When an `OptimizelyUserContext` is created, the SDK will automatically send an identify request to the ODP server to facilitate observing user activities.
+
+* New APIs added to `Optimizely::Project`:
+
+ * `send_odp_event()`: customers can build/send arbitrary ODP events that will bind user identifiers and data to user profiles in ODP.
+
+For details, refer to our documentation pages:
+
+* [Advanced Audience Targeting](https://docs.developers.optimizely.com/feature-experimentation/docs/optimizely-data-platform-advanced-audience-targeting)
+
+* [Server SDK Support](https://docs.developers.optimizely.com/feature-experimentation/v1.0/docs/advanced-audience-targeting-for-server-side-sdks)
+
+* [Initialize Ruby SDK](https://docs.developers.optimizely.com/feature-experimentation/docs/initialize-sdk-ruby)
+
+* [OptimizelyUserContext Ruby SDK](https://docs.developers.optimizely.com/feature-experimentation/docs/optimizelyusercontext-ruby)
+
+* [Advanced Audience Targeting segment qualification methods](https://docs.developers.optimizely.com/feature-experimentation/docs/advanced-audience-targeting-segment-qualification-methods-ruby)
+
+* [Send Optimizely Data Platform data using Advanced Audience Targeting](https://docs.developers.optimizely.com/feature-experimentation/docs/send-odp-data-using-advanced-audience-targeting-ruby)
+
+### Logging
+
+* Add warning to polling intervals below 30 seconds ([#338](https://github.com/optimizely/ruby-sdk/pull/338))
+* Add warning to duplicate experiment keys ([#343](https://github.com/optimizely/ruby-sdk/pull/343))
+
+### Enhancements
+* Removed polling config manager stop restriction, allowing it to be restarted ([#340](https://github.com/optimizely/ruby-sdk/pull/340)).
+* Include object id/key in invalid object errors ([#301](https://github.com/optimizely/ruby-sdk/pull/301)).
+
+### Breaking Changes
+
+* Updated required Ruby version from 2.7 -> 3.0
+* `Optimizely::Project` initialization arguments have been changed from positional to keyword ([#342](https://github.com/optimizely/ruby-sdk/pull/342)).
+* `ODPManager` in the SDK is enabled by default. Unless an ODP account is integrated into the Optimizely projects, most `ODPManager` functions will be ignored. If needed, `ODPManager` can be disabled when `Optimizely::Project` is instantiated.
+
+* `ProjectConfigManager` interface now requires a `sdk_key` method ([#323](https://github.com/optimizely/ruby-sdk/pull/323)).
+* `HTTPProjectConfigManager` requires either the `sdk_key` parameter or a datafile containing an sdkKey ([#323](https://github.com/optimizely/ruby-sdk/pull/323)).
+* `BatchEventProcessor` is now the default `EventProcessor` when `Optimizely::Project` is instantiated ([#325](https://github.com/optimizely/ruby-sdk/pull/325)).
+
## 5.0.0-beta
April 28th, 2023
diff --git a/LICENSE b/LICENSE
index 006d13d5..e2d14477 100644
--- a/LICENSE
+++ b/LICENSE
@@ -187,7 +187,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
- Copyright 2016, Optimizely and contributors
+ © Optimizely 2016
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
diff --git a/lib/optimizely/version.rb b/lib/optimizely/version.rb
index 43d4f749..ae357133 100644
--- a/lib/optimizely/version.rb
+++ b/lib/optimizely/version.rb
@@ -17,5 +17,5 @@
#
module Optimizely
CLIENT_ENGINE = 'ruby-sdk'
- VERSION = '5.0.0-beta'
+ VERSION = '5.0.0'
end
From 2a284da2280d470b75de5099a1eb0a9641c054af Mon Sep 17 00:00:00 2001
From: Andy Leap <104936100+andrewleap-optimizely@users.noreply.github.com>
Date: Fri, 2 Feb 2024 17:09:11 -0500
Subject: [PATCH 10/18] [FSSDK-9990] add missing info to gemspec (#348)
---
optimizely-sdk.gemspec | 24 ++++++++++++++----------
1 file changed, 14 insertions(+), 10 deletions(-)
diff --git a/optimizely-sdk.gemspec b/optimizely-sdk.gemspec
index a34450b1..2a3c87c5 100644
--- a/optimizely-sdk.gemspec
+++ b/optimizely-sdk.gemspec
@@ -3,19 +3,23 @@
require_relative 'lib/optimizely/version'
Gem::Specification.new do |spec|
- spec.name = 'optimizely-sdk'
- spec.version = Optimizely::VERSION
- spec.authors = ['Optimizely']
- spec.email = ['developers@optimizely.com']
+ spec.name = 'optimizely-sdk'
+ spec.version = Optimizely::VERSION
+ spec.authors = ['Optimizely']
+ spec.email = ['developers@optimizely.com']
spec.required_ruby_version = '>= 3.0'
- spec.summary = "Ruby SDK for Optimizely's testing framework"
- spec.description = 'A Ruby SDK for use with Optimizely Feature Experimentation, Optimizely Full Stack (legacy), and Optimizely Rollouts'
- spec.homepage = 'https://www.optimizely.com/'
- spec.license = 'Apache-2.0'
+ spec.summary = "Ruby SDK for Optimizely's testing framework"
+ spec.description = 'A Ruby SDK for use with Optimizely Feature Experimentation, Optimizely Full Stack (legacy), and Optimizely Rollouts'
+ spec.homepage = 'https://github.com/optimizely/ruby-sdk'
+ spec.license = 'Apache-2.0'
+ spec.metadata = {
+ 'source_code_uri' => 'https://github.com/optimizely/ruby-sdk',
+ 'changelog_uri' => 'https://github.com/optimizely/ruby-sdk/blob/master/CHANGELOG.md'
+ }
- spec.files = Dir['lib/**/*', 'LICENSE']
- spec.require_paths = ['lib']
+ spec.files = Dir['lib/**/*', 'LICENSE']
+ spec.require_paths = ['lib']
spec.add_development_dependency 'bundler'
spec.add_development_dependency 'coveralls_reborn'
From a8c3c7bbf7ba0e0141efd3d98e83fc14489d7448 Mon Sep 17 00:00:00 2001
From: Matjaz Pirnovar
Date: Thu, 8 Feb 2024 11:49:38 -0800
Subject: [PATCH 11/18] update version and changelog (#349)
---
CHANGELOG.md | 5 +++++
lib/optimizely/version.rb | 4 ++--
2 files changed, 7 insertions(+), 2 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 80701e78..8cbf930d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
# Optimizely Ruby SDK Changelog
+## 5.0.1
+February 8th, 2024
+
+The 5.0.1 minor release introduces update of metadata in gemspec.
+
## 5.0.0
January 18th, 2024
diff --git a/lib/optimizely/version.rb b/lib/optimizely/version.rb
index ae357133..77ce669f 100644
--- a/lib/optimizely/version.rb
+++ b/lib/optimizely/version.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
#
-# Copyright 2016-2023, Optimizely and contributors
+# Copyright 2016-2024, Optimizely and contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -17,5 +17,5 @@
#
module Optimizely
CLIENT_ENGINE = 'ruby-sdk'
- VERSION = '5.0.0'
+ VERSION = '5.0.1'
end
From 4142ae6b49164de33bcf720eca1e4488c06001ce Mon Sep 17 00:00:00 2001
From: Matjaz Pirnovar
Date: Mon, 10 Jun 2024 13:16:21 -0700
Subject: [PATCH 12/18] [FSSDK-9461] Bump Ruby version to 3.3 (#351)
* add ruby 3.3.0
* add rb 3.3.0 to devcontainer.json
---
.devcontainer/devcontainer.json | 2 +-
.github/workflows/ruby.yml | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 756e7ae0..de1db2b4 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -1,7 +1,7 @@
{
"name": "Ruby SDK",
- "image": "mcr.microsoft.com/devcontainers/ruby:1-3.2-bullseye",
+ "image": "mcr.microsoft.com/devcontainers/ruby:1-3.3-bullseye",
"postCreateCommand": "set -e && bundle install && gem install optimizely-sdk && rake build && gem install pkg/* && gem install solargraph",
diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml
index a3fa180a..98f73108 100644
--- a/.github/workflows/ruby.yml
+++ b/.github/workflows/ruby.yml
@@ -25,7 +25,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- ruby: [ '3.0.0', '3.1.0', '3.2.0' ]
+ ruby: [ '3.0.0', '3.1.0', '3.2.0', '3.3.0' ]
steps:
- uses: actions/checkout@v3
- name: Set up Ruby ${{ matrix.ruby }}
From db57cd0db6f4ab85a74c46135365169b86970d52 Mon Sep 17 00:00:00 2001
From: Farhan Anjum
Date: Wed, 25 Sep 2024 22:50:24 +0600
Subject: [PATCH 13/18] FSSDK-10665] fix: Github Actions YAML files vulnerable
to script injections corrected (#352)
---
.github/workflows/integration_test.yml | 12 ++++++++----
1 file changed, 8 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/integration_test.yml b/.github/workflows/integration_test.yml
index 03a424f2..0b8d086e 100644
--- a/.github/workflows/integration_test.yml
+++ b/.github/workflows/integration_test.yml
@@ -23,15 +23,19 @@ jobs:
path: 'home/runner/travisci-tools'
ref: 'master'
- name: set SDK Branch if PR
+ env:
+ HEAD_REF: ${{ github.head_ref }}
if: ${{ github.event_name == 'pull_request' }}
run: |
- echo "SDK_BRANCH=${{ github.head_ref }}" >> $GITHUB_ENV
- echo "TRAVIS_BRANCH=${{ github.head_ref }}" >> $GITHUB_ENV
+ echo "SDK_BRANCH=$HEAD_REF" >> $GITHUB_ENV
+ echo "TRAVIS_BRANCH=$HEAD_REF" >> $GITHUB_ENV
- name: set SDK Branch if not pull request
+ env:
+ REF_NAME: ${{ github.ref_name }}
if: ${{ github.event_name != 'pull_request' }}
run: |
- echo "SDK_BRANCH=${{ github.ref_name }}" >> $GITHUB_ENV
- echo "TRAVIS_BRANCH=${{ github.ref_name }}" >> $GITHUB_ENV
+ echo "SDK_BRANCH=$REF_NAME" >> $GITHUB_ENV
+ echo "TRAVIS_BRANCH=$REF_NAME" >> $GITHUB_ENV
- name: Trigger build
env:
SDK: ruby
From 321947e3ae11de72929add63d9592a5323e1c789 Mon Sep 17 00:00:00 2001
From: Muzahidul Islam <129880873+muzahidul-opti@users.noreply.github.com>
Date: Fri, 13 Dec 2024 18:28:29 +0600
Subject: [PATCH 14/18] Update ruby.yml (#355)
---
.github/workflows/ruby.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml
index 98f73108..6e9458c7 100644
--- a/.github/workflows/ruby.yml
+++ b/.github/workflows/ruby.yml
@@ -25,7 +25,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- ruby: [ '3.0.0', '3.1.0', '3.2.0', '3.3.0' ]
+ ruby: [ '3.0.0' ]
steps:
- uses: actions/checkout@v3
- name: Set up Ruby ${{ matrix.ruby }}
From 39e8e7e17874e254eb9423c2262a7e0bdaa95de4 Mon Sep 17 00:00:00 2001
From: Muzahidul Islam <129880873+muzahidul-opti@users.noreply.github.com>
Date: Fri, 13 Dec 2024 19:07:59 +0600
Subject: [PATCH 15/18] Revert "Update ruby.yml (#355)" (#357)
This reverts commit 321947e3ae11de72929add63d9592a5323e1c789.
---
.github/workflows/ruby.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml
index 6e9458c7..98f73108 100644
--- a/.github/workflows/ruby.yml
+++ b/.github/workflows/ruby.yml
@@ -25,7 +25,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- ruby: [ '3.0.0' ]
+ ruby: [ '3.0.0', '3.1.0', '3.2.0', '3.3.0' ]
steps:
- uses: actions/checkout@v3
- name: Set up Ruby ${{ matrix.ruby }}
From 69b2453fe1b11e85a18213c631d4e3dc6cdedd27 Mon Sep 17 00:00:00 2001
From: Farhan Anjum
Date: Fri, 10 Jan 2025 01:53:44 +0600
Subject: [PATCH 16/18] [FSSDK-10765] enhancement: Implement UPS request
batching for decideForKeys (#353)
* user profile tracker created
* lib/optimizely.rb -> Added user_profile_tracker require
lib/optimizely.rb -> Updated decide_for_keys method
lib/optimizely.rb -> Enhanced decision-making logic
lib/optimizely.rb -> Integrated UserProfileTracker usage
lib/optimizely.rb -> Refined decision reasons handling
lib/optimizely/user_profile_tracker.rb -> New user profile tracker class
* Implementation complete. Unit Tests are failing.
* lib/optimizely.rb -> Made optional parameter explicit
lib/optimizely/decision_service.rb -> Added user profile tracker usage
lib/optimizely/decision_service.rb -> Clarified handling of user profiles
lib/optimizely/user_profile_tracker.rb -> Fixed user ID reference in error
spec/decision_service_spec.rb -> Adjusted tests for user profile tracker
* lib/optimizely/decision_service.rb -> Simplified decision logging
lib/optimizely/user_profile_tracker.rb -> Improved user profile lookup handling
spec/project_spec.rb -> Updated mocks for decision service calls
* lib/optimizely/decision_service.rb -> Removed user profile tracker instantiation.
lib/optimizely/user_profile_tracker.rb -> Improved error logging message.
spec/decision_service_spec.rb -> Refactored user profile tracking in tests.
spec/project_spec.rb -> Updated decision service method stubs.
spec/user_profile_tracker.rb -> Updated lookup, update and save tests for user_profile_tracker
* spec/user_profile_tracker_spec.rb -> Updated error messages in tests.
* spec/user_profile_tracker_spec.rb -> linting fix
* linting fixes
* Update README.md
* Update README.md
* Trigger checks
* Trigger checks
* Trigger checks
* Trigger checks
* lib/optimizely/user_profile_tracker.rb -> Added user profile init check.
* lib/optimizely/decision_service.rb -> Updated user profile tracker initialization.
* lib/optimizely/decision_service.rb -> Update user profile save method
---------
Co-authored-by: Matjaz Pirnovar
---
lib/optimizely.rb | 174 +++++++++++++++++--------
lib/optimizely/decision_service.rb | 70 ++++++----
lib/optimizely/helpers/validator.rb | 4 +-
lib/optimizely/optimizely_factory.rb | 1 -
lib/optimizely/user_profile_tracker.rb | 64 +++++++++
spec/decision_service_spec.rb | 173 +++++++-----------------
spec/project_spec.rb | 37 ++++--
spec/user_profile_tracker_spec.rb | 101 ++++++++++++++
8 files changed, 402 insertions(+), 222 deletions(-)
create mode 100644 lib/optimizely/user_profile_tracker.rb
create mode 100644 spec/user_profile_tracker_spec.rb
diff --git a/lib/optimizely.rb b/lib/optimizely.rb
index 1dbd54c4..7c5571b3 100644
--- a/lib/optimizely.rb
+++ b/lib/optimizely.rb
@@ -42,6 +42,7 @@
require_relative 'optimizely/odp/lru_cache'
require_relative 'optimizely/odp/odp_manager'
require_relative 'optimizely/helpers/sdk_settings'
+require_relative 'optimizely/user_profile_tracker'
module Optimizely
class Project
@@ -172,65 +173,18 @@ def create_user_context(user_id, attributes = nil)
OptimizelyUserContext.new(self, user_id, attributes)
end
- def decide(user_context, key, decide_options = [])
- # raising on user context as it is internal and not provided directly by the user.
- raise if user_context.class != OptimizelyUserContext
-
- reasons = []
-
- # check if SDK is ready
- unless is_valid
- @logger.log(Logger::ERROR, InvalidProjectConfigError.new('decide').message)
- reasons.push(OptimizelyDecisionMessage::SDK_NOT_READY)
- return OptimizelyDecision.new(flag_key: key, user_context: user_context, reasons: reasons)
- end
-
- # validate that key is a string
- unless key.is_a?(String)
- @logger.log(Logger::ERROR, 'Provided key is invalid')
- reasons.push(format(OptimizelyDecisionMessage::FLAG_KEY_INVALID, key))
- return OptimizelyDecision.new(flag_key: key, user_context: user_context, reasons: reasons)
- end
-
- # validate that key maps to a feature flag
- config = project_config
- feature_flag = config.get_feature_flag_from_key(key)
- unless feature_flag
- @logger.log(Logger::ERROR, "No feature flag was found for key '#{key}'.")
- reasons.push(format(OptimizelyDecisionMessage::FLAG_KEY_INVALID, key))
- return OptimizelyDecision.new(flag_key: key, user_context: user_context, reasons: reasons)
- end
-
- # merge decide_options and default_decide_options
- if decide_options.is_a? Array
- decide_options += @default_decide_options
- else
- @logger.log(Logger::DEBUG, 'Provided decide options is not an array. Using default decide options.')
- decide_options = @default_decide_options
- end
-
+ def create_optimizely_decision(user_context, flag_key, decision, reasons, decide_options, config)
# Create Optimizely Decision Result.
user_id = user_context.user_id
attributes = user_context.user_attributes
variation_key = nil
feature_enabled = false
rule_key = nil
- flag_key = key
all_variables = {}
decision_event_dispatched = false
+ feature_flag = config.get_feature_flag_from_key(flag_key)
experiment = nil
decision_source = Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT']
- context = Optimizely::OptimizelyUserContext::OptimizelyDecisionContext.new(key, nil)
- variation, reasons_received = @decision_service.validated_forced_decision(config, context, user_context)
- reasons.push(*reasons_received)
-
- if variation
- decision = Optimizely::DecisionService::Decision.new(nil, variation, Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST'])
- else
- decision, reasons_received = @decision_service.get_variation_for_feature(config, feature_flag, user_context, decide_options)
- reasons.push(*reasons_received)
- end
-
# Send impression event if Decision came from a feature test and decide options doesn't include disableDecisionEvent
if decision.is_a?(Optimizely::DecisionService::Decision)
experiment = decision.experiment
@@ -249,7 +203,7 @@ def decide(user_context, key, decide_options = [])
# Generate all variables map if decide options doesn't include excludeVariables
unless decide_options.include? OptimizelyDecideOption::EXCLUDE_VARIABLES
feature_flag['variables'].each do |variable|
- variable_value = get_feature_variable_for_variation(key, feature_enabled, variation, variable, user_id)
+ variable_value = get_feature_variable_for_variation(flag_key, feature_enabled, variation, variable, user_id)
all_variables[variable['key']] = Helpers::VariableType.cast_value_to_type(variable_value, variable['type'], @logger)
end
end
@@ -281,6 +235,47 @@ def decide(user_context, key, decide_options = [])
)
end
+ def decide(user_context, key, decide_options = [])
+ # raising on user context as it is internal and not provided directly by the user.
+ raise if user_context.class != OptimizelyUserContext
+
+ reasons = []
+
+ # check if SDK is ready
+ unless is_valid
+ @logger.log(Logger::ERROR, InvalidProjectConfigError.new('decide').message)
+ reasons.push(OptimizelyDecisionMessage::SDK_NOT_READY)
+ return OptimizelyDecision.new(flag_key: key, user_context: user_context, reasons: reasons)
+ end
+
+ # validate that key is a string
+ unless key.is_a?(String)
+ @logger.log(Logger::ERROR, 'Provided key is invalid')
+ reasons.push(format(OptimizelyDecisionMessage::FLAG_KEY_INVALID, key))
+ return OptimizelyDecision.new(flag_key: key, user_context: user_context, reasons: reasons)
+ end
+
+ # validate that key maps to a feature flag
+ config = project_config
+ feature_flag = config.get_feature_flag_from_key(key)
+ unless feature_flag
+ @logger.log(Logger::ERROR, "No feature flag was found for key '#{key}'.")
+ reasons.push(format(OptimizelyDecisionMessage::FLAG_KEY_INVALID, key))
+ return OptimizelyDecision.new(flag_key: key, user_context: user_context, reasons: reasons)
+ end
+
+ # merge decide_options and default_decide_options
+ if decide_options.is_a? Array
+ decide_options += @default_decide_options
+ else
+ @logger.log(Logger::DEBUG, 'Provided decide options is not an array. Using default decide options.')
+ decide_options = @default_decide_options
+ end
+
+ decide_options.delete(OptimizelyDecideOption::ENABLED_FLAGS_ONLY) if decide_options.include?(OptimizelyDecideOption::ENABLED_FLAGS_ONLY)
+ decide_for_keys(user_context, [key], decide_options, true)[key]
+ end
+
def decide_all(user_context, decide_options = [])
# raising on user context as it is internal and not provided directly by the user.
raise if user_context.class != OptimizelyUserContext
@@ -298,7 +293,7 @@ def decide_all(user_context, decide_options = [])
decide_for_keys(user_context, keys, decide_options)
end
- def decide_for_keys(user_context, keys, decide_options = [])
+ def decide_for_keys(user_context, keys, decide_options = [], ignore_default_options = false) # rubocop:disable Style/OptionalBooleanParameter
# raising on user context as it is internal and not provided directly by the user.
raise if user_context.class != OptimizelyUserContext
@@ -308,13 +303,79 @@ def decide_for_keys(user_context, keys, decide_options = [])
return {}
end
- enabled_flags_only = (!decide_options.nil? && (decide_options.include? OptimizelyDecideOption::ENABLED_FLAGS_ONLY)) || (@default_decide_options.include? OptimizelyDecideOption::ENABLED_FLAGS_ONLY)
+ # merge decide_options and default_decide_options
+ unless ignore_default_options
+ if decide_options.is_a?(Array)
+ decide_options += @default_decide_options
+ else
+ @logger.log(Logger::DEBUG, 'Provided decide options is not an array. Using default decide options.')
+ decide_options = @default_decide_options
+ end
+ end
+
+ # enabled_flags_only = (!decide_options.nil? && (decide_options.include? OptimizelyDecideOption::ENABLED_FLAGS_ONLY)) || (@default_decide_options.include? OptimizelyDecideOption::ENABLED_FLAGS_ONLY)
decisions = {}
+ valid_keys = []
+ decision_reasons_dict = {}
+ config = project_config
+ return decisions unless config
+
+ flags_without_forced_decision = []
+ flag_decisions = {}
+
keys.each do |key|
- decision = decide(user_context, key, decide_options)
- decisions[key] = decision unless enabled_flags_only && !decision.enabled
+ # Retrieve the feature flag from the project's feature flag key map
+ feature_flag = config.feature_flag_key_map[key]
+
+ # If the feature flag is nil, create a default OptimizelyDecision and move to the next key
+ if feature_flag.nil?
+ decisions[key] = OptimizelyDecision.new(nil, false, nil, nil, key, user_context, [])
+ next
+ end
+ valid_keys.push(key)
+ decision_reasons = []
+ decision_reasons_dict[key] = decision_reasons
+
+ config = project_config
+ context = Optimizely::OptimizelyUserContext::OptimizelyDecisionContext.new(key, nil)
+ variation, reasons_received = @decision_service.validated_forced_decision(config, context, user_context)
+ decision_reasons_dict[key].push(*reasons_received)
+ if variation
+ decision = Optimizely::DecisionService::Decision.new(nil, variation, Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST'])
+ flag_decisions[key] = decision
+ else
+ flags_without_forced_decision.push(feature_flag)
+ end
end
+ decision_list = @decision_service.get_variations_for_feature_list(config, flags_without_forced_decision, user_context, decide_options)
+
+ flags_without_forced_decision.each_with_index do |flag, i|
+ decision = decision_list[i][0]
+ reasons = decision_list[i][1]
+ flag_key = flag['key']
+ flag_decisions[flag_key] = decision
+ decision_reasons_dict[flag_key] ||= []
+ decision_reasons_dict[flag_key].push(*reasons)
+ end
+ valid_keys.each do |key|
+ flag_decision = flag_decisions[key]
+ decision_reasons = decision_reasons_dict[key]
+ optimizely_decision = create_optimizely_decision(
+ user_context,
+ key,
+ flag_decision,
+ decision_reasons,
+ decide_options,
+ config
+ )
+
+ enabled_flags_only_missing = !decide_options.include?(OptimizelyDecideOption::ENABLED_FLAGS_ONLY)
+ is_enabled = optimizely_decision.enabled
+
+ decisions[key] = optimizely_decision if enabled_flags_only_missing || is_enabled
+ end
+
decisions
end
@@ -959,7 +1020,10 @@ def get_variation_with_config(experiment_key, user_id, attributes, config)
return nil unless user_inputs_valid?(attributes)
user_context = OptimizelyUserContext.new(self, user_id, attributes, identify: false)
- variation_id, = @decision_service.get_variation(config, experiment_id, user_context)
+ user_profile_tracker = UserProfileTracker.new(user_id, @user_profile_service, @logger)
+ user_profile_tracker.load_user_profile
+ variation_id, = @decision_service.get_variation(config, experiment_id, user_context, user_profile_tracker)
+ user_profile_tracker.save_user_profile
variation = config.get_variation_from_id(experiment_key, variation_id) unless variation_id.nil?
variation_key = variation['key'] if variation
decision_notification_type = if config.feature_experiment?(experiment_id)
diff --git a/lib/optimizely/decision_service.rb b/lib/optimizely/decision_service.rb
index 3dbbf1d0..3303907d 100644
--- a/lib/optimizely/decision_service.rb
+++ b/lib/optimizely/decision_service.rb
@@ -52,17 +52,20 @@ def initialize(logger, user_profile_service = nil)
@forced_variation_map = {}
end
- def get_variation(project_config, experiment_id, user_context, decide_options = [])
+ def get_variation(project_config, experiment_id, user_context, user_profile_tracker = nil, decide_options = [], reasons = [])
# Determines variation into which user will be bucketed.
#
# project_config - project_config - Instance of ProjectConfig
# experiment_id - Experiment for which visitor variation needs to be determined
# user_context - Optimizely user context instance
+ # user_profile_tracker: Tracker for reading and updating user profile of the user.
+ # reasons: Decision reasons.
#
# Returns variation ID where visitor will be bucketed
# (nil if experiment is inactive or user does not meet audience conditions)
-
+ user_profile_tracker = UserProfileTracker.new(user_context.user_id, @user_profile_service, @logger) unless user_profile_tracker.is_a?(Optimizely::UserProfileTracker)
decide_reasons = []
+ decide_reasons.push(*reasons)
user_id = user_context.user_id
attributes = user_context.user_attributes
# By default, the bucketing ID should be the user ID
@@ -92,10 +95,8 @@ def get_variation(project_config, experiment_id, user_context, decide_options =
should_ignore_user_profile_service = decide_options.include? Optimizely::Decide::OptimizelyDecideOption::IGNORE_USER_PROFILE_SERVICE
# Check for saved bucketing decisions if decide_options do not include ignoreUserProfileService
- unless should_ignore_user_profile_service
- user_profile, reasons_received = get_user_profile(user_id)
- decide_reasons.push(*reasons_received)
- saved_variation_id, reasons_received = get_saved_variation_id(project_config, experiment_id, user_profile)
+ unless should_ignore_user_profile_service && user_profile_tracker
+ saved_variation_id, reasons_received = get_saved_variation_id(project_config, experiment_id, user_profile_tracker.user_profile)
decide_reasons.push(*reasons_received)
if saved_variation_id
message = "Returning previously activated variation ID #{saved_variation_id} of experiment '#{experiment_key}' for user '#{user_id}' from user profile."
@@ -131,7 +132,7 @@ def get_variation(project_config, experiment_id, user_context, decide_options =
decide_reasons.push(message)
# Persist bucketing decision
- save_user_profile(user_profile, experiment_id, variation_id) unless should_ignore_user_profile_service
+ user_profile_tracker.update_user_profile(experiment_id, variation_id) unless should_ignore_user_profile_service && user_profile_tracker
[variation_id, decide_reasons]
end
@@ -143,21 +144,46 @@ def get_variation_for_feature(project_config, feature_flag, user_context, decide
# user_context - Optimizely user context instance
#
# Returns Decision struct (nil if the user is not bucketed into any of the experiments on the feature)
+ get_variations_for_feature_list(project_config, [feature_flag], user_context, decide_options).first
+ end
- decide_reasons = []
-
- # check if the feature is being experiment on and whether the user is bucketed into the experiment
- decision, reasons_received = get_variation_for_feature_experiment(project_config, feature_flag, user_context, decide_options)
- decide_reasons.push(*reasons_received)
- return decision, decide_reasons unless decision.nil?
-
- decision, reasons_received = get_variation_for_feature_rollout(project_config, feature_flag, user_context)
- decide_reasons.push(*reasons_received)
-
- [decision, decide_reasons]
+ def get_variations_for_feature_list(project_config, feature_flags, user_context, decide_options = [])
+ # Returns the list of experiment/variation the user is bucketed in for the given list of features.
+ #
+ # Args:
+ # project_config: Instance of ProjectConfig.
+ # feature_flags: Array of features for which we are determining if it is enabled or not for the given user.
+ # user_context: User context for user.
+ # decide_options: Decide options.
+ #
+ # Returns:
+ # Array of Decision struct.
+ ignore_ups = decide_options.include? Optimizely::Decide::OptimizelyDecideOption::IGNORE_USER_PROFILE_SERVICE
+ user_profile_tracker = nil
+ unless ignore_ups && @user_profile_service
+ user_profile_tracker = UserProfileTracker.new(user_context.user_id, @user_profile_service, @logger)
+ user_profile_tracker.load_user_profile
+ end
+ decisions = []
+ feature_flags.each do |feature_flag|
+ decide_reasons = []
+ # check if the feature is being experiment on and whether the user is bucketed into the experiment
+ decision, reasons_received = get_variation_for_feature_experiment(project_config, feature_flag, user_context, user_profile_tracker, decide_options)
+ decide_reasons.push(*reasons_received)
+ if decision
+ decisions << [decision, decide_reasons]
+ else
+ # Proceed to rollout if the decision is nil
+ rollout_decision, reasons_received = get_variation_for_feature_rollout(project_config, feature_flag, user_context)
+ decide_reasons.push(*reasons_received)
+ decisions << [rollout_decision, decide_reasons]
+ end
+ end
+ user_profile_tracker&.save_user_profile
+ decisions
end
- def get_variation_for_feature_experiment(project_config, feature_flag, user_context, decide_options = [])
+ def get_variation_for_feature_experiment(project_config, feature_flag, user_context, user_profile_tracker, decide_options = [])
# Gets the variation the user is bucketed into for the feature flag's experiment.
#
# project_config - project_config - Instance of ProjectConfig
@@ -187,7 +213,7 @@ def get_variation_for_feature_experiment(project_config, feature_flag, user_cont
end
experiment_id = experiment['id']
- variation_id, reasons_received = get_variation_from_experiment_rule(project_config, feature_flag_key, experiment, user_context, decide_options)
+ variation_id, reasons_received = get_variation_from_experiment_rule(project_config, feature_flag_key, experiment, user_context, user_profile_tracker, decide_options)
decide_reasons.push(*reasons_received)
next unless variation_id
@@ -252,7 +278,7 @@ def get_variation_for_feature_rollout(project_config, feature_flag, user_context
[nil, decide_reasons]
end
- def get_variation_from_experiment_rule(project_config, flag_key, rule, user, options = [])
+ def get_variation_from_experiment_rule(project_config, flag_key, rule, user, user_profile_tracker, options = [])
# Determine which variation the user is in for a given rollout.
# Returns the variation from experiment rules.
#
@@ -270,7 +296,7 @@ def get_variation_from_experiment_rule(project_config, flag_key, rule, user, opt
return [variation['id'], reasons] if variation
- variation_id, response_reasons = get_variation(project_config, rule['id'], user, options)
+ variation_id, response_reasons = get_variation(project_config, rule['id'], user, user_profile_tracker, options)
reasons.push(*response_reasons)
[variation_id, reasons]
diff --git a/lib/optimizely/helpers/validator.rb b/lib/optimizely/helpers/validator.rb
index d3baa447..4d975483 100644
--- a/lib/optimizely/helpers/validator.rb
+++ b/lib/optimizely/helpers/validator.rb
@@ -122,11 +122,11 @@ def inputs_valid?(variables, logger = NoOpLogger.new, level = Logger::ERROR)
return false unless variables.respond_to?(:each) && !variables.empty?
- is_valid = true
+ is_valid = true # rubocop:disable Lint/UselessAssignment
if variables.include? :user_id
# Empty str is a valid user ID.
unless variables[:user_id].is_a?(String)
- is_valid = false
+ is_valid = false # rubocop:disable Lint/UselessAssignment
logger.log(level, "#{Constants::INPUT_VARIABLES['USER_ID']} is invalid")
end
variables.delete :user_id
diff --git a/lib/optimizely/optimizely_factory.rb b/lib/optimizely/optimizely_factory.rb
index 04c7ecdd..717e43d9 100644
--- a/lib/optimizely/optimizely_factory.rb
+++ b/lib/optimizely/optimizely_factory.rb
@@ -142,7 +142,6 @@ def self.custom_instance( # rubocop:disable Metrics/ParameterLists
notification_center = nil,
settings = nil
)
-
error_handler ||= NoOpErrorHandler.new
logger ||= NoOpLogger.new
notification_center = notification_center.is_a?(Optimizely::NotificationCenter) ? notification_center : NotificationCenter.new(logger, error_handler)
diff --git a/lib/optimizely/user_profile_tracker.rb b/lib/optimizely/user_profile_tracker.rb
new file mode 100644
index 00000000..082576b0
--- /dev/null
+++ b/lib/optimizely/user_profile_tracker.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require_relative 'logger'
+
+module Optimizely
+ class UserProfileTracker
+ attr_reader :user_profile
+
+ def initialize(user_id, user_profile_service = nil, logger = nil)
+ @user_id = user_id
+ @user_profile_service = user_profile_service
+ @logger = logger || NoOpLogger.new
+ @profile_updated = false
+ @user_profile = {
+ user_id: user_id,
+ experiment_bucket_map: {}
+ }
+ end
+
+ def load_user_profile(reasons = [], error_handler = nil)
+ return if reasons.nil?
+
+ begin
+ @user_profile = @user_profile_service.lookup(@user_id) if @user_profile_service
+ if @user_profile.nil?
+ @user_profile = {
+ user_id: @user_id,
+ experiment_bucket_map: {}
+ }
+ end
+ rescue => e
+ message = "Error while looking up user profile for user ID '#{@user_id}': #{e}."
+ reasons << message
+ @logger.log(Logger::ERROR, message)
+ error_handler&.handle_error(e)
+ end
+ end
+
+ def update_user_profile(experiment_id, variation_id)
+ user_id = @user_profile[:user_id]
+ begin
+ @user_profile[:experiment_bucket_map][experiment_id] = {
+ variation_id: variation_id
+ }
+ @profile_updated = true
+ @logger.log(Logger::INFO, "Updated variation ID #{variation_id} of experiment ID #{experiment_id} for user '#{user_id}'.")
+ rescue => e
+ @logger.log(Logger::ERROR, "Error while updating user profile for user ID '#{user_id}': #{e}.")
+ end
+ end
+
+ def save_user_profile(error_handler = nil)
+ return unless @profile_updated && @user_profile_service
+
+ begin
+ @user_profile_service.save(@user_profile)
+ @logger.log(Logger::INFO, "Saved user profile for user '#{@user_profile[:user_id]}'.")
+ rescue => e
+ @logger.log(Logger::ERROR, "Failed to save user profile for user '#{@user_profile[:user_id]}': #{e}.")
+ error_handler&.handle_error(e)
+ end
+ end
+ end
+end
diff --git a/spec/decision_service_spec.rb b/spec/decision_service_spec.rb
index 10f58792..af22b18b 100644
--- a/spec/decision_service_spec.rb
+++ b/spec/decision_service_spec.rb
@@ -73,7 +73,8 @@
it 'should return the correct variation ID for a given user ID and key of a running experiment' do
user_context = project_instance.create_user_context('test_user')
- variation_received, reasons = decision_service.get_variation(config, '111127', user_context)
+ user_profile_tracker = Optimizely::UserProfileTracker.new(user_context.user_id)
+ variation_received, reasons = decision_service.get_variation(config, '111127', user_context, user_profile_tracker)
expect(variation_received).to eq('111128')
expect(reasons).to eq([
@@ -90,7 +91,8 @@
it 'should return nil when user ID is not bucketed' do
allow(decision_service.bucketer).to receive(:bucket).and_return(nil)
user_context = project_instance.create_user_context('test_user')
- variation_received, reasons = decision_service.get_variation(config, '111127', user_context)
+ user_profile_tracker = Optimizely::UserProfileTracker.new(user_context.user_id)
+ variation_received, reasons = decision_service.get_variation(config, '111127', user_context, user_profile_tracker)
expect(variation_received).to eq(nil)
expect(reasons).to eq([
"Audiences for experiment 'test_experiment' collectively evaluated to TRUE.",
@@ -189,7 +191,8 @@
it 'should return nil if the user does not meet the audience conditions for a given experiment' do
user_attributes = {'browser_type' => 'chrome'}
user_context = project_instance.create_user_context('test_user', user_attributes)
- variation_received, reasons = decision_service.get_variation(config, '122227', user_context)
+ user_profile_tracker = Optimizely::UserProfileTracker.new(user_context.user_id)
+ variation_received, reasons = decision_service.get_variation(config, '122227', user_context, user_profile_tracker)
expect(variation_received).to eq(nil)
expect(reasons).to eq([
"Starting to evaluate audience '11154' with conditions: [\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", \"type\": \"custom_attribute\", \"value\": \"firefox\"}]]].",
@@ -240,7 +243,8 @@
it 'should bucket normally if user is whitelisted into a forced variation that is not in the datafile' do
user_context = project_instance.create_user_context('forced_user_with_invalid_variation')
- variation_received, reasons = decision_service.get_variation(config, '111127', user_context)
+ user_profile_tracker = Optimizely::UserProfileTracker.new(user_context.user_id)
+ variation_received, reasons = decision_service.get_variation(config, '111127', user_context, user_profile_tracker)
expect(variation_received).to eq('111128')
expect(reasons).to eq([
"User 'forced_user_with_invalid_variation' is whitelisted into variation 'invalid_variation', which is not in the datafile.",
@@ -259,50 +263,14 @@
end
describe 'when a UserProfile service is provided' do
- it 'should look up the UserProfile, bucket normally, and save the result if no saved profile is found' do
- expected_user_profile = {
- user_id: 'test_user',
- experiment_bucket_map: {
- '111127' => {
- variation_id: '111128'
- }
- }
- }
- expect(spy_user_profile_service).to receive(:lookup).once.and_return(nil)
-
- user_context = project_instance.create_user_context('test_user')
- variation_received, reasons = decision_service.get_variation(config, '111127', user_context)
- expect(variation_received).to eq('111128')
- expect(reasons).to eq([
- "Audiences for experiment 'test_experiment' collectively evaluated to TRUE.",
- "User 'test_user' is in variation 'control' of experiment '111127'."
- ])
-
- # bucketing should have occurred
- expect(decision_service.bucketer).to have_received(:bucket).once
- # bucketing decision should have been saved
- expect(spy_user_profile_service).to have_received(:save).once.with(expected_user_profile)
- expect(spy_logger).to have_received(:log).once
- .with(Logger::INFO, "Saved variation ID 111128 of experiment ID 111127 for user 'test_user'.")
- end
-
- it 'should look up the UserProfile, bucket normally (using Bucketing ID attribute), and save the result if no saved profile is found' do
- expected_user_profile = {
- user_id: 'test_user',
- experiment_bucket_map: {
- '111127' => {
- variation_id: '111129'
- }
- }
- }
+ it 'bucket normally (using Bucketing ID attribute)' do
user_attributes = {
'browser_type' => 'firefox',
Optimizely::Helpers::Constants::CONTROL_ATTRIBUTES['BUCKETING_ID'] => 'pid'
}
- expect(spy_user_profile_service).to receive(:lookup).once.and_return(nil)
-
user_context = project_instance.create_user_context('test_user', user_attributes)
- variation_received, reasons = decision_service.get_variation(config, '111127', user_context)
+ user_profile_tracker = Optimizely::UserProfileTracker.new(user_context.user_id, spy_user_profile_service, spy_logger)
+ variation_received, reasons = decision_service.get_variation(config, '111127', user_context, user_profile_tracker)
expect(variation_received).to eq('111129')
expect(reasons).to eq([
"Audiences for experiment 'test_experiment' collectively evaluated to TRUE.",
@@ -311,13 +279,9 @@
# bucketing should have occurred
expect(decision_service.bucketer).to have_received(:bucket).once
- # bucketing decision should have been saved
- expect(spy_user_profile_service).to have_received(:save).once.with(expected_user_profile)
- expect(spy_logger).to have_received(:log).once
- .with(Logger::INFO, "Saved variation ID 111129 of experiment ID 111127 for user 'test_user'.")
end
- it 'should look up the user profile and skip normal bucketing if a profile with a saved decision is found' do
+ it 'skip normal bucketing if a profile with a saved decision is found' do
saved_user_profile = {
user_id: 'test_user',
experiment_bucket_map: {
@@ -330,7 +294,9 @@
.with('test_user').once.and_return(saved_user_profile)
user_context = project_instance.create_user_context('test_user')
- variation_received, reasons = decision_service.get_variation(config, '111127', user_context)
+ user_profile_tracker = Optimizely::UserProfileTracker.new(user_context.user_id, spy_user_profile_service, spy_logger)
+ user_profile_tracker.load_user_profile
+ variation_received, reasons = decision_service.get_variation(config, '111127', user_context, user_profile_tracker)
expect(variation_received).to eq('111129')
expect(reasons).to eq([
"Returning previously activated variation ID 111129 of experiment 'test_experiment' for user 'test_user' from user profile."
@@ -346,7 +312,7 @@
expect(spy_user_profile_service).not_to have_received(:save)
end
- it 'should look up the user profile and bucket normally if a profile without a saved decision is found' do
+ it 'bucket normally if a profile without a saved decision is found' do
saved_user_profile = {
user_id: 'test_user',
experiment_bucket_map: {
@@ -360,7 +326,9 @@
.once.with('test_user').and_return(saved_user_profile)
user_context = project_instance.create_user_context('test_user')
- variation_received, reasons = decision_service.get_variation(config, '111127', user_context)
+ user_profile_tracker = Optimizely::UserProfileTracker.new(user_context.user_id, spy_user_profile_service, spy_logger)
+ user_profile_tracker.load_user_profile
+ variation_received, reasons = decision_service.get_variation(config, '111127', user_context, user_profile_tracker)
expect(variation_received).to eq('111128')
expect(reasons).to eq([
"Audiences for experiment 'test_experiment' collectively evaluated to TRUE.",
@@ -369,20 +337,6 @@
# bucketing should have occurred
expect(decision_service.bucketer).to have_received(:bucket).once
-
- # user profile should have been updated with bucketing decision
- expected_user_profile = {
- user_id: 'test_user',
- experiment_bucket_map: {
- '111127' => {
- variation_id: '111128'
- },
- '122227' => {
- variation_id: '122228'
- }
- }
- }
- expect(spy_user_profile_service).to have_received(:save).once.with(expected_user_profile)
end
it 'should bucket normally if the user profile contains a variation ID not in the datafile' do
@@ -399,7 +353,9 @@
.once.with('test_user').and_return(saved_user_profile)
user_context = project_instance.create_user_context('test_user')
- variation_received, reasons = decision_service.get_variation(config, '111127', user_context)
+ user_profile_tracker = Optimizely::UserProfileTracker.new(user_context.user_id, spy_user_profile_service, spy_logger)
+ user_profile_tracker.load_user_profile
+ variation_received, reasons = decision_service.get_variation(config, '111127', user_context, user_profile_tracker)
expect(variation_received).to eq('111128')
expect(reasons).to eq([
"User 'test_user' was previously bucketed into variation ID '111111' for experiment '111127', but no matching variation was found. Re-bucketing user.",
@@ -409,27 +365,18 @@
# bucketing should have occurred
expect(decision_service.bucketer).to have_received(:bucket).once
-
- # user profile should have been updated with bucketing decision
- expected_user_profile = {
- user_id: 'test_user',
- experiment_bucket_map: {
- '111127' => {
- variation_id: '111128'
- }
- }
- }
- expect(spy_user_profile_service).to have_received(:save).with(expected_user_profile)
end
- it 'should bucket normally if the user profile service throws an error during lookup' do
+ it 'should bucket normally if the user profile tracker throws an error during lookup' do
expect(spy_user_profile_service).to receive(:lookup).once.with('test_user').and_throw(:LookupError)
user_context = project_instance.create_user_context('test_user')
- variation_received, reasons = decision_service.get_variation(config, '111127', user_context)
+ user_profile_tracker = Optimizely::UserProfileTracker.new(user_context.user_id, spy_user_profile_service, spy_logger)
+ user_profile_tracker.load_user_profile
+ variation_received, reasons = decision_service.get_variation(config, '111127', user_context, user_profile_tracker)
+ user_profile_tracker.save_user_profile
expect(variation_received).to eq('111128')
expect(reasons).to eq([
- "Error while looking up user profile for user ID 'test_user': uncaught throw :LookupError.",
"Audiences for experiment 'test_experiment' collectively evaluated to TRUE.",
"User 'test_user' is in variation 'control' of experiment '111127'."
])
@@ -440,46 +387,15 @@
expect(decision_service.bucketer).to have_received(:bucket).once
end
- it 'should log an error if the user profile service throws an error during save' do
- expect(spy_user_profile_service).to receive(:save).once.and_throw(:SaveError)
-
- user_context = project_instance.create_user_context('test_user')
- variation_received, reasons = decision_service.get_variation(config, '111127', user_context)
- expect(variation_received).to eq('111128')
- expect(reasons).to eq([
- "Audiences for experiment 'test_experiment' collectively evaluated to TRUE.",
- "User 'test_user' is in variation 'control' of experiment '111127'."
- ])
-
- expect(spy_logger).to have_received(:log).once
- .with(Logger::ERROR, "Error while saving user profile for user ID 'test_user': uncaught throw :SaveError.")
- end
-
describe 'IGNORE_USER_PROFILE_SERVICE decide option' do
it 'should ignore user profile service if this option is set' do
allow(spy_user_profile_service).to receive(:lookup)
.with('test_user').once.and_return(nil)
user_context = project_instance.create_user_context('test_user', nil)
- variation_received, reasons = decision_service.get_variation(config, '111127', user_context, [Optimizely::Decide::OptimizelyDecideOption::IGNORE_USER_PROFILE_SERVICE])
- expect(variation_received).to eq('111128')
- expect(reasons).to eq([
- "Audiences for experiment 'test_experiment' collectively evaluated to TRUE.",
- "User 'test_user' is in variation 'control' of experiment '111127'."
- ])
-
- expect(decision_service.bucketer).to have_received(:bucket)
- expect(Optimizely::Audience).to have_received(:user_meets_audience_conditions?)
- expect(spy_user_profile_service).not_to have_received(:lookup)
- expect(spy_user_profile_service).not_to have_received(:save)
- end
-
- it 'should not ignore user profile service if this option is not set' do
- allow(spy_user_profile_service).to receive(:lookup)
- .with('test_user').once.and_return(nil)
-
- user_context = project_instance.create_user_context('test_user')
- variation_received, reasons = decision_service.get_variation(config, '111127', user_context)
+ user_profile_tracker = Optimizely::UserProfileTracker.new(user_context.user_id, spy_user_profile_service, spy_logger)
+ user_profile_tracker.load_user_profile
+ variation_received, reasons = decision_service.get_variation(config, '111127', user_context, user_profile_tracker, [Optimizely::Decide::OptimizelyDecideOption::IGNORE_USER_PROFILE_SERVICE])
expect(variation_received).to eq('111128')
expect(reasons).to eq([
"Audiences for experiment 'test_experiment' collectively evaluated to TRUE.",
@@ -488,8 +404,6 @@
expect(decision_service.bucketer).to have_received(:bucket)
expect(Optimizely::Audience).to have_received(:user_meets_audience_conditions?)
- expect(spy_user_profile_service).to have_received(:lookup)
- expect(spy_user_profile_service).to have_received(:save)
end
end
end
@@ -499,11 +413,11 @@
config_body_json = OptimizelySpec::VALID_CONFIG_BODY_JSON
project_instance = Optimizely::Project.new(datafile: config_body_json)
user_context = project_instance.create_user_context('user_1', {})
-
+ user_profile_tracker = Optimizely::UserProfileTracker.new(user_context.user_id)
describe 'when the feature flag\'s experiment ids array is empty' do
it 'should return nil and log a message' do
feature_flag = config.feature_flag_key_map['empty_feature']
- variation_received, reasons = decision_service.get_variation_for_feature_experiment(config, feature_flag, user_context)
+ variation_received, reasons = decision_service.get_variation_for_feature_experiment(config, feature_flag, user_context, user_profile_tracker)
expect(variation_received).to eq(nil)
expect(reasons).to eq(["The feature flag 'empty_feature' is not used in any experiments."])
@@ -517,7 +431,8 @@
feature_flag = config.feature_flag_key_map['boolean_feature'].dup
# any string that is not an experiment id in the data file
feature_flag['experimentIds'] = ['1333333337']
- variation_received, reasons = decision_service.get_variation_for_feature_experiment(config, feature_flag, user_context)
+ user_profile_tracker = Optimizely::UserProfileTracker.new(user_context.user_id)
+ variation_received, reasons = decision_service.get_variation_for_feature_experiment(config, feature_flag, user_context, user_profile_tracker)
expect(variation_received).to eq(nil)
expect(reasons).to eq(["Feature flag experiment with ID '1333333337' is not in the datafile."])
expect(spy_logger).to have_received(:log).once
@@ -526,19 +441,19 @@
end
describe 'when the feature flag is associated with a non-mutex experiment' do
+ user_profile_tracker = Optimizely::UserProfileTracker.new(user_context.user_id)
describe 'and the user is not bucketed into the feature flag\'s experiments' do
before(:each) do
multivariate_experiment = config.experiment_key_map['test_experiment_multivariate']
-
# make sure the user is not bucketed into the feature experiment
allow(decision_service).to receive(:get_variation)
- .with(config, multivariate_experiment['id'], user_context, [])
+ .with(config, multivariate_experiment['id'], user_context, user_profile_tracker, [])
.and_return([nil, nil])
end
it 'should return nil and log a message' do
feature_flag = config.feature_flag_key_map['multi_variate_feature']
- variation_received, reasons = decision_service.get_variation_for_feature_experiment(config, feature_flag, user_context, [])
+ variation_received, reasons = decision_service.get_variation_for_feature_experiment(config, feature_flag, user_context, user_profile_tracker, [])
expect(variation_received).to eq(nil)
expect(reasons).to eq(["The user 'user_1' is not bucketed into any of the experiments on the feature 'multi_variate_feature'."])
@@ -560,8 +475,8 @@
config.variation_id_map['test_experiment_multivariate']['122231'],
Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
)
-
- variation_received, reasons = decision_service.get_variation_for_feature_experiment(config, feature_flag, user_context)
+ user_profile_tracker = Optimizely::UserProfileTracker.new(user_context.user_id)
+ variation_received, reasons = decision_service.get_variation_for_feature_experiment(config, feature_flag, user_context, user_profile_tracker)
expect(variation_received).to eq(expected_decision)
expect(reasons).to eq([])
end
@@ -586,27 +501,29 @@
it 'should return the variation the user is bucketed into' do
feature_flag = config.feature_flag_key_map['mutex_group_feature']
- variation_received, reasons = decision_service.get_variation_for_feature_experiment(config, feature_flag, user_context)
+ user_profile_tracker = Optimizely::UserProfileTracker.new(user_context.user_id)
+ variation_received, reasons = decision_service.get_variation_for_feature_experiment(config, feature_flag, user_context, user_profile_tracker)
expect(variation_received).to eq(expected_decision)
expect(reasons).to eq([])
end
end
describe 'and the user is not bucketed into any of the mutex experiments' do
+ user_profile_tracker = Optimizely::UserProfileTracker.new(user_context.user_id)
before(:each) do
mutex_exp = config.experiment_key_map['group1_exp1']
mutex_exp2 = config.experiment_key_map['group1_exp2']
allow(decision_service).to receive(:get_variation)
- .with(config, mutex_exp['id'], user_context, [])
+ .with(config, mutex_exp['id'], user_context, user_profile_tracker, [])
.and_return([nil, nil])
allow(decision_service).to receive(:get_variation)
- .with(config, mutex_exp2['id'], user_context, [])
+ .with(config, mutex_exp2['id'], user_context, user_profile_tracker, [])
.and_return([nil, nil])
end
it 'should return nil and log a message' do
feature_flag = config.feature_flag_key_map['mutex_group_feature']
- variation_received, reasons = decision_service.get_variation_for_feature_experiment(config, feature_flag, user_context)
+ variation_received, reasons = decision_service.get_variation_for_feature_experiment(config, feature_flag, user_context, user_profile_tracker)
expect(variation_received).to eq(nil)
expect(reasons).to eq(["The user 'user_1' is not bucketed into any of the experiments on the feature 'mutex_group_feature'."])
diff --git a/spec/project_spec.rb b/spec/project_spec.rb
index 2c1aeaca..7c02f765 100644
--- a/spec/project_spec.rb
+++ b/spec/project_spec.rb
@@ -3766,7 +3766,9 @@ def callback(_args); end
variation_to_return,
Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
)
- allow(project_instance.decision_service).to receive(:get_variation_for_feature).and_return(decision_to_return)
+ decision_list_to_be_returned = []
+ decision_list_to_be_returned << [decision_to_return, []]
+ allow(project_instance.decision_service).to receive(:get_variations_for_feature_list).and_return(decision_list_to_be_returned)
user_context = project_instance.create_user_context('user1')
decision = project_instance.decide(user_context, 'multi_variate_feature')
expect(decision.as_json).to include(
@@ -3807,7 +3809,9 @@ def callback(_args); end
variation_to_return,
Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT']
)
- allow(project_instance.decision_service).to receive(:get_variation_for_feature).and_return(decision_to_return)
+ decision_list_to_be_returned = []
+ decision_list_to_be_returned << [decision_to_return, []]
+ allow(project_instance.decision_service).to receive(:get_variations_for_feature_list).and_return(decision_list_to_be_returned)
user_context = project_instance.create_user_context('user1')
decision = project_instance.decide(user_context, 'multi_variate_feature')
@@ -3888,7 +3892,8 @@ def callback(_args); end
variation_to_return,
Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT']
)
- allow(project_instance.decision_service).to receive(:get_variation_for_feature).and_return(decision_to_return)
+ decision_list_to_return = [[decision_to_return, []]]
+ allow(project_instance.decision_service).to receive(:get_variations_for_feature_list).and_return(decision_list_to_return)
user_context = project_instance.create_user_context('user1')
decision = project_instance.decide(user_context, 'multi_variate_feature')
expect(decision.as_json).to include(
@@ -4055,8 +4060,9 @@ def callback(_args); end
variation_to_return,
Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
)
+ decision_list_to_be_returned = [[decision_to_return, []]]
allow(project_instance.event_dispatcher).to receive(:dispatch_event).with(instance_of(Optimizely::Event))
- allow(project_instance.decision_service).to receive(:get_variation_for_feature).and_return(decision_to_return)
+ allow(project_instance.decision_service).to receive(:get_variations_for_feature_list).and_return(decision_list_to_be_returned)
user_context = project_instance.create_user_context('user1')
decision = project_instance.decide(user_context, 'multi_variate_feature', [Optimizely::Decide::OptimizelyDecideOption::EXCLUDE_VARIABLES])
expect(decision.as_json).to include(
@@ -4078,8 +4084,9 @@ def callback(_args); end
variation_to_return,
Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
)
+ decision_list_to_return = [[decision_to_return, []]]
allow(project_instance.event_dispatcher).to receive(:dispatch_event).with(instance_of(Optimizely::Event))
- allow(project_instance.decision_service).to receive(:get_variation_for_feature).and_return(decision_to_return)
+ allow(project_instance.decision_service).to receive(:get_variations_for_feature_list).and_return(decision_list_to_return)
user_context = project_instance.create_user_context('user1')
decision = project_instance.decide(user_context, 'multi_variate_feature')
expect(decision.as_json).to include(
@@ -4096,8 +4103,6 @@ def callback(_args); end
describe 'INCLUDE_REASONS' do
it 'should include reasons when the option is set' do
- expect(project_instance.notification_center).to receive(:send_notifications)
- .once.with(Optimizely::NotificationCenter::NOTIFICATION_TYPES[:LOG_EVENT], any_args)
expect(project_instance.notification_center).to receive(:send_notifications)
.once.with(
Optimizely::NotificationCenter::NOTIFICATION_TYPES[:DECISION],
@@ -4119,6 +4124,8 @@ def callback(_args); end
],
decision_event_dispatched: true
)
+ expect(project_instance.notification_center).to receive(:send_notifications)
+ .once.with(Optimizely::NotificationCenter::NOTIFICATION_TYPES[:LOG_EVENT], any_args)
allow(project_instance.event_dispatcher).to receive(:dispatch_event).with(instance_of(Optimizely::Event))
user_context = project_instance.create_user_context('user1')
decision = project_instance.decide(user_context, 'multi_variate_feature', [Optimizely::Decide::OptimizelyDecideOption::INCLUDE_REASONS])
@@ -4180,23 +4187,23 @@ def callback(_args); end
variation_to_return,
Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
)
+ decision_list_to_return = [[decision_to_return, []]]
allow(project_instance.event_dispatcher).to receive(:dispatch_event).with(instance_of(Optimizely::Event))
- allow(project_instance.decision_service).to receive(:get_variation_for_feature).and_return(decision_to_return)
+ allow(project_instance.decision_service).to receive(:get_variations_for_feature_list).and_return(decision_list_to_return)
user_context = project_instance.create_user_context('user1')
- expect(project_instance.decision_service).to receive(:get_variation_for_feature)
+ expect(project_instance.decision_service).to receive(:get_variations_for_feature_list)
.with(anything, anything, anything, []).once
project_instance.decide(user_context, 'multi_variate_feature')
- expect(project_instance.decision_service).to receive(:get_variation_for_feature)
+ expect(project_instance.decision_service).to receive(:get_variations_for_feature_list)
.with(anything, anything, anything, [Optimizely::Decide::OptimizelyDecideOption::DISABLE_DECISION_EVENT]).once
project_instance.decide(user_context, 'multi_variate_feature', [Optimizely::Decide::OptimizelyDecideOption::DISABLE_DECISION_EVENT])
- expect(project_instance.decision_service).to receive(:get_variation_for_feature)
+ expect(project_instance.decision_service).to receive(:get_variations_for_feature_list)
.with(anything, anything, anything, [
Optimizely::Decide::OptimizelyDecideOption::DISABLE_DECISION_EVENT,
Optimizely::Decide::OptimizelyDecideOption::EXCLUDE_VARIABLES,
- Optimizely::Decide::OptimizelyDecideOption::ENABLED_FLAGS_ONLY,
Optimizely::Decide::OptimizelyDecideOption::IGNORE_USER_PROFILE_SERVICE,
Optimizely::Decide::OptimizelyDecideOption::INCLUDE_REASONS,
Optimizely::Decide::OptimizelyDecideOption::EXCLUDE_VARIABLES
@@ -4400,8 +4407,9 @@ def callback(_args); end
variation_to_return,
Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
)
+ decision_list_to_return = [[decision_to_return, []]]
allow(custom_project_instance.event_dispatcher).to receive(:dispatch_event).with(instance_of(Optimizely::Event))
- allow(custom_project_instance.decision_service).to receive(:get_variation_for_feature).and_return(decision_to_return)
+ allow(custom_project_instance.decision_service).to receive(:get_variations_for_feature_list).and_return(decision_list_to_return)
user_context = custom_project_instance.create_user_context('user1')
decision = custom_project_instance.decide(user_context, 'multi_variate_feature')
expect(decision.as_json).to include(
@@ -4428,8 +4436,9 @@ def callback(_args); end
variation_to_return,
Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
)
+ decision_list_to_return = [[decision_to_return, []]]
allow(custom_project_instance.event_dispatcher).to receive(:dispatch_event).with(instance_of(Optimizely::Event))
- allow(custom_project_instance.decision_service).to receive(:get_variation_for_feature).and_return(decision_to_return)
+ allow(custom_project_instance.decision_service).to receive(:get_variations_for_feature_list).and_return(decision_list_to_return)
user_context = custom_project_instance.create_user_context('user1')
decision = custom_project_instance.decide(user_context, 'multi_variate_feature')
expect(decision.as_json).to include(
diff --git a/spec/user_profile_tracker_spec.rb b/spec/user_profile_tracker_spec.rb
new file mode 100644
index 00000000..85515bb1
--- /dev/null
+++ b/spec/user_profile_tracker_spec.rb
@@ -0,0 +1,101 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'rspec'
+
+RSpec.describe Optimizely::UserProfileTracker do
+ let(:user_id) { 'test_user' }
+ let(:mock_user_profile_service) { instance_double('UserProfileService') }
+ let(:mock_logger) { instance_double('Logger') }
+ let(:user_profile_tracker) { described_class.new(user_id, mock_user_profile_service, mock_logger) }
+
+ describe '#initialize' do
+ it 'initializes with a user ID and default values' do
+ tracker = described_class.new(user_id)
+ expect(tracker.user_profile[:user_id]).to eq(user_id)
+ expect(tracker.user_profile[:experiment_bucket_map]).to eq({})
+ end
+
+ it 'accepts a user profile service and logger' do
+ expect(user_profile_tracker.instance_variable_get(:@user_profile_service)).to eq(mock_user_profile_service)
+ expect(user_profile_tracker.instance_variable_get(:@logger)).to eq(mock_logger)
+ end
+ end
+
+ describe '#load_user_profile' do
+ it 'loads the user profile from the service if provided' do
+ expected_profile = {
+ user_id: user_id,
+ experiment_bucket_map: {'111127' => {variation_id: '111128'}}
+ }
+ allow(mock_user_profile_service).to receive(:lookup).with(user_id).and_return(expected_profile)
+ user_profile_tracker.load_user_profile
+ expect(user_profile_tracker.user_profile).to eq(expected_profile)
+ end
+
+ it 'handles errors during lookup and logs them' do
+ allow(mock_user_profile_service).to receive(:lookup).with(user_id).and_raise(StandardError.new('lookup error'))
+ allow(mock_logger).to receive(:log)
+
+ reasons = []
+ user_profile_tracker.load_user_profile(reasons)
+ expect(reasons).to include("Error while looking up user profile for user ID 'test_user': lookup error.")
+ expect(mock_logger).to have_received(:log).with(Logger::ERROR, "Error while looking up user profile for user ID 'test_user': lookup error.")
+ end
+
+ it 'does nothing if reasons array is nil' do
+ expect(mock_user_profile_service).not_to receive(:lookup)
+ user_profile_tracker.load_user_profile(nil)
+ end
+ end
+
+ describe '#update_user_profile' do
+ let(:experiment_id) { '111127' }
+ let(:variation_id) { '111128' }
+
+ before do
+ allow(mock_logger).to receive(:log)
+ end
+
+ it 'updates the experiment bucket map with the given experiment and variation IDs' do
+ user_profile_tracker.update_user_profile(experiment_id, variation_id)
+
+ # Verify the experiment and variation were added
+ expect(user_profile_tracker.user_profile[:experiment_bucket_map][experiment_id][:variation_id]).to eq(variation_id)
+ # Verify the profile_updated flag was set
+ expect(user_profile_tracker.instance_variable_get(:@profile_updated)).to eq(true)
+ # Verify a log message was recorded
+ expect(mock_logger).to have_received(:log).with(Logger::INFO, "Updated variation ID #{variation_id} of experiment ID #{experiment_id} for user 'test_user'.")
+ end
+ end
+
+ describe '#save_user_profile' do
+ it 'saves the user profile if updates were made and service is available' do
+ allow(mock_user_profile_service).to receive(:save)
+ allow(mock_logger).to receive(:log)
+
+ user_profile_tracker.update_user_profile('111127', '111128')
+ user_profile_tracker.save_user_profile
+
+ expect(mock_user_profile_service).to have_received(:save).with(user_profile_tracker.user_profile)
+ expect(mock_logger).to have_received(:log).with(Logger::INFO, "Saved user profile for user 'test_user'.")
+ end
+
+ it 'does not save the user profile if no updates were made' do
+ allow(mock_user_profile_service).to receive(:save)
+
+ user_profile_tracker.save_user_profile
+ expect(mock_user_profile_service).not_to have_received(:save)
+ end
+
+ it 'handles errors during save and logs them' do
+ allow(mock_user_profile_service).to receive(:save).and_raise(StandardError.new('save error'))
+ allow(mock_logger).to receive(:log)
+
+ user_profile_tracker.update_user_profile('111127', '111128')
+ user_profile_tracker.save_user_profile
+
+ expect(mock_logger).to have_received(:log).with(Logger::ERROR, "Failed to save user profile for user 'test_user': save error.")
+ end
+ end
+end
From cfdd02224c94de02fa8b6ccde3df830f0939aec9 Mon Sep 17 00:00:00 2001
From: Farhan Anjum
Date: Mon, 13 Jan 2025 20:14:02 +0600
Subject: [PATCH 17/18] [FSSDK-10847] chore: preparing for release 5.1.0 (#360)
* chore: preparing for release 5.1.0
* Trigger checks
* rubocop autocorrectable linting errors corrected
---
CHANGELOG.md | 5 +++++
lib/optimizely/event_builder.rb | 2 +-
lib/optimizely/version.rb | 2 +-
3 files changed, 7 insertions(+), 2 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8cbf930d..0330fa06 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
# Optimizely Ruby SDK Changelog
+## 5.1.0
+January 10th, 2025
+
+Added support for batch processing in DecideAll and DecideForKeys, enabling more efficient handling of multiple decisions in the User Profile Service.([#353](https://github.com/optimizely/ruby-sdk/pull/353))
+
## 5.0.1
February 8th, 2024
diff --git a/lib/optimizely/event_builder.rb b/lib/optimizely/event_builder.rb
index 9b4ccd1b..4c743cc3 100644
--- a/lib/optimizely/event_builder.rb
+++ b/lib/optimizely/event_builder.rb
@@ -78,7 +78,7 @@ def get_common_params(project_config, user_id, attributes)
)
end
# Append Bot Filtering Attribute
- if project_config.bot_filtering == true || project_config.bot_filtering == false
+ if [true, false].include?(project_config.bot_filtering)
visitor_attributes.push(
entity_id: Optimizely::Helpers::Constants::CONTROL_ATTRIBUTES['BOT_FILTERING'],
key: Optimizely::Helpers::Constants::CONTROL_ATTRIBUTES['BOT_FILTERING'],
diff --git a/lib/optimizely/version.rb b/lib/optimizely/version.rb
index 77ce669f..27894065 100644
--- a/lib/optimizely/version.rb
+++ b/lib/optimizely/version.rb
@@ -17,5 +17,5 @@
#
module Optimizely
CLIENT_ENGINE = 'ruby-sdk'
- VERSION = '5.0.1'
+ VERSION = '5.1.0'
end
From 61a95c3fe34538dd07e4abbdb97777f933b710a2 Mon Sep 17 00:00:00 2001
From: Farhan Anjum
Date: Fri, 16 May 2025 21:16:31 +0600
Subject: [PATCH 18/18] [FSSDK-11389] update: experiment_id and variation_id
added to payloads (#361)
* optimizely.rb -> added variation id and experiment id to notification listerner payload
optimizely_user_context_spec.rb -> fixed unit tests
* -unit tests updated
-rubocop autocorrection
* chore: trigger CI
* fix: rubocop corrections
---
lib/optimizely.rb | 19 +++++++++-----
lib/optimizely/helpers/validator.rb | 4 +--
spec/optimizely_user_context_spec.rb | 12 ++++++---
spec/project_spec.rb | 38 ++++++++++++++++++++--------
4 files changed, 52 insertions(+), 21 deletions(-)
diff --git a/lib/optimizely.rb b/lib/optimizely.rb
index 7c5571b3..f2e1dd82 100644
--- a/lib/optimizely.rb
+++ b/lib/optimizely.rb
@@ -185,12 +185,17 @@ def create_optimizely_decision(user_context, flag_key, decision, reasons, decide
feature_flag = config.get_feature_flag_from_key(flag_key)
experiment = nil
decision_source = Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT']
+ experiment_id = nil
+ variation_id = nil
+
# Send impression event if Decision came from a feature test and decide options doesn't include disableDecisionEvent
if decision.is_a?(Optimizely::DecisionService::Decision)
experiment = decision.experiment
rule_key = experiment ? experiment['key'] : nil
+ experiment_id = experiment ? experiment['id'] : nil
variation = decision['variation']
variation_key = variation ? variation['key'] : nil
+ variation_id = variation ? variation['id'] : nil
feature_enabled = variation ? variation['featureEnabled'] : false
decision_source = decision.source
end
@@ -214,14 +219,16 @@ def create_optimizely_decision(user_context, flag_key, decision, reasons, decide
@notification_center.send_notifications(
NotificationCenter::NOTIFICATION_TYPES[:DECISION],
Helpers::Constants::DECISION_NOTIFICATION_TYPES['FLAG'],
- user_id, (attributes || {}),
+ user_id, attributes || {},
flag_key: flag_key,
enabled: feature_enabled,
variables: all_variables,
variation_key: variation_key,
rule_key: rule_key,
reasons: should_include_reasons ? reasons : [],
- decision_event_dispatched: decision_event_dispatched
+ decision_event_dispatched: decision_event_dispatched,
+ experiment_id: experiment_id,
+ variation_id: variation_id
)
OptimizelyDecision.new(
@@ -625,7 +632,7 @@ def is_feature_enabled(feature_flag_key, user_id, attributes = nil)
@notification_center.send_notifications(
NotificationCenter::NOTIFICATION_TYPES[:DECISION],
Helpers::Constants::DECISION_NOTIFICATION_TYPES['FEATURE'],
- user_id, (attributes || {}),
+ user_id, attributes || {},
feature_key: feature_flag_key,
feature_enabled: feature_enabled,
source: source_string,
@@ -853,7 +860,7 @@ def get_all_feature_variables(feature_flag_key, user_id, attributes = nil)
@notification_center.send_notifications(
NotificationCenter::NOTIFICATION_TYPES[:DECISION],
- Helpers::Constants::DECISION_NOTIFICATION_TYPES['ALL_FEATURE_VARIABLES'], user_id, (attributes || {}),
+ Helpers::Constants::DECISION_NOTIFICATION_TYPES['ALL_FEATURE_VARIABLES'], user_id, attributes || {},
feature_key: feature_flag_key,
feature_enabled: feature_enabled,
source: source_string,
@@ -1033,7 +1040,7 @@ def get_variation_with_config(experiment_key, user_id, attributes, config)
end
@notification_center.send_notifications(
NotificationCenter::NOTIFICATION_TYPES[:DECISION],
- decision_notification_type, user_id, (attributes || {}),
+ decision_notification_type, user_id, attributes || {},
experiment_key: experiment_key,
variation_key: variation_key
)
@@ -1108,7 +1115,7 @@ def get_feature_variable_for_type(feature_flag_key, variable_key, variable_type,
@notification_center.send_notifications(
NotificationCenter::NOTIFICATION_TYPES[:DECISION],
- Helpers::Constants::DECISION_NOTIFICATION_TYPES['FEATURE_VARIABLE'], user_id, (attributes || {}),
+ Helpers::Constants::DECISION_NOTIFICATION_TYPES['FEATURE_VARIABLE'], user_id, attributes || {},
feature_key: feature_flag_key,
feature_enabled: feature_enabled,
source: source_string,
diff --git a/lib/optimizely/helpers/validator.rb b/lib/optimizely/helpers/validator.rb
index 4d975483..d3baa447 100644
--- a/lib/optimizely/helpers/validator.rb
+++ b/lib/optimizely/helpers/validator.rb
@@ -122,11 +122,11 @@ def inputs_valid?(variables, logger = NoOpLogger.new, level = Logger::ERROR)
return false unless variables.respond_to?(:each) && !variables.empty?
- is_valid = true # rubocop:disable Lint/UselessAssignment
+ is_valid = true
if variables.include? :user_id
# Empty str is a valid user ID.
unless variables[:user_id].is_a?(String)
- is_valid = false # rubocop:disable Lint/UselessAssignment
+ is_valid = false
logger.log(level, "#{Constants::INPUT_VARIABLES['USER_ID']} is invalid")
end
variables.delete :user_id
diff --git a/spec/optimizely_user_context_spec.rb b/spec/optimizely_user_context_spec.rb
index c968c336..515068c0 100644
--- a/spec/optimizely_user_context_spec.rb
+++ b/spec/optimizely_user_context_spec.rb
@@ -251,7 +251,9 @@
variation_key: '3324490562',
rule_key: nil,
reasons: [],
- decision_event_dispatched: true
+ decision_event_dispatched: true,
+ experiment_id: nil,
+ variation_id: '3324490562'
)
user_context_obj = forced_decision_project_instance.create_user_context(user_id)
context = Optimizely::OptimizelyUserContext::OptimizelyDecisionContext.new(feature_key, nil)
@@ -347,7 +349,9 @@
variation_key: 'b',
rule_key: 'exp_with_audience',
reasons: ['Variation (b) is mapped to flag (feature_1), rule (exp_with_audience) and user (tester) in the forced decision map.'],
- decision_event_dispatched: true
+ decision_event_dispatched: true,
+ experiment_id: '10390977673',
+ variation_id: '10416523121'
)
user_context_obj = Optimizely::OptimizelyUserContext.new(forced_decision_project_instance, user_id, original_attributes)
context = Optimizely::OptimizelyUserContext::OptimizelyDecisionContext.new(feature_key, 'exp_with_audience')
@@ -464,7 +468,9 @@
variation_key: '3324490562',
rule_key: nil,
reasons: [],
- decision_event_dispatched: true
+ decision_event_dispatched: true,
+ experiment_id: nil,
+ variation_id: '3324490562'
)
user_context_obj = forced_decision_project_instance.create_user_context(user_id)
context_with_flag = Optimizely::OptimizelyUserContext::OptimizelyDecisionContext.new(feature_key, nil)
diff --git a/spec/project_spec.rb b/spec/project_spec.rb
index 7c02f765..f857a5ce 100644
--- a/spec/project_spec.rb
+++ b/spec/project_spec.rb
@@ -2079,7 +2079,7 @@ def callback(_args); end
it 'should return only enabled feature flags keys' do
# Sets all feature-flags keys with randomly assigned status
features_keys = project_config.feature_flags.map do |item|
- {key: (item['key']).to_s, value: [true, false].sample} # '[true, false].sample' generates random boolean
+ {key: item['key'].to_s, value: [true, false].sample} # '[true, false].sample' generates random boolean
end
enabled_features = features_keys.map { |x| x[:key] if x[:value] == true }.compact
@@ -3758,7 +3758,9 @@ def callback(_args); end
variation_key: 'Fred',
rule_key: 'test_experiment_multivariate',
reasons: [],
- decision_event_dispatched: true
+ decision_event_dispatched: true,
+ experiment_id: experiment_to_return['id'],
+ variation_id: variation_to_return['id']
)
allow(project_instance.event_dispatcher).to receive(:dispatch_event).with(instance_of(Optimizely::Event))
decision_to_return = Optimizely::DecisionService::Decision.new(
@@ -3801,7 +3803,9 @@ def callback(_args); end
variation_key: 'Fred',
rule_key: 'test_experiment_multivariate',
reasons: [],
- decision_event_dispatched: true
+ decision_event_dispatched: true,
+ experiment_id: experiment_to_return['id'],
+ variation_id: variation_to_return['id']
)
allow(project_instance.event_dispatcher).to receive(:dispatch_event).with(instance_of(Optimizely::Event))
decision_to_return = Optimizely::DecisionService::Decision.new(
@@ -3883,7 +3887,9 @@ def callback(_args); end
variation_key: 'Fred',
rule_key: 'test_experiment_multivariate',
reasons: [],
- decision_event_dispatched: false
+ decision_event_dispatched: false,
+ experiment_id: experiment_to_return['id'],
+ variation_id: variation_to_return['id']
)
allow(project_config).to receive(:send_flag_decisions).and_return(false)
allow(project_instance.event_dispatcher).to receive(:dispatch_event).with(instance_of(Optimizely::Event))
@@ -3921,7 +3927,9 @@ def callback(_args); end
variation_key: nil,
rule_key: nil,
reasons: [],
- decision_event_dispatched: false
+ decision_event_dispatched: false,
+ experiment_id: nil,
+ variation_id: nil
)
allow(project_config).to receive(:send_flag_decisions).and_return(false)
allow(project_instance.event_dispatcher).to receive(:dispatch_event).with(instance_of(Optimizely::Event))
@@ -3958,7 +3966,9 @@ def callback(_args); end
variation_key: nil,
rule_key: nil,
reasons: [],
- decision_event_dispatched: true
+ decision_event_dispatched: true,
+ experiment_id: nil,
+ variation_id: nil
)
allow(project_instance.event_dispatcher).to receive(:dispatch_event).with(instance_of(Optimizely::Event))
decision_to_return = nil
@@ -4122,7 +4132,9 @@ def callback(_args); end
"The user 'user1' is not bucketed into any of the experiments on the feature 'multi_variate_feature'.",
"Feature flag 'multi_variate_feature' is not used in a rollout."
],
- decision_event_dispatched: true
+ decision_event_dispatched: true,
+ experiment_id: nil,
+ variation_id: nil
)
expect(project_instance.notification_center).to receive(:send_notifications)
.once.with(Optimizely::NotificationCenter::NOTIFICATION_TYPES[:LOG_EVENT], any_args)
@@ -4162,7 +4174,9 @@ def callback(_args); end
variation_key: nil,
rule_key: nil,
reasons: [],
- decision_event_dispatched: true
+ decision_event_dispatched: true,
+ experiment_id: nil,
+ variation_id: nil
)
allow(project_instance.event_dispatcher).to receive(:dispatch_event).with(instance_of(Optimizely::Event))
user_context = project_instance.create_user_context('user1')
@@ -4481,7 +4495,9 @@ def callback(_args); end
"The user 'user1' is not bucketed into any of the experiments on the feature 'multi_variate_feature'.",
"Feature flag 'multi_variate_feature' is not used in a rollout."
],
- decision_event_dispatched: true
+ decision_event_dispatched: true,
+ experiment_id: nil,
+ variation_id: nil
)
allow(custom_project_instance.event_dispatcher).to receive(:dispatch_event).with(instance_of(Optimizely::Event))
user_context = custom_project_instance.create_user_context('user1')
@@ -4521,7 +4537,9 @@ def callback(_args); end
variation_key: nil,
rule_key: nil,
reasons: [],
- decision_event_dispatched: true
+ decision_event_dispatched: true,
+ experiment_id: nil,
+ variation_id: nil
)
allow(custom_project_instance.event_dispatcher).to receive(:dispatch_event).with(instance_of(Optimizely::Event))
user_context = custom_project_instance.create_user_context('user1')