Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
4f8765b
feat(Decide): Add Optimizely User Context
oakbani Oct 21, 2020
a7b78d8
line at EOF
oakbani Oct 21, 2020
c5f138c
Merge branch 'master' into oakbani/decide/user-context
oakbani Oct 22, 2020
c8d3a35
feat(Decide): Add Decide API
oakbani Oct 23, 2020
f770ce7
fix
oakbani Oct 23, 2020
244c4f3
nil by []
oakbani Oct 23, 2020
1bd6df7
fix enabled
oakbani Oct 23, 2020
ddd5349
add to_json in entities
oakbani Oct 27, 2020
98ced8c
Made it work with
zashraf1985 Oct 30, 2020
e7366f4
fixed decide_all api
zashraf1985 Oct 30, 2020
03a397f
fixed decide_for_keys
zashraf1985 Nov 2, 2020
3e9e417
fixed some failing unit tests
zashraf1985 Nov 2, 2020
ab824c7
added some test cases
zashraf1985 Nov 3, 2020
507dac6
added decide temporary tags
zashraf1985 Nov 4, 2020
13833de
added pending to forcefully fail unfinished tests
zashraf1985 Nov 4, 2020
b981f64
added some unit tests for decide
zashraf1985 Nov 4, 2020
d102a14
added unit tests for decide api
zashraf1985 Nov 5, 2020
7fb2ddf
added tests for ignoring user profile service option
zashraf1985 Nov 6, 2020
16a509d
added few more tests and some cleanup
zashraf1985 Nov 6, 2020
f6437b7
added a null check before cloning attributes
zashraf1985 Nov 6, 2020
ac90cd0
simplified the null check
zashraf1985 Nov 6, 2020
4026855
changed array append to push to support old ruby versions
zashraf1985 Nov 6, 2020
d6be7c2
added support for ENABLED_FLAGS_ONLY decide option
zashraf1985 Nov 12, 2020
752114d
Added unit tests for decide_for_keys and decide_all
zashraf1985 Nov 13, 2020
ee32ee1
added flag decisions support in decide api
zashraf1985 Nov 13, 2020
57ac8a0
added getters to OptimizelyDecision
zashraf1985 Nov 13, 2020
a300b3e
Merge branch 'master' into oakbani/decide-internal
zashraf1985 Nov 17, 2020
0070d3b
added unit tests for flag decisionss support in decide API
zashraf1985 Nov 17, 2020
6727d30
added sdk ready check to all apis
zashraf1985 Nov 17, 2020
d9a5549
added a check to include reasons
zashraf1985 Nov 18, 2020
cd1648e
added decide reasons
zashraf1985 Nov 18, 2020
8ec1619
fixed a minor issue with decide reasons
zashraf1985 Nov 19, 2020
a041cb6
Merge branch 'master' into oakbani/decide-internal
zashraf1985 Nov 24, 2020
7d122a4
removed some logs from decide reasons
zashraf1985 Nov 24, 2020
c40237f
additional null check for optimizely object
zashraf1985 Nov 24, 2020
7f6d985
fixed failing FSC test for some events
zashraf1985 Nov 24, 2020
b793012
merged the send impression calls under one check
zashraf1985 Nov 25, 2020
0a38bdd
fixed failing FSC test
zashraf1985 Nov 25, 2020
c401588
modified some tests
zashraf1985 Nov 25, 2020
cffa963
added event payload tests
zashraf1985 Nov 26, 2020
43b14ee
Added tests for include_reasons
zashraf1985 Nov 26, 2020
245637e
fixed a test title
zashraf1985 Nov 26, 2020
b76af55
added unit tests for default_decide_options
zashraf1985 Nov 30, 2020
5057b3f
added sdk not ready tests for decide_all and decide_for_keys
zashraf1985 Nov 30, 2020
92afe26
incorporated some review changes
zashraf1985 Dec 7, 2020
1ef9539
added a synchronize block when setting attributes
zashraf1985 Dec 7, 2020
89c3d6b
Added doc comments
zashraf1985 Dec 8, 2020
315d4aa
lint fixes
zashraf1985 Dec 9, 2020
f58d8bd
clone and sync user attributes and context
zashraf1985 Dec 10, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 146 additions & 1 deletion lib/optimizely.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -107,6 +122,136 @@ def initialize(
end
end

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(OptimizelyDecisionMessage::VARIABLE_VALUE_INVALID)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FLAG_KEY_INVALID

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

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

decision = @decision_service.get_variation_for_feature(config, feature_flag, user_id, attributes, decide_options)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Plan to add collecting decision "reasons" in a separate PR?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.


# Send impression event if Decision came from a feature test and decide options doesn't include disableDecisionEvent
if decision.is_a?(Optimizely::DecisionService::Decision)
variation = decision['variation']
variation_key = variation['key']
feature_enabled = variation['featureEnabled']
flag_key = key
rule_key = decision.experiment['key']
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When rule-key is not available, it's ok to set it to experiment key (pre-velociraptor). Ideally, we can add "rule_key" into Decision for velociraptor support as well.


if decision.source == Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
unless decide_options.include? Optimizely::Decide::OptimizelyDecideOption::DISABLE_DECISION_EVENT
source_string = Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
send_impression(
config, decision.experiment, variation_key, flag_key, rule_key, source_string, user_id, attributes
)
decision_event_dispatched = true
end
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be changed to support flag-decision (send events for all feature decision).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

end

# Generate all variables map if decide options doesn't include excludeVariables
unless decide_options.include? Optimizely::Decide::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

# 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: 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: 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

Copy link
Contributor

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

decisions = {}
project_config.feature_flags.each do |feature_flag|
decisions[feature_flag['key']] = decide(user_context, feature_flag['key'], decide_options)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to support option for "ENABLED_FLAGS_ONLY"

end
decisions
end

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

decide_for_keys() api?

Copy link
Contributor

Choose a reason for hiding this comment

The 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.
Expand Down
28 changes: 28 additions & 0 deletions lib/optimizely/decide/optimizely_decide_option.rb
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
58 changes: 58 additions & 0 deletions lib/optimizely/decide/optimizely_decision.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# 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
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
26 changes: 26 additions & 0 deletions lib/optimizely/decide/optimizely_decision_message.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# 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 OptimizelyDecisionMessage
SDK_NOT_READY = 'Optimizely SDK not configured properly yet.'
FLAG_KEY_INVALID = 'No flag was found for key "%s".'
VARIABLE_VALUE_INVALID = 'Variable value for key "%s" is invalid or wrong type.'
end
end
end
33 changes: 18 additions & 15 deletions lib/optimizely/decision_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def initialize(logger, user_profile_service = nil)
@forced_variation_map = {}
end

def get_variation(project_config, experiment_key, user_id, attributes = nil)
def get_variation(project_config, experiment_key, user_id, attributes = nil, decide_options = [])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to collect decision reasons here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

# Determines variation into which user will be bucketed.
#
# project_config - project_config - Instance of ProjectConfig
Expand Down Expand Up @@ -83,15 +83,18 @@ def get_variation(project_config, experiment_key, user_id, attributes = nil)
whitelisted_variation_id = get_whitelisted_variation_id(project_config, experiment_key, user_id)
return whitelisted_variation_id if whitelisted_variation_id

# Check for saved bucketing decisions
user_profile = get_user_profile(user_id)
saved_variation_id = get_saved_variation_id(project_config, experiment_id, user_profile)
if saved_variation_id
@logger.log(
Logger::INFO,
"Returning previously activated variation ID #{saved_variation_id} of experiment '#{experiment_key}' for user '#{user_id}' from user profile."
)
return saved_variation_id
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 = get_user_profile(user_id)
saved_variation_id = get_saved_variation_id(project_config, experiment_id, user_profile)
if saved_variation_id
@logger.log(
Logger::INFO,
"Returning previously activated variation ID #{saved_variation_id} of experiment '#{experiment_key}' for user '#{user_id}' from user profile."
)
return saved_variation_id
end
end

# Check audience conditions
Expand All @@ -118,11 +121,11 @@ def get_variation(project_config, experiment_key, user_id, attributes = nil)
end

# Persist bucketing decision
save_user_profile(user_profile, experiment_id, variation_id)
save_user_profile(user_profile, experiment_id, variation_id) unless should_ignore_user_profile_service
variation_id
end

def get_variation_for_feature(project_config, feature_flag, user_id, attributes = nil)
def get_variation_for_feature(project_config, feature_flag, user_id, attributes = nil, decide_options = [])
# Get the variation the user is bucketed into for the given FeatureFlag.
#
# project_config - project_config - Instance of ProjectConfig
Expand All @@ -133,15 +136,15 @@ def get_variation_for_feature(project_config, feature_flag, user_id, attributes
# Returns Decision struct (nil if the user is not bucketed into any of the experiments on the feature)

# check if the feature is being experiment on and whether the user is bucketed into the experiment
decision = get_variation_for_feature_experiment(project_config, feature_flag, user_id, attributes)
decision = get_variation_for_feature_experiment(project_config, feature_flag, user_id, attributes, decide_options)
return decision unless decision.nil?

decision = get_variation_for_feature_rollout(project_config, feature_flag, user_id, attributes)

decision
end

def get_variation_for_feature_experiment(project_config, feature_flag, user_id, attributes = nil)
def get_variation_for_feature_experiment(project_config, feature_flag, user_id, attributes = nil, decide_options = [])
# Gets the variation the user is bucketed into for the feature flag's experiment.
#
# project_config - project_config - Instance of ProjectConfig
Expand Down Expand Up @@ -172,7 +175,7 @@ def get_variation_for_feature_experiment(project_config, feature_flag, user_id,
end

experiment_key = experiment['key']
variation_id = get_variation(project_config, experiment_key, user_id, attributes)
variation_id = get_variation(project_config, experiment_key, user_id, attributes, decide_options)

next unless variation_id

Expand Down
1 change: 1 addition & 0 deletions lib/optimizely/helpers/constants.rb
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,7 @@ module Constants
'FEATURE' => 'feature',
'FEATURE_TEST' => 'feature-test',
'FEATURE_VARIABLE' => 'feature-variable',
'FLAG' => 'flag',
'ALL_FEATURE_VARIABLES' => 'all-feature-variables'
}.freeze

Expand Down
Loading