Skip to content

Commit fc5731e

Browse files
rashidspmikeproeng37
authored andcommitted
implements new decision structure (#74)
1 parent 28da3af commit fc5731e

File tree

4 files changed

+78
-66
lines changed

4 files changed

+78
-66
lines changed

lib/optimizely.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -260,9 +260,9 @@ def is_feature_enabled(feature_flag_key, user_id, attributes = nil)
260260
decision = @decision_service.get_variation_for_feature(feature_flag, user_id, attributes)
261261
unless decision.nil?
262262
variation = decision['variation']
263-
experiment = decision['experiment']
264-
unless experiment.nil?
265-
send_impression(experiment, variation['key'], user_id, attributes)
263+
# Send event if Decision came from an experiment.
264+
if decision.source == Optimizely::DecisionService::DECISION_SOURCE_EXPERIMENT
265+
send_impression(decision.experiment, variation['key'], user_id, attributes)
266266
else
267267
@logger.log(Logger::DEBUG,
268268
"The user '#{user_id}' is not being experimented on in feature '#{feature_flag_key}'.")
@@ -273,7 +273,7 @@ def is_feature_enabled(feature_flag_key, user_id, attributes = nil)
273273
end
274274

275275
@logger.log(Logger::INFO,
276-
"Feature '#{feature_flag_key}' is not enabled for user '#{user_id}'.")
276+
"Feature '#{feature_flag_key}' is not enabled for user '#{user_id}'.")
277277
false
278278
end
279279

lib/optimizely/decision_service.rb

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ class DecisionService
3434
attr_reader :bucketer
3535
attr_reader :config
3636

37+
Decision = Struct.new(:experiment, :variation, :source)
38+
DECISION_SOURCE_EXPERIMENT = 'experiment'.freeze
39+
DECISION_SOURCE_ROLLOUT = 'rollout'.freeze
40+
3741
def initialize(config, user_profile_service = nil)
3842
@config = config
3943
@user_profile_service = user_profile_service
@@ -105,31 +109,28 @@ def get_variation_for_feature(feature_flag, user_id, attributes = nil)
105109
# user_id - String ID for the user
106110
# attributes - Hash representing user attributes
107111
#
108-
# Returns hash with the experiment and variation where visitor will be bucketed (nil if the user is not bucketed into any of the experiments on the feature)
112+
# Returns Decision struct (nil if the user is not bucketed into any of the experiments on the feature)
109113

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

114118
feature_flag_key = feature_flag['key']
115-
variation = get_variation_for_feature_rollout(feature_flag, user_id, attributes)
116-
if variation
119+
decision = get_variation_for_feature_rollout(feature_flag, user_id, attributes)
120+
if decision
117121
@config.logger.log(
118122
Logger::INFO,
119123
"User '#{user_id}' is bucketed into a rollout for feature flag '#{feature_flag_key}'."
120124
)
121-
# return decision with nil experiment so we don't track impressions for it
122-
return {
123-
'experiment' => nil,
124-
'variation' => variation
125-
}
126-
else
127-
@config.logger.log(
128-
Logger::INFO,
129-
"User '#{user_id}' is not bucketed into a rollout for feature flag '#{feature_flag_key}'."
130-
)
125+
126+
return decision
131127
end
132128

129+
@config.logger.log(
130+
Logger::INFO,
131+
"User '#{user_id}' is not bucketed into a rollout for feature flag '#{feature_flag_key}'."
132+
)
133+
133134
nil
134135
end
135136

