8000 [FSSDK-10765] enhancement: Implement UPS request batching for decideForKeys by FarhanAnjum-opti · Pull Request #353 · optimizely/ruby-sdk · GitHub
[go: up one dir, main page]

Skip to content

[FSSDK-10765] enhancement: Implement UPS request batching for decideForKeys #353

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 19 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
Prev Previous commit
Next Next commit
lib/optimizely/decision_service.rb -> Simplified decision logging
lib/optimizely/user_profile_tracker.rb -> Improved user profile lookup handling
spec/project_spec.rb -> Updated mocks for decision service calls
  • Loading branch information
FarhanAnjum-opti committed Dec 9, 2024
commit 061f8bc89fb05b7db3730f023ec7e68638e8295d
92 changes: 44 additions & 48 deletions lib/optimizely.rb
Original file line number Diff line number Diff line change
Expand Up @@ -173,48 +173,6 @@ 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

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 create_optimizely_decision(user_context, flag_key, decision, reasons, decide_options, config)
# Create Optimizely Decision Result.
user_id = user_context.user_id
Expand Down Expand Up @@ -277,6 +235,47 @@ def create_optimizely_decision(user_context, flag_key, decision, reasons, decide
)
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
Expand Down Expand Up @@ -322,8 +321,8 @@ def decide_for_keys(user_context, keys, decide_options = [], ignore_default_opti
config = project_config
return decisions unless config

flags_without_forced_decision = [] #: list[entities.FeatureFlag]
flag_decisions = {} #: dict[str, Decision]
flags_without_forced_decision = []
flag_decisions = {}

keys.each do |key|
# Retrieve the feature flag from the project's feature flag key map
Expand All @@ -342,15 +341,13 @@ def decide_for_keys(user_context, keys, decide_options = [], ignore_default_opti
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|
Expand All @@ -359,9 +356,8 @@ def decide_for_keys(user_context, keys, decide_options = [], ignore_default_opti
flag_key = flag['key']
flag_decisions[flag_key] = decision
decision_reasons_dict[flag_key] ||= []
decision_reasons_dict[flag_key] += reasons
decision_reasons_dict[flag_key].push(*reasons)
end

valid_keys.each do |key|
flag_decision = flag_decisions[key]
decision_reasons = decision_reasons_dict[key]
Expand Down
3 changes: 1 addition & 2 deletions lib/optimizely/decision_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,8 @@ def get_variations_for_feature_list(project_config, feature_flags, user_context,
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
# Push the decision and reasons to decisions if the decision is not nil
decide_reasons.push(*reasons_received)
decisions << [decision, decide_reasons]
else
# Proceed to rollout if the decision is nil
Expand Down
2 changes: 1 addition & 1 deletion lib/optimizely/user_profile_tracker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def load_user_profile(reasons = [], error_handler = nil)
return if reasons.nil?

begin
@user_profile = @user_profile_service.lookup(@user_id) || @user_profile
@user_profile = @user_profile_service.lookup(@user_id) if @user_profile_service
rescue => e
message = "Error while loading user profile in user profile tracker for user ID '#{@user_id}': #{e}."
reasons << e.message
Expand Down
31 changes: 19 additions & 12 deletions spec/project_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3766,7 +3766,9 @@ def callback(_args); end
variation_to_return,
Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
)
allow(project_instance.decision_service).to receive(:get_variation_for_feature).and_return(decision_to_return)
decision_list_to_be_returned = []
decision_list_to_be_returned << [decision_to_return, []]
allow(project_instance.decision_service).to receive(:get_variations_for_feature_list).and_return(decision_list_to_be_returned)
user_context = project_instance.create_user_context('user1')
decision = project_instance.decide(user_context, 'multi_variate_feature')
expect(decision.as_json).to include(
Expand Down Expand Up @@ -3807,7 +3809,9 @@ def callback(_args); end
variation_to_return,
Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT']
)
allow(project_instance.decision_service).to receive(:get_variation_for_feature).and_return(decision_to_return)
decision_list_to_be_returned = []
decision_list_to_be_returned << [decision_to_return, []]
allow(project_instance.decision_service).to receive(:get_variations_for_feature_list).and_return(decision_list_to_be_returned)
user_context = project_instance.create_user_context('user1')
decision = project_instance.decide(user_context, 'multi_variate_feature')

Expand Down Expand Up @@ -3888,7 +3892,8 @@ def callback(_args); end
variation_to_return,
Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT']
)
allow(project_instance.decision_service).to receive(:get_variation_for_feature).and_return(decision_to_return)
decision_list_to_return = [[decision_to_return, []]]
allow(project_instance.decision_service).to receive(:get_variations_for_feature_list).and_return(decision_list_to_return)
user_context = project_instance.create_user_context('user1')
decision = project_instance.decide(user_context, 'multi_variate_feature')
expect(decision.as_json).to include(
Expand Down Expand Up @@ -4055,8 +4060,9 @@ def callback(_args); end
variation_to_return,
Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
)
decision_list_to_be_returned = [[decision_to_return, []]]
allow(project_instance.event_dispatcher).to receive(:dispatch_event).with(instance_of(Optimizely::Event))
allow(project_instance.decision_service).to receive(:get_variation_for_feature).and_return(decision_to_return)
allow(project_instance.decision_service).to receive(:get_variations_for_feature_list).and_return(decision_list_to_be_returned)
user_context = project_instance.create_user_context('user1')
decision = project_instance.decide(user_context, 'multi_variate_feature', [Optimizely::Decide::OptimizelyDecideOption::EXCLUDE_VARIABLES])
expect(decision 8000 .as_json).to include(
Expand All @@ -4078,8 +4084,9 @@ def callback(_args); end
variation_to_return,
Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
)
decision_list_to_return = [[decision_to_return, []]]
allow(project_instance.event_dispatcher).to receive(:dispatch_event).with(instance_of(Optimizely::Event))
allow(project_instance.decision_service).to receive(:get_variation_for_feature).and_return(decision_to_return)
allow(project_instance.decision_service).to receive(:get_variations_for_feature_list).and_return(decision_list_to_return)
user_context = project_instance.create_user_context('user1')
decision = project_instance.decide(user_context, 'multi_variate_feature')
expect(decision.as_json).to include(
Expand All @@ -4096,8 +4103,6 @@ def callback(_args); end

describe 'INCLUDE_REASONS' do
it 'should include reasons when the option is set' do
expect(project_instance.notification_center).to receive(:send_notifications)
.once.with(Optimizely::NotificationCenter::NOTIFICATION_TYPES[:LOG_EVENT], any_args)
expect(project_instance.notification_center).to receive(:send_notifications)
.once.with(
Optimizely::NotificationCenter::NOTIFICATION_TYPES[:DECISION],
Expand All @@ -4119,6 +4124,8 @@ def callback(_args); end
],
decision_event_dispatched: true
)
expect(project_instance.notification_center).to receive(:send_notifications)
.once.with(Optimizely::NotificationCenter::NOTIFICATION_TYPES[:LOG_EVENT], any_args)
allow(project_instance.event_dispatcher).to receive(:dispatch_event).with(instance_of(Optimizely::Event))
user_context = project_instance.create_user_context('user1')
decision = project_instance.decide(user_context, 'multi_variate_feature', [Optimizely::Decide::OptimizelyDecideOption::INCLUDE_REASONS])
Expand Down Expand Up @@ -4180,23 +4187,23 @@ def callback(_args); end
variation_to_return,
Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
)
decision_list_to_return = [[decision_to_return, []]]
allow(project_instance.event_dispatcher).to receive(:dispatch_event).with(instance_of(Optimizely::Event))
allow(project_instance.decision_service).to receive(:get_variation_for_feature).and_return(decision_to_return)
allow(project_instance.decision_service).to receive(:get_variations_for_feature_list).and_return(decision_list_to_return)
user_context = project_instance.create_user_context('user1')

expect(project_instance.decision_service).to receive(:get_variation_for_feature)
expect(project_instance.decision_service).to receive(:get_variations_for_feature_list)
.with(anything, anything, anything, []).once
project_instance.decide(user_context, 'multi_variate_feature')

expect(project_instance.decision_service).to receive(:get_variation_for_feature)
expect(project_instance.decision_service).to receive(:get_variations_for_feature_list)
.with(anything, anything, anything, [Optimizely::Decide::OptimizelyDecideOption::DISABLE_DECISION_EVENT]).once
project_instance.decide(user_context, 'multi_variate_feature', [Optimizely::Decide::OptimizelyDecideOption::DISABLE_DECISION_EVENT])

expect(project_instance.decision_service).to receive(:get_variation_for_feature)
expect(project_instance.decision_service).to receive(:get_variations_for_feature_list)
.with(anything, anything, anything, [
Optimizely::Decide::OptimizelyDecideOption::DISABLE_DECISION_EVENT,
Optimizely::Decide::OptimizelyDecideOption::EXCLUDE_VARIABLES,
Optimizely::Decide::OptimizelyDecideOption::ENABLED_FLAGS_ONLY,
Optimizely::Decide::OptimizelyDecideOption::IGNORE_USER_PROFILE_SERVICE,
Optimizely::Decide::OptimizelyDecideOption::INCLUDE_REASONS,
Optimizely::Decide::OptimizelyDecideOption::EXCLUDE_VARIABLES
Expand Down
0