diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 00000000..de1db2b4
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,17 @@
+{
+ "name": "Ruby SDK",
+
+ "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",
+
+ "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/.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
diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml
index 1e22e74e..98f73108 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', '3.3.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/CHANGELOG.md b/CHANGELOG.md
index 6c54b808..0330fa06 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,77 @@
# 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
+
+The 5.0.1 minor release introduces update of metadata in gemspec.
+
+## 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/README.md b/README.md
index a10ba8d9..be5e0613 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
@@ -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
@@ -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.rb b/lib/optimizely.rb
index 93f4fc3c..f2e1dd82 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
@@ -70,20 +71,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
@@ -172,71 +173,29 @@ 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
+ 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
@@ -249,7 +208,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
@@ -260,14 +219,16 @@ def decide(user_context, key, decide_options = [])
@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(
@@ -281,6 +242,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 +300,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 +310,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
@@ -564,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,
@@ -792,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,
@@ -889,7 +957,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
@@ -959,7 +1027,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)
@@ -969,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
)
@@ -1044,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/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/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/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/config_manager/http_project_config_manager.rb b/lib/optimizely/config_manager/http_project_config_manager.rb
index 0da73c1f..03e177b5 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
@@ -146,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
@@ -268,6 +263,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
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/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/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..4c743cc3 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,23 +62,23 @@ 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)
- 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
+ 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/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/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_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/lib/optimizely/optimizely_factory.rb b/lib/optimizely/optimizely_factory.rb
index b6734872..717e43d9 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.
@@ -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)
@@ -167,19 +166,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/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/lib/optimizely/version.rb b/lib/optimizely/version.rb
index 43d4f749..27894065 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-beta'
+ VERSION = '5.1.0'
end
diff --git a/optimizely-sdk.gemspec b/optimizely-sdk.gemspec
index c1c5b881..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.required_ruby_version = '>= 2.7'
+ 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'
@@ -24,6 +28,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
diff --git a/spec/audience_spec.rb b/spec/audience_spec.rb
index 73560aff..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 }
@@ -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']
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/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
diff --git a/spec/decision_service_spec.rb b/spec/decision_service_spec.rb
index 7646c032..af22b18b 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 }
@@ -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
@@ -497,13 +411,13 @@
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', {})
-
+ 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'."])
@@ -619,7 +536,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 +733,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..4164d3ca 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.
@@ -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 }
@@ -26,16 +27,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
@@ -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
diff --git a/spec/optimizely_user_context_spec.rb b/spec/optimizely_user_context_spec.rb
index 6a99c57b..515068c0 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
{
@@ -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 d00f93c1..f857a5ce 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'.")
@@ -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
@@ -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(
@@ -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(
@@ -3766,7 +3768,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(
@@ -3799,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(
@@ -3807,7 +3813,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')
@@ -3879,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))
@@ -3888,7 +3898,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(
@@ -3916,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))
@@ -3953,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
@@ -4055,8 +4070,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 +4094,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 +4113,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],
@@ -4117,8 +4132,12 @@ 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)
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])
@@ -4155,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')
@@ -4180,23 +4201,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
@@ -4216,7 +4237,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 +4304,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 +4375,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 +4413,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(
@@ -4400,8 +4421,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(
@@ -4418,8 +4440,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]
@@ -4428,8 +4450,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(
@@ -4448,8 +4471,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)
@@ -4472,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')
@@ -4497,7 +4522,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)
@@ -4512,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')
@@ -4532,7 +4559,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 +4576,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 +4601,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 +4614,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 +4626,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 +4639,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 +4651,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 +4663,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 +4677,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 +4691,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 +4711,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 +4725,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 +4749,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 +4765,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 +4788,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 +4803,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 +4818,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 +4830,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 +4841,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 +4850,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 +4860,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 +4891,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 +4908,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 +4931,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 }
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