Skip to content

Commit 0e9e4f8

Browse files
update: Integrate CMAB components into Project class and enhance decision handling
1 parent f48dbc2 commit 0e9e4f8

File tree

5 files changed

+188
-83
lines changed

5 files changed

+188
-83
lines changed

lib/optimizely.rb

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,17 @@
4343
require_relative 'optimizely/odp/odp_manager'
4444
require_relative 'optimizely/helpers/sdk_settings'
4545
require_relative 'optimizely/user_profile_tracker'
46+
require_relative 'optimizely/cmab/cmab_client'
47+
require_relative 'optimizely/cmab/cmab_service'
4648

4749
module Optimizely
4850
class Project
4951
include Optimizely::Decide
5052

53+
# CMAB Constants
54+
DEFAULT_CMAB_CACHE_TIMEOUT = (30 * 60 * 1000)
55+
DEFAULT_CMAB_CACHE_SIZE = 1000
56+
5157
attr_reader :notification_center
5258
# @api no-doc
5359
attr_reader :config_manager, :decision_service, :error_handler, :event_dispatcher,
@@ -131,7 +137,19 @@ def initialize(
131137

132138
setup_odp!(@config_manager.sdk_key)
133139

134-
@decision_service = DecisionService.new(@logger, @user_profile_service)
140+
# Initialize CMAB components
141+
@cmab_client = DefaultCmabClient.new(
142+
retry_config: CmabRetryConfig.new,
143+
logger: @logger
144+
)
145+
@cmab_cache = LRUCache.new(DEFAULT_CMAB_CACHE_SIZE, DEFAULT_CMAB_CACHE_TIMEOUT)
146+
@cmab_service = DefaultCmabService.new(
147+
@cmab_cache,
148+
@cmab_client,
149+
@logger
150+
)
151+
152+
@decision_service = DecisionService.new(@logger, @cmab_service, @user_profile_service)
135153

136154
@event_processor = if event_processor.respond_to?(:process)
137155
event_processor
@@ -358,9 +376,17 @@ def decide_for_keys(user_context, keys, decide_options = [], ignore_default_opti
358376
decision_list = @decision_service.get_variations_for_feature_list(config, flags_without_forced_decision, user_context, decide_options)
359377

360378
flags_without_forced_decision.each_with_index do |flag, i|
361-
decision = decision_list[i][0]
362-
reasons = decision_list[i][1]
379+
decision = decision_list[i].decision
380+
reasons = decision_list[i].reasons
381+
error = decision_list[i].error
363382
flag_key = flag['key']
383+
# store error decision against key and remove key from valid keys
384+
if error
385+
optimizely_decision = OptimizelyDecision.new_error_decision(flag_key, user_context, reasons)
386+
decisions[flag_key] = optimizely_decision
387+
valid_keys.delete(flag_key) if valid_keys.include?(flag_key)
388+
next
389+
end
364390
flag_decisions[flag_key] = decision
365391
decision_reasons_dict[flag_key] ||= []
366392
decision_reasons_dict[flag_key].push(*reasons)
@@ -599,8 +625,8 @@ def is_feature_enabled(feature_flag_key, user_id, attributes = nil)
599625
end
600626

601627
user_context = OptimizelyUserContext.new(self, user_id, attributes, identify: false)
602-
decision, = @decision_service.get_variation_for_feature(config, feature_flag, user_context)
603-
628+
decision_result = @decision_service.get_variation_for_feature(config, feature_flag, user_context)
629+
decision = decision_result.decision
604630
feature_enabled = false
605631
source_string = Optimizely::DecisionService::DECISION_SOURCES['ROLLOUT']
606632
if decision.is_a?(Optimizely::DecisionService::Decision)
@@ -839,7 +865,8 @@ def get_all_feature_variables(feature_flag_key, user_id, attributes = nil)
839865
end
840866

841867
user_context = OptimizelyUserContext.new(self, user_id, attributes, identify: false)
842-
decision, = @decision_service.get_variation_for_feature(config, feature_flag, user_context)
868+
decision_result = @decision_service.get_variation_for_feature(config, feature_flag, user_context)
869+
decision = decision_result.decision
843870
variation = decision ? decision['variation'] : nil
844871
feature_enabled = variation ? variation['featureEnabled'] : false
845872
all_variables = {}
@@ -1029,7 +1056,8 @@ def get_variation_with_config(experiment_key, user_id, attributes, config)
10291056
user_context = OptimizelyUserContext.new(self, user_id, attributes, identify: false)
10301057
user_profile_tracker = UserProfileTracker.new(user_id, @user_profile_service, @logger)
10311058
user_profile_tracker.load_user_profile
1032-
variation_id, = @decision_service.get_variation(config, experiment_id, user_context, user_profile_tracker)
1059+
variation_result = @decision_service.get_variation(config, experiment_id, user_context, user_profile_tracker)
1060+
variation_id = variation_result.variation_id
10331061
user_profile_tracker.save_user_profile
10341062
variation = config.get_variation_from_id(experiment_key, variation_id) unless variation_id.nil?
10351063
variation_key = variation['key'] if variation
@@ -1097,7 +1125,8 @@ def get_feature_variable_for_type(feature_flag_key, variable_key, variable_type,
10971125
end
10981126

10991127
user_context = OptimizelyUserContext.new(self, user_id, attributes, identify: false)
1100-
decision, = @decision_service.get_variation_for_feature(config, feature_flag, user_context)
1128+
decision_result = @decision_service.get_variation_for_feature(config, feature_flag, user_context)
1129+
decision = decision_result.decision
11011130
variation = decision ? decision['variation'] : nil
11021131
feature_enabled = variation ? variation['featureEnabled'] : false
11031132

lib/optimizely/decide/optimizely_decision.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,25 @@ def as_json
5555
def to_json(*args)
5656
as_json.to_json(*args)
5757
end
58+
59+
# Create a new OptimizelyDecision representing an error state.
60+
#
61+
# @param key [String] The flag key
62+
# @param user [OptimizelyUserContext] The user context
63+
# @param reasons [Array<String>] List of reasons explaining the error
64+
#
65+
# @return [OptimizelyDecision] OptimizelyDecision with error state values
66+
def self.new_error_decision(key, user, reasons = [])
67+
new(
68+
variation_key: nil,
69+
enabled: false,
70+
variables: {},
71+
rule_key: nil,
72+
flag_key: key,
73+
user_context: user,
74+
reasons: reasons
75+
)
76+
end
5877
end
5978
end
6079
end

lib/optimizely/decision_service.rb

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,7 @@ def get_variation(project_config, experiment_id, user_context, user_profile_trac
6666
# user_profile_tracker: Tracker for reading and updating user profile of the user.
6767
# reasons: Decision reasons.
6868
#
69-
# Returns variation ID where visitor will be bucketed
70-
# (nil if experiment is inactive or user does not meet audience conditions)
69+
# Returns VariationResult struct
7170
user_profile_tracker = UserProfileTracker.new(user_context.user_id, @user_profile_service, @logger) unless user_profile_tracker.is_a?(Optimizely::UserProfileTracker)
7271
decide_reasons = []
7372
decide_reasons.push(*reasons)
@@ -189,7 +188,12 @@ def get_variations_for_feature_list(project_config, feature_flags, user_context,
189188
feature_flags.each do |feature_flag|
190189
# check if the feature is being experiment on and whether the user is bucketed into the experiment
191190
decision_result = get_variation_for_feature_experiment(project_config, feature_flag, user_context, user_profile_tracker, decide_options)
192-
decision_result = get_variation_for_feature_rollout(project_config, feature_flag, user_context) unless decision_result.decision
191+
# Only process rollout if no experiment decision was found and no error
192+
if decision_result.decision.nil? && !decision_result.error
193+
decision_result_rollout = get_variation_for_feature_rollout(project_config, feature_flag, user_context) unless decision_result.decision
194+
decision_result.decision = decision_result_rollout.decision
195+
decision_result.reasons.push(*decision_result_rollout.reasons)
196+
end
193197
decisions << decision_result
194198
end
195199
user_profile_tracker&.save_user_profile
@@ -232,7 +236,8 @@ def get_variation_for_feature_experiment(project_config, feature_flag, user_cont
232236
variation_id = variation_result.variation_id
233237
cmab_uuid = variation_result.cmab_uuid
234238
decide_reasons.push(*reasons_received)
235-
239+
puts 'final reasons'
240+
puts decide_reasons
236241
next unless variation_id
237242

238243
variation = project_config.get_variation_from_id_by_experiment_id(experiment_id, variation_id)
@@ -312,10 +317,11 @@ def get_variation_from_experiment_rule(project_config, flag_key, rule, user, use
312317
context = Optimizely::OptimizelyUserContext::OptimizelyDecisionContext.new(flag_key, rule['key'])
313318
variation, forced_reasons = validated_forced_decision(project_config, context, user)
314319
reasons.push(*forced_reasons)
315-
316320
return VariationResult.new(nil, false, reasons, variation['id']) if variation
317321

318-
get_variation(project_config, rule['id'], user, user_profile_tracker, options)
322+
variation_result = get_variation(project_config, rule['id'], user, user_profile_tracker, options)
323+
variation_result.reasons = reasons + variation_result.reasons
324+
variation_result
319325
end
320326

321327
def get_variation_from_delivery_rule(project_config, flag_key, rules, rule_index, user_context)

spec/optimizely_user_context_spec.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,7 @@
556556
decision = user_context_obj.decide(feature_key, [Optimizely::Decide::OptimizelyDecideOption::INCLUDE_REASONS])
557557
expect(decision.variation_key).to eq('18257766532')
558558
expect(decision.rule_key).to eq('18322080788')
559+
# puts decision.reasons
559560
expect(decision.reasons).to include('Invalid variation is mapped to flag (feature_1), rule (exp_with_audience) and user (tester) in the forced decision map.')
560561

561562
# delivery-rule-to-decision

0 commit comments

Comments
 (0)