-
Notifications
You must be signed in to change notification settings - Fork 28
feat(Decide): Add Decide API #274
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
49 commits
Select commit
Hold shift + click to select a range
4f8765b
feat(Decide): Add Optimizely User Context
oakbani a7b78d8
line at EOF
oakbani c5f138c
Merge branch 'master' into oakbani/decide/user-context
oakbani c8d3a35
feat(Decide): Add Decide API
oakbani f770ce7
fix
oakbani 244c4f3
nil by []
oakbani 1bd6df7
fix enabled
oakbani ddd5349
add to_json in entities
oakbani 98ced8c
Made it work with
zashraf1985 e7366f4
fixed decide_all api
zashraf1985 03a397f
fixed decide_for_keys
zashraf1985 3e9e417
fixed some failing unit tests
zashraf1985 ab824c7
added some test cases
zashraf1985 507dac6
added decide temporary tags
zashraf1985 13833de
added pending to forcefully fail unfinished tests
zashraf1985 b981f64
added some unit tests for decide
zashraf1985 d102a14
added unit tests for decide api
zashraf1985 7fb2ddf
added tests for ignoring user profile service option
zashraf1985 16a509d
added few more tests and some cleanup
zashraf1985 f6437b7
added a null check before cloning attributes
zashraf1985 ac90cd0
simplified the null check
zashraf1985 4026855
changed array append to push to support old ruby versions
zashraf1985 d6be7c2
added support for ENABLED_FLAGS_ONLY decide option
zashraf1985 752114d
Added unit tests for decide_for_keys and decide_all
zashraf1985 ee32ee1
added flag decisions support in decide api
zashraf1985 57ac8a0
added getters to OptimizelyDecision
zashraf1985 a300b3e
Merge branch 'master' into oakbani/decide-internal
zashraf1985 0070d3b
added unit tests for flag decisionss support in decide API
zashraf1985 6727d30
added sdk ready check to all apis
zashraf1985 d9a5549
added a check to include reasons
zashraf1985 cd1648e
added decide reasons
zashraf1985 8ec1619
fixed a minor issue with decide reasons
zashraf1985 a041cb6
Merge branch 'master' into oakbani/decide-internal
zashraf1985 7d122a4
removed some logs from decide reasons
zashraf1985 c40237f
additional null check for optimizely object
zashraf1985 7f6d985
fixed failing FSC test for some events
zashraf1985 b793012
merged the send impression calls under one check
zashraf1985 0a38bdd
fixed failing FSC test
zashraf1985 c401588
modified some tests
zashraf1985 cffa963
added event payload tests
zashraf1985 43b14ee
Added tests for include_reasons
zashraf1985 245637e
fixed a test title
zashraf1985 b76af55
added unit tests for default_decide_options
zashraf1985 5057b3f
added sdk not ready tests for decide_all and decide_for_keys
zashraf1985 92afe26
incorporated some review changes
zashraf1985 1ef9539
added a synchronize block when setting attributes
zashraf1985 89c3d6b
Added doc comments
zashraf1985 315d4aa
lint fixes
zashraf1985 f58d8bd
clone and sync user attributes and context
zashraf1985 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,6 +19,9 @@ | |
require_relative 'optimizely/config/datafile_project_config' | ||
require_relative 'optimizely/config_manager/http_project_config_manager' | ||
require_relative 'optimizely/config_manager/static_project_config_manager' | ||
require_relative 'optimizely/decide/optimizely_decide_option' | ||
require_relative 'optimizely/decide/optimizely_decision' | ||
require_relative 'optimizely/decide/optimizely_decision_message' | ||
require_relative 'optimizely/decision_service' | ||
require_relative 'optimizely/error_handler' | ||
require_relative 'optimizely/event_builder' | ||
|
@@ -34,9 +37,12 @@ | |
require_relative 'optimizely/logger' | ||
require_relative 'optimizely/notification_center' | ||
require_relative 'optimizely/optimizely_config' | ||
require_relative 'optimizely/optimizely_user_context' | ||
|
||
module Optimizely | ||
class Project | ||
include Optimizely::Decide | ||
|
||
attr_reader :notification_center | ||
# @api no-doc | ||
attr_reader :config_manager, :decision_service, :error_handler, :event_dispatcher, | ||
|
@@ -67,12 +73,21 @@ def initialize( | |
sdk_key = nil, | ||
config_manager = nil, | ||
notification_center = nil, | ||
event_processor = nil | ||
event_processor = nil, | ||
default_decide_options = [] | ||
) | ||
@logger = logger || NoOpLogger.new | ||
@error_handler = error_handler || NoOpErrorHandler.new | ||
@event_dispatcher = event_dispatcher || EventDispatcher.new(logger: @logger, error_handler: @error_handler) | ||
@user_profile_service = user_profile_service | ||
@default_decide_options = [] | ||
|
||
if default_decide_options.is_a? Array | ||
@default_decide_options = default_decide_options.clone | ||
else | ||
@logger.log(Logger::DEBUG, 'Provided default decide options is not an array.') | ||
@default_decide_options = [] | ||
end | ||
|
||
begin | ||
validate_instantiation_options | ||
|
@@ -107,6 +122,174 @@ def initialize( | |
end | ||
end | ||
|
||
# Create a context of the user for which decision APIs will be called. | ||
# | ||
# A user context will be created successfully even when the SDK is not fully configured yet. | ||
# | ||
# @param user_id - The user ID to be used for bucketing. | ||
# @param attributes - A Hash representing user attribute names and values. | ||
# | ||
# @return [OptimizelyUserContext] An OptimizelyUserContext associated with this OptimizelyClient. | ||
# @return [nil] If user attributes are not in valid format. | ||
|
||
def create_user_context(user_id, attributes = nil) | ||
# We do not check for is_valid here as a user context can be created successfully | ||
# even when the SDK is not fully configured. | ||
|
||
# validate user_id | ||
return nil unless Optimizely::Helpers::Validator.inputs_valid?( | ||
{ | ||
user_id: user_id | ||
}, @logger, Logger::ERROR | ||
) | ||
|
||
# validate attributes | ||
return nil unless user_inputs_valid?(attributes) | ||
|
||
user_context = OptimizelyUserContext.new(self, user_id, attributes) | ||
user_context | ||
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 | ||
|
||
# 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 | ||
experiment = nil | ||
decision_source = Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT'] | ||
|
||
decision = @decision_service.get_variation_for_feature(config, feature_flag, user_id, attributes, decide_options, reasons) | ||
|
||
# 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['key'] | ||
variation = decision['variation'] | ||
variation_key = variation['key'] | ||
feature_enabled = variation['featureEnabled'] | ||
decision_source = decision.source | ||
end | ||
|
||
unless decide_options.include? OptimizelyDecideOption::DISABLE_DECISION_EVENT | ||
if decision_source == Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST'] || config.send_flag_decisions | ||
send_impression(config, experiment, variation_key || '', flag_key, rule_key || '', feature_enabled, decision_source, user_id, attributes) | ||
decision_event_dispatched = true | ||
end | ||
end | ||
|
||
# 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) | ||
all_variables[variable['key']] = Helpers::VariableType.cast_value_to_type(variable_value, variable['type'], @logger) | ||
end | ||
end | ||
|
||
should_include_reasons = decide_options.include? OptimizelyDecideOption::INCLUDE_REASONS | ||
|
||
# Send notification | ||
@notification_center.send_notifications( | ||
NotificationCenter::NOTIFICATION_TYPES[:DECISION], | ||
Helpers::Constants::DECISION_NOTIFICATION_TYPES['FLAG'], | ||
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 | ||
) | ||
|
||
OptimizelyDecision.new( | ||
variation_key: variation_key, | ||
enabled: feature_enabled, | ||
variables: all_variables, | ||
rule_key: rule_key, | ||
flag_key: flag_key, | ||
user_context: user_context, | ||
reasons: should_include_reasons ? reasons : [] | ||
) | ||
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 | ||
|
||
# check if SDK is ready | ||
unless is_valid | ||
@logger.log(Logger::ERROR, InvalidProjectConfigError.new('decide_all').message) | ||
return {} | ||
end | ||
|
||
keys = [] | ||
project_config.feature_flags.each do |feature_flag| | ||
keys.push(feature_flag['key']) | ||
end | ||
decide_for_keys(user_context, keys, decide_options) | ||
end | ||
|
||
def decide_for_keys(user_context, keys, decide_options = []) | ||
# raising on user context as it is internal and not provided directly by the user. | ||
raise if user_context.class != OptimizelyUserContext | ||
|
||
# check if SDK is ready | ||
unless is_valid | ||
@logger.log(Logger::ERROR, InvalidProjectConfigError.new('decide_for_keys').message) | ||
return {} | ||
end | ||
|
||
enabled_flags_only = (!decide_options.nil? && (decide_options.include? OptimizelyDecideOption::ENABLED_FLAGS_ONLY)) || (@default_decide_options.include? OptimizelyDecideOption::ENABLED_FLAGS_ONLY) | ||
|
||
decisions = {} | ||
keys.each do |key| | ||
decision = decide(user_context, key, decide_options) | ||
decisions[key] = decision unless enabled_flags_only && !decision.enabled | ||
end | ||
decisions | ||
end | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. decide_for_keys() api? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. added now |
||
# Buckets visitor and sends impression event to Optimizely. | ||
# | ||
# @param experiment_key - Experiment which needs to be activated. | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
# frozen_string_literal: true | ||
|
||
# Copyright 2020, 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. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
# | ||
|
||
module Optimizely | ||
module Decide | ||
module OptimizelyDecideOption | ||
DISABLE_DECISION_EVENT = 'DISABLE_DECISION_EVENT' | ||
ENABLED_FLAGS_ONLY = 'ENABLED_FLAGS_ONLY' | ||
IGNORE_USER_PROFILE_SERVICE = 'IGNORE_USER_PROFILE_SERVICE' | ||
INCLUDE_REASONS = 'INCLUDE_REASONS' | ||
EXCLUDE_VARIABLES = 'EXCLUDE_VARIABLES' | ||
end | ||
end | ||
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
# frozen_string_literal: true | ||
|
||
# Copyright 2020, 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. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
# | ||
|
||
require 'json' | ||
|
||
module Optimizely | ||
module Decide | ||
class OptimizelyDecision | ||
attr_reader :variation_key, :enabled, :variables, :rule_key, :flag_key, :user_context, :reasons | ||
|
||
def initialize( | ||
variation_key: nil, | ||
enabled: nil, | ||
variables: nil, | ||
rule_key: nil, | ||
flag_key: nil, | ||
user_context: nil, | ||
reasons: nil | ||
) | ||
@variation_key = variation_key | ||
@enabled = enabled || false | ||
@variables = variables || {} | ||
@rule_key = rule_key | ||
@flag_key = flag_key | ||
@user_context = user_context | ||
@reasons = reasons || [] | ||
end | ||
|
||
def as_json | ||
{ | ||
variation_key: @variation_key, | ||
enabled: @enabled, | ||
variables: @variables, | ||
rule_key: @rule_key, | ||
flag_key: @flag_key, | ||
user_context: @user_context.as_json, | ||
reasons: @reasons | ||
} | ||
end | ||
|
||
def to_json(*args) | ||
as_json.to_json(*args) | ||
end | ||
end | ||
end | ||
end |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add checking SDK_NOT_READY error here? If not, we should return an empty map instead of a map of error-decisions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done