@@ -140,7 +141,7 @@ def get_variation_for_feature_experiment(feature_flag, user_id, attributes = nil
140141
# user_id - String ID for the user
141142
# attributes - Hash representing user attributes
142143
#
143-
# Returns a hash with the experiment and variation where visitor will be bucketed
144+
# Returns Decision struct (nil if the user is not bucketed into any of the experiments on the feature)
144145
# or nil if the user is not bucketed into any of the experiments on the feature
145146
feature_flag_key = feature_flag['key']
146147
if feature_flag['experimentIds'].empty?
@@ -150,6 +151,7 @@ def get_variation_for_feature_experiment(feature_flag, user_id, attributes = nil
150151
)
151152
return nil
152153
end
154+
153155
# Evaluate each experiment and return the first bucketed experiment variation
154156
feature_flag['experimentIds'].each do |experiment_id|
155157
experiment = @config.experiment_id_map[experiment_id]
@@ -160,18 +162,19 @@ def get_variation_for_feature_experiment(feature_flag, user_id, attributes = nil
160162
)
161163
return nil
162164
end
165+
163166
experiment_key = experiment['key']
164167
variation_id = get_variation(experiment_key, user_id, attributes)
168+
165169
next unless variation_id
166170
variation = @config.variation_id_map[experiment_key][variation_id]
167-
@config.logger.log(Logger::INFO,
168-
"The user '#{user_id}' is bucketed into experiment '#{experiment_key}' of feature "\
169-
"'#{feature_flag_key}'.")
170-
return {
171-
'experiment' => experiment,
172-
'variation' => variation
173-
}
171+
@config.logger.log(
172+
Logger::INFO,
173+
"The user '#{user_id}' is bucketed into experiment '#{experiment_key}' of feature '#{feature_flag_key}'."
174+
)
175+
return Decision.new(experiment, variation, DECISION_SOURCE_EXPERIMENT)
174176
end
177+
175178
@config.logger.log(
176179
Logger::INFO,
177180
"The user '#{user_id}' is not bucketed into any of the experiments on the feature '#{feature_flag_key}'."
@@ -188,10 +191,9 @@ def get_variation_for_feature_rollout(feature_flag, user_id, attributes = nil)
188191
# user_id - String ID for the user
189192
# attributes - Hash representing user attributes
190193
#
191-
# Returns the variation the user is bucketed into or nil if not bucketed into any of the targeting rules
194+
# Returns the Decision struct or nil if not bucketed into any of the targeting rules
192195

193196
bucketing_id = get_bucketing_id(user_id, attributes)
194-
195197
rollout_id = feature_flag['rolloutId']
196198
if rollout_id.nil? || rollout_id.empty?
197199
feature_flag_key = feature_flag['key']
@@ -233,14 +235,15 @@ def get_variation_for_feature_rollout(feature_flag, user_id, attributes = nil)
233235
next
234236
end
235237

238+
# User failed traffic allocation, jump to Everyone Else rule
236239
@config.logger.log(
237240
Logger::DEBUG,
238241
"Attempting to bucket user '#{user_id}' into rollout rule for audience '#{audience_name}'."
239242
)
243+
240244
# Evaluate if user satisfies the traffic allocation for this rollout rule
241245
variation = @bucketer.bucket(rollout_rule, bucketing_id, user_id)
242-
return variation unless variation.nil?
243-
246+
return Decision.new(rollout_rule, variation, DECISION_SOURCE_ROLLOUT) unless variation.nil?
244247
# User failed traffic allocation, jump to Everyone Else rule
245248
@config.logger.log(
246249
Logger::DEBUG,
@@ -253,7 +256,7 @@ def get_variation_for_feature_rollout(feature_flag, user_id, attributes = nil)
253256
# get last rule which is the everyone else rule
254257
everyone_else_experiment = rollout_rules[number_of_rules]
255258
variation = @bucketer.bucket(everyone_else_experiment, bucketing_id, user_id)
256-
return variation unless variation.nil?
259+
return Decision.new(everyone_else_experiment, variation, DECISION_SOURCE_ROLLOUT) unless variation.nil?
257260

258261
@config.logger.log(
259262
Logger::DEBUG,

spec/decision_service_spec.rb

Lines changed: 32 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,6 @@
349349
# any string that is not an experiment id in the data file
350350
feature_flag['experimentIds'] = ['1333333337']
351351
expect(decision_service.get_variation_for_feature_experiment(feature_flag, user_id, user_attributes)).to eq(nil)
352-
353352
expect(spy_logger).to have_received(:log).once
354353
.with(Logger::DEBUG, "Feature flag experiment with ID '1333333337' is not in the datafile.")
355354
end
@@ -384,10 +383,11 @@
384383
it 'should return the variation' do
385384
user_attributes = {}
386385
feature_flag = config.feature_flag_key_map['multi_variate_feature']
387-
expected_decision = {
388-
'experiment' => config.experiment_key_map['test_experiment_multivariate'],
389-
'variation' => config.variation_id_map['test_experiment_multivariate']['122231']
390-
}
386+
expected_decision = Optimizely::DecisionService::Decision.new(
387+
config.experiment_key_map['test_experiment_multivariate'],
388+
config.variation_id_map['test_experiment_multivariate']['122231'],
389+
Optimizely::DecisionService::DECISION_SOURCE_EXPERIMENT
390+
)
391391
expect(decision_service.get_variation_for_feature_experiment(feature_flag, 'user_1', user_attributes)).to eq(expected_decision)
392392

393393
expect(spy_logger).to have_received(:log).once
@@ -402,13 +402,14 @@
402402
describe 'and the user is bucketed into one of the experiments' do
403403
before(:each) do
404404
mutex_exp = config.experiment_key_map['group1_exp1']
405-
expected_variation = mutex_exp['variations'][0]
406-
expected_decision = {
407-
'experiment' => mutex_exp,
408-
'variation' => expected_variation
409-
}
405+
variation = mutex_exp['variations'][0]
406+
expected_decision = Optimizely::DecisionService::Decision.new(
407+
mutex_exp,
408+
variation,
409+
Optimizely::DecisionService::DECISION_SOURCE_EXPERIMENT
410+
)
410411
allow(decision_service).to receive(:get_variation)
411-
.and_return(expected_variation['id'])
412+
.and_return(variation['id'])
412413
end
413414

414415
it 'should return the variation the user is bucketed into' do
@@ -483,15 +484,16 @@
483484
it 'should return the variation the user is bucketed into' do
484485
feature_flag = config.feature_flag_key_map['boolean_single_variable_feature']
485486
rollout_experiment = config.rollout_id_map[feature_flag['rolloutId']]['experiments'][0]
486-
expected_variation = rollout_experiment['variations'][0]
487+
variation = rollout_experiment['variations'][0]
487488
audience_id = rollout_experiment['audienceIds'][0]
488489
audience_name = config.get_audience_from_id(audience_id)['name']
490+
expected_decision = Optimizely::DecisionService::Decision.new(rollout_experiment, variation, Optimizely::DecisionService::DECISION_SOURCE_ROLLOUT)
489491

490492
allow(Optimizely::Audience).to receive(:user_in_experiment?).and_return(true)
491493
allow(decision_service.bucketer).to receive(:bucket)
492494
.with(rollout_experiment, user_id, user_id)
493-
.and_return(expected_variation)
494-
expect(decision_service.get_variation_for_feature_rollout(feature_flag, user_id, user_attributes)).to eq(expected_variation)
495+
.and_return(variation)
496+
expect(decision_service.get_variation_for_feature_rollout(feature_flag, user_id, user_attributes)).to eq(expected_decision)
495497

496498
expect(spy_logger).to have_received(:log).once
497499
.with(Logger::DEBUG, "Attempting to bucket user '#{user_id}' into rollout rule "\
@@ -542,17 +544,17 @@
542544
feature_flag = config.feature_flag_key_map['boolean_single_variable_feature']
543545
rollout = config.rollout_id_map[feature_flag['rolloutId']]
544546
everyone_else_experiment = rollout['experiments'][2]
545-
expected_variation = everyone_else_experiment['variations'][0]
546-
547+
variation = everyone_else_experiment['variations'][0]
548+
expected_decision = Optimizely::DecisionService::Decision.new(everyone_else_experiment, variation, Optimizely::DecisionService::DECISION_SOURCE_ROLLOUT)
547549
allow(Optimizely::Audience).to receive(:user_in_experiment?).and_return(true)
548550
allow(decision_service.bucketer).to receive(:bucket)
549551
.with(rollout['experiments'][0], user_id, user_id)
550552
.and_return(nil)
551553
allow(decision_service.bucketer).to receive(:bucket)
552554
.with(everyone_else_experiment, user_id, user_id)
553-
.and_return(expected_variation)
555+
.and_return(variation)
554556

555-
expect(decision_service.get_variation_for_feature_rollout(feature_flag, user_id, user_attributes)).to eq(expected_variation)
557+
expect(decision_service.get_variation_for_feature_rollout(feature_flag, user_id, user_attributes)).to eq(expected_decision)
556558

557559
# make sure we only checked the audience for the first rule
558560
expect(Optimizely::Audience).to have_received(:user_in_experiment?).once
@@ -579,14 +581,15 @@
579581
feature_flag = config.feature_flag_key_map['boolean_single_variable_feature']
580582
rollout = config.rollout_id_map[feature_flag['rolloutId']]
581583
everyone_else_experiment = rollout['experiments'][2]
582-
expected_variation = everyone_else_experiment['variations'][0]
584+
variation = everyone_else_experiment['variations'][0]
585+
expected_decision = Optimizely::DecisionService::Decision.new(everyone_else_experiment, variation, Optimizely::DecisionService::DECISION_SOURCE_ROLLOUT)
583586

584587
allow(Optimizely::Audience).to receive(:user_in_experiment?).and_return(false)
585588
allow(decision_service.bucketer).to receive(:bucket)
586589
.with(everyone_else_experiment, user_id, user_id)
587-
.and_return(expected_variation)
590+
.and_return(variation)
588591

589-
expect(decision_service.get_variation_for_feature_rollout(feature_flag, user_id, user_attributes)).to eq(expected_variation)
592+
expect(decision_service.get_variation_for_feature_rollout(feature_flag, user_id, user_attributes)).to eq(expected_decision)
590593

591594
# verify we tried to bucket in all targeting rules except for the everyone else rule
592595
expect(Optimizely::Audience).to have_received(:user_in_experiment?).once
@@ -636,13 +639,15 @@
636639
it 'should return the bucketed variation and nil experiment' do
637640
feature_flag = config.feature_flag_key_map['string_single_variable_feature']
638641
rollout = config.rollout_id_map[feature_flag['rolloutId']]
639-
expected_variation = rollout['experiments'][0]['variations'][0]
640-
expected_decision = {
641-
'experiment' => nil,
642-
'variation' => expected_variation
643-
}
642+
variation = rollout['experiments'][0]['variations'][0]
643+
expected_decision = Optimizely::DecisionService::Decision.new(
644+
nil,
645+
variation,
646+
Optimizely::DecisionService::DECISION_SOURCE_ROLLOUT
647+
)
648+
644649
allow(decision_service).to receive(:get_variation_for_feature_experiment).and_return(nil)
645-
allow(decision_service).to receive(:get_variation_for_feature_rollout).and_return(expected_variation)
650+
allow(decision_service).to receive(:get_variation_for_feature_rollout).and_return(expected_decision)
646651

647652
expect(decision_service.get_variation_for_feature(feature_flag, user_id, user_attributes)).to eq(expected_decision)
648653
expect(spy_logger).to have_received(:log).once

spec/project_spec.rb

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -659,10 +659,12 @@ class InvalidErrorHandler; end
659659
it 'should return true but not send an impression if the user is not bucketed into a feature experiment' do
660660
experiment_to_return = config_body['rollouts'][0]['experiments'][0]
661661
variation_to_return = experiment_to_return['variations'][0]
662-
decision_to_return = {
663-
'experiment' => nil,
664-
'variation' => variation_to_return
665-
}
662+
663+
decision_to_return = Optimizely::DecisionService::Decision.new(
664+
experiment_to_return,
665+
variation_to_return,
666+
Optimizely::DecisionService::DECISION_SOURCE_ROLLOUT
667+
)
666668
allow(project_instance.decision_service).to receive(:get_variation_for_feature).and_return(decision_to_return)
667669

668670
expect(project_instance.is_feature_enabled('boolean_single_variable_feature', 'test_user')).to be true
@@ -674,10 +676,12 @@ class InvalidErrorHandler; end
674676
allow(project_instance.event_dispatcher).to receive(:dispatch_event).with(instance_of(Optimizely::Event))
675677
experiment_to_return = config_body['experiments'][3]
676678
variation_to_return = experiment_to_return['variations'][0]
677-
decision_to_return = {
678-
'experiment' => experiment_to_return,
679-
'variation' => variation_to_return
680-
}
679+
decision_to_return = Optimizely::DecisionService::Decision.new(
680+
experiment_to_return,
681+
variation_to_return,
682+
Optimizely::DecisionService::DECISION_SOURCE_EXPERIMENT
683+
)
684+
681685
allow(project_instance.decision_service).to receive(:get_variation_for_feature).and_return(decision_to_return)
682686

683687
expected_params = @expected_bucketed_params
@@ -746,7 +750,7 @@ class InvalidErrorHandler; end
746750

747751
expect(project_instance.get_feature_variable_string('integer_single_variable_feature', 'integer_variable', user_id, user_attributes))
748752
.to eq(nil)
749-
753+
750754
expect(spy_logger).to have_received(:log).once
751755
.with(
752756
Logger::WARN,

0 commit comments

Comments
 (0)