Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
53 changes: 41 additions & 12 deletions lib/optimizely.rb
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ def decide(user_context, key, decide_options = [])
# 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)
reasons.push(format(OptimizelyDecisionMessage::FLAG_KEY_INVALID, key))
return OptimizelyDecision.new(flag_key: key, user_context: user_context, reasons: reasons)
end

Expand Down Expand Up @@ -187,7 +187,7 @@ def decide(user_context, key, decide_options = [])
all_variables = {}
decision_event_dispatched = false

decision = @decision_service.get_variation_for_feature(config, feature_flag, user_id, attributes, decide_options)
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)
Expand All @@ -197,19 +197,22 @@ def decide(user_context, key, decide_options = [])
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
)
unless decide_options.include? OptimizelyDecideOption::DISABLE_DECISION_EVENT
if decision.source == Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST'] ||
(decision.source == Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT'] && config.send_flag_decisions)
send_impression(config, decision.experiment, variation_key, flag_key, rule_key, decision.source, user_id, attributes)
decision_event_dispatched = true
end
end
end

if decision.nil? && config.send_flag_decisions
send_impression(config, nil, '', flag_key, '', Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT'], user_id, attributes)
decision_event_dispatched = true
end
Copy link
Contributor

Choose a reason for hiding this comment

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

It can be better if we can merge these two into a single "send_impression"

Copy link
Contributor

Choose a reason for hiding this comment

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

Done


# Generate all variables map if decide options doesn't include excludeVariables
unless decide_options.include? Optimizely::Decide::OptimizelyDecideOption::EXCLUDE_VARIABLES
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)
Expand All @@ -230,24 +233,50 @@ def decide(user_context, key, decide_options = [])
decision_event_dispatched: decision_event_dispatched
)

should_include_reasons = decide_options.include? OptimizelyDecideOption::INCLUDE_REASONS
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
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

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 = {}
# 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|
decisions[feature_flag['key']] = decide(user_context, feature_flag['key'], decide_options)
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)
Copy link
Contributor

Choose a reason for hiding this comment

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

Should check "default_decide_options" as well.

Copy link
Contributor

Choose a reason for hiding this comment

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

default_decide_options are already being merged at the beginning of the function.

Copy link
Contributor

Choose a reason for hiding this comment

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

I see default_decide_options are merged in "decide()" function, but not in "decide_for_keys()".

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 = {}
keys.each do |key|
decision = decide(user_context, key, decide_options)
decisions[key] = decision unless enabled_flags_only && !decision.enabled
end
decisions
end
Expand Down
37 changes: 19 additions & 18 deletions lib/optimizely/bucketer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def initialize(logger)
@bucket_seed = HASH_SEED
end

def bucket(project_config, experiment, bucketing_id, user_id)
def bucket(project_config, experiment, bucketing_id, user_id, decide_reasons = nil)
# Determines ID of variation to be shown for a given experiment key and user ID.
#
# project_config - Instance of ProjectConfig
Expand All @@ -58,46 +58,45 @@ def bucket(project_config, experiment, bucketing_id, user_id)
bucketed_experiment_id = find_bucket(bucketing_id, user_id, group_id, traffic_allocations)
# return if the user is not bucketed into any experiment
unless bucketed_experiment_id
@logger.log(Logger::INFO, "User '#{user_id}' is in no experiment.")
message = "User '#{user_id}' is in no experiment."
@logger.log(Logger::INFO, message)
decide_reasons&.push(message)
return nil
end

# return if the user is bucketed into a different experiment than the one specified
if bucketed_experiment_id != experiment_id
@logger.log(
Logger::INFO,
"User '#{user_id}' is not in experiment '#{experiment_key}' of group #{group_id}."
)
message = "User '#{user_id}' is not in experiment '#{experiment_key}' of group #{group_id}."
@logger.log(Logger::INFO, message)
decide_reasons&.push(message)
return nil
end

# continue bucketing if the user is bucketed into the experiment specified
@logger.log(
Logger::INFO,
"User '#{user_id}' is in experiment '#{experiment_key}' of group #{group_id}."
)
message = "User '#{user_id}' is in experiment '#{experiment_key}' of group #{group_id}."
@logger.log(Logger::INFO, message)
decide_reasons&.push(message)
end
end

traffic_allocations = experiment['trafficAllocation']
variation_id = find_bucket(bucketing_id, user_id, experiment_id, traffic_allocations)
variation_id = find_bucket(bucketing_id, user_id, experiment_id, traffic_allocations, decide_reasons)
if variation_id && variation_id != ''
variation = project_config.get_variation_from_id(experiment_key, variation_id)
return variation
end

# Handle the case when the traffic range is empty due to sticky bucketing
if variation_id == ''
@logger.log(
Logger::DEBUG,
'Bucketed into an empty traffic range. Returning nil.'
)
message = 'Bucketed into an empty traffic range. Returning nil.'
@logger.log(Logger::DEBUG, message)
decide_reasons&.push(message)
end

nil
end

def find_bucket(bucketing_id, user_id, parent_id, traffic_allocations)
def find_bucket(bucketing_id, user_id, parent_id, traffic_allocations, decide_reasons = nil)
# Helper function to find the matching entity ID for a given bucketing value in a list of traffic allocations.
#
# bucketing_id - String A customer-assigned value user to generate bucketing key
Expand All @@ -108,8 +107,10 @@ def find_bucket(bucketing_id, user_id, parent_id, traffic_allocations)
# Returns entity ID corresponding to the provided bucket value or nil if no match is found.
bucketing_key = format(BUCKETING_ID_TEMPLATE, bucketing_id: bucketing_id, entity_id: parent_id)
bucket_value = generate_bucket_value(bucketing_key)
@logger.log(Logger::DEBUG, "Assigned bucket #{bucket_value} to user '#{user_id}' "\
"with bucketing ID: '#{bucketing_id}'.")

message = "Assigned bucket #{bucket_value} to user '#{user_id}' with bucketing ID: '#{bucketing_id}'."
@logger.log(Logger::DEBUG, message)
decide_reasons&.push(message)

traffic_allocations.each do |traffic_allocation|
current_end_of_range = traffic_allocation['endOfRange']
Expand Down
2 changes: 2 additions & 0 deletions lib/optimizely/decide/optimizely_decision.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
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,
Expand Down
Loading