Skip to content

Commit 7cd21e7

Browse files
lib/optimizely/decision_service.rb -> Removed user profile tracker instantiation.
lib/optimizely/user_profile_tracker.rb -> Improved error logging message. spec/decision_service_spec.rb -> Refactored user profile tracking in tests. spec/project_spec.rb -> Updated decision service method stubs. spec/user_profile_tracker.rb -> Updated lookup, update and save tests for user_profile_tracker
1 parent 061f8bc commit 7cd21e7

File tree

5 files changed

+130
-117
lines changed

5 files changed

+130
-117
lines changed

lib/optimizely/decision_service.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,6 @@ def get_variation(project_config, experiment_id, user_context, user_profile_trac
9494
return whitelisted_variation_id, decide_reasons if whitelisted_variation_id
9595

9696
should_ignore_user_profile_service = decide_options.include? Optimizely::Decide::OptimizelyDecideOption::IGNORE_USER_PROFILE_SERVICE
97-
# user_profile_tracker = Optimizely::UserProfileTracker.new(user_context.user_id, @user_profile_service, @logger) if user_profile_tracker.nil?
9897
# Check for saved bucketing decisions if decide_options do not include ignoreUserProfileService
9998
unless should_ignore_user_profile_service && user_profile_tracker
10099
saved_variation_id, reasons_received = get_saved_variation_id(project_config, experiment_id, user_profile_tracker.user_profile)

lib/optimizely/user_profile_tracker.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ def load_user_profile(reasons = [], error_handler = nil)
2323
begin
2424
@user_profile = @user_profile_service.lookup(@user_id) if @user_profile_service
2525
rescue => e
26-
message = "Error while loading user profile in user profile tracker for user ID '#{@user_id}': #{e}."
27-
reasons << e.message
26+
message = "Error while looking up user profile for user ID '#{@user_id}': #{e}."
27+
reasons << message
2828
@logger.log(Logger::ERROR, message)
2929
error_handler&.handle_error(e)
3030
end

spec/decision_service_spec.rb

Lines changed: 22 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -263,50 +263,14 @@
263263
end
264264

265265
describe 'when a UserProfile service is provided' do
266-
it 'should look up the UserProfile, bucket normally, and save the result if no saved profile is found' do
267-
expected_user_profile = {
268-
user_id: 'test_user',
269-
experiment_bucket_map: {
270-
'111127' => {
271-
variation_id: '111128'
272-
}
273-
}
274-
}
275-
expect(spy_user_profile_service).to receive(:lookup).once.and_return(nil)
276-
277-
user_context = project_instance.create_user_context('test_user')
278-
variation_received, reasons = decision_service.get_variation(config, '111127', user_context)
279-
expect(variation_received).to eq('111128')
280-
expect(reasons).to eq([
281-
"Audiences for experiment 'test_experiment' collectively evaluated to TRUE.",
282-
"User 'test_user' is in variation 'control' of experiment '111127'."
283-
])
284-
285-
# bucketing should have occurred
286-
expect(decision_service.bucketer).to have_received(:bucket).once
287-
# bucketing decision should have been saved
288-
expect(spy_user_profile_service).to have_received(:save).once.with(expected_user_profile)
289-
expect(spy_logger).to have_received(:log).once
290-
.with(Logger::INFO, "Saved variation ID 111128 of experiment ID 111127 for user 'test_user'.")
291-
end
292-
293-
it 'should look up the UserProfile, bucket normally (using Bucketing ID attribute), and save the result if no saved profile is found' do
294-
expected_user_profile = {
295-
user_id: 'test_user',
296-
experiment_bucket_map: {
297-
'111127' => {
298-
variation_id: '111129'
299-
}
300-
}
301-
}
266+
it 'bucket normally (using Bucketing ID attribute)' do
302267
user_attributes = {
303268
'browser_type' => 'firefox',
304269
Optimizely::Helpers::Constants::CONTROL_ATTRIBUTES['BUCKETING_ID'] => 'pid'
305270
}
306-
expect(spy_user_profile_service).to receive(:lookup).once.and_return(nil)
307-
308271
user_context = project_instance.create_user_context('test_user', user_attributes)
309-
variation_received, reasons = decision_service.get_variation(config, '111127', user_context)
272+
user_profile_tracker = Optimizely::UserProfileTracker.new(user_context.user_id, spy_user_profile_service, spy_logger)
273+
variation_received, reasons = decision_service.get_variation(config, '111127', user_context, user_profile_tracker)
310274
expect(variation_received).to eq('111129')
311275
expect(reasons).to eq([
312276
"Audiences for experiment 'test_experiment' collectively evaluated to TRUE.",
@@ -315,13 +279,9 @@
315279

316280
# bucketing should have occurred
317281
expect(decision_service.bucketer).to have_received(:bucket).once
318-
# bucketing decision should have been saved
319-
expect(spy_user_profile_service).to have_received(:save).once.with(expected_user_profile)
320-
expect(spy_logger).to have_received(:log).once
321-
.with(Logger::INFO, "Saved variation ID 111129 of experiment ID 111127 for user 'test_user'.")
322282
end
323283

324-
it 'should look up the user profile and skip normal bucketing if a profile with a saved decision is found' do
284+
it 'skip normal bucketing if a profile with a saved decision is found' do
325285
saved_user_profile = {
326286
user_id: 'test_user',
327287
experiment_bucket_map: {
@@ -334,7 +294,9 @@
334294
.with('test_user').once.and_return(saved_user_profile)
335295

336296
user_context = project_instance.create_user_context('test_user')
337-
variation_received, reasons = decision_service.get_variation(config, '111127', user_context)
297+
user_profile_tracker = Optimizely::UserProfileTracker.new(user_context.user_id, spy_user_profile_service, spy_logger)
298+
user_profile_tracker.load_user_profile
299+
variation_received, reasons = decision_service.get_variation(config, '111127', user_context, user_profile_tracker)
338300
expect(variation_received).to eq('111129')
339301
expect(reasons).to eq([
340302
"Returning previously activated variation ID 111129 of experiment 'test_experiment' for user 'test_user' from user profile."
@@ -350,7 +312,7 @@
350312
expect(spy_user_profile_service).not_to have_received(:save)
351313
end
352314

353-
it 'should look up the user profile and bucket normally if a profile without a saved decision is found' do
315+
it 'bucket normally if a profile without a saved decision is found' do
354316
saved_user_profile = {
355317
user_id: 'test_user',
356318
experiment_bucket_map: {
@@ -364,7 +326,9 @@
364326
.once.with('test_user').and_return(saved_user_profile)
365327

366328
user_context = project_instance.create_user_context('test_user')
367-
variation_received, reasons = decision_service.get_variation(config, '111127', user_context)
329+
user_profile_tracker = Optimizely::UserProfileTracker.new(user_context.user_id, spy_user_profile_service, spy_logger)
330+
user_profile_tracker.load_user_profile
331+
variation_received, reasons = decision_service.get_variation(config, '111127', user_context, user_profile_tracker)
368332
expect(variation_received).to eq('111128')
369333
expect(reasons).to eq([
370334
"Audiences for experiment 'test_experiment' collectively evaluated to TRUE.",
@@ -373,20 +337,6 @@
373337

374338
# bucketing should have occurred
375339
expect(decision_service.bucketer).to have_received(:bucket).once
376-
377-
# user profile should have been updated with bucketing decision
378-
expected_user_profile = {
379-
user_id: 'test_user',
380-
experiment_bucket_map: {
381-
'111127' => {
382-
variation_id: '111128'
383-
},
384-
'122227' => {
385-
variation_id: '122228'
386-
}
387-
}
388-
}
389-
expect(spy_user_profile_service).to have_received(:save).once.with(expected_user_profile)
390340
end
391341

392342
it 'should bucket normally if the user profile contains a variation ID not in the datafile' do
@@ -403,7 +353,9 @@
403353
.once.with('test_user').and_return(saved_user_profile)
404354

405355
user_context = project_instance.create_user_context('test_user')
406-
variation_received, reasons = decision_service.get_variation(config, '111127', user_context)
356+
user_profile_tracker = Optimizely::UserProfileTracker.new(user_context.user_id, spy_user_profile_service, spy_logger)
357+
user_profile_tracker.load_user_profile
358+
variation_received, reasons = decision_service.get_variation(config, '111127', user_context, user_profile_tracker)
407359
expect(variation_received).to eq('111128')
408360
expect(reasons).to eq([
409361
"User 'test_user' was previously bucketed into variation ID '111111' for experiment '111127', but no matching variation was found. Re-bucketing user.",
@@ -413,27 +365,18 @@
413365

414366
# bucketing should have occurred
415367
expect(decision_service.bucketer).to have_received(:bucket).once
416-
417-
# user profile should have been updated with bucketing decision
418-
expected_user_profile = {
419-
user_id: 'test_user',
420-
experiment_bucket_map: {
421-
'111127' => {
422-
variation_id: '111128'
423-
}
424-
}
425-
}
426-
expect(spy_user_profile_service).to have_received(:save).with(expected_user_profile)
427368
end
428369

429-
it 'should bucket normally if the user profile service throws an error during lookup' do
370+
it 'should bucket normally if the user profile tracker throws an error during lookup' do
430371
expect(spy_user_profile_service).to receive(:lookup).once.with('test_user').and_throw(:LookupError)
431372

432373
user_context = project_instance.create_user_context('test_user')
433-
variation_received, reasons = decision_service.get_variation(config, '111127', user_context)
374+
user_profile_tracker = Optimizely::UserProfileTracker.new(user_context.user_id, spy_user_profile_service, spy_logger)
375+
user_profile_tracker.load_user_profile
376+
variation_received, reasons = decision_service.get_variation(config, '111127', user_context, user_profile_tracker)
377+
user_profile_tracker.save_user_profile
434378
expect(variation_received).to eq('111128')
435379
expect(reasons).to eq([
436-
"Error while looking up user profile for user ID 'test_user': uncaught throw :LookupError.",
437380
"Audiences for experiment 'test_experiment' collectively evaluated to TRUE.",
438381
"User 'test_user' is in variation 'control' of experiment '111127'."
439382
])
@@ -444,46 +387,15 @@
444387
expect(decision_service.bucketer).to have_received(:bucket).once
445388
end
446389

447-
it 'should log an error if the user profile service throws an error during save' do
448-
expect(spy_user_profile_service).to receive(:save).once.and_throw(:SaveError)
449-
450-
user_context = project_instance.create_user_context('test_user')
451-
variation_received, reasons = decision_service.get_variation(config, '111127', user_context)
452-
expect(variation_received).to eq('111128')
453-
expect(reasons).to eq([
454-
"Audiences for experiment 'test_experiment' collectively evaluated to TRUE.",
455-
"User 'test_user' is in variation 'control' of experiment '111127'."
456-
])
457-
458-
expect(spy_logger).to have_received(:log).once
459-
.with(Logger::ERROR, "Error while saving user profile for user ID 'test_user': uncaught throw :SaveError.")
460-
end
461-
462390
describe 'IGNORE_USER_PROFILE_SERVICE decide option' do
463391
it 'should ignore user profile service if this option is set' do
464392
allow(spy_user_profile_service).to receive(:lookup)
465393
.with('test_user').once.and_return(nil)
466394

467395
user_context = project_instance.create_user_context('test_user', nil)
468-
variation_received, reasons = decision_service.get_variation(config, '111127', user_context, [Optimizely::Decide::OptimizelyDecideOption::IGNORE_USER_PROFILE_SERVICE])
469-
expect(variation_received).to eq('111128')
470-
expect(reasons).to eq([
471-
"Audiences for experiment 'test_experiment' collectively evaluated to TRUE.",
472-
"User 'test_user' is in variation 'control' of experiment '111127'."
473-
])
474-
475-
expect(decision_service.bucketer).to have_received(:bucket)
476-
expect(Optimizely::Audience).to have_received(:user_meets_audience_conditions?)
477-
expect(spy_user_profile_service).not_to have_received(:lookup)
478-
expect(spy_user_profile_service).not_to have_received(:save)
479-
end
480-
481-
it 'should not ignore user profile service if this option is not set' do
482-
allow(spy_user_profile_service).to receive(:lookup)
483-
.with('test_user').once.and_return(nil)
484-
485-
user_context = project_instance.create_user_context('test_user')
486-
variation_received, reasons = decision_service.get_variation(config, '111127', user_context)
396+
user_profile_tracker = Optimizely::UserProfileTracker.new(user_context.user_id, spy_user_profile_service, spy_logger)
397+
user_profile_tracker.load_user_profile
398+
variation_received, reasons = decision_service.get_variation(config, '111127', user_context, user_profile_tracker, [Optimizely::Decide::OptimizelyDecideOption::IGNORE_USER_PROFILE_SERVICE])
487399
expect(variation_received).to eq('111128')
488400
expect(reasons).to eq([
489401
"Audiences for experiment 'test_experiment' collectively evaluated to TRUE.",
@@ -492,8 +404,6 @@
492404

493405
expect(decision_service.bucketer).to have_received(:bucket)
494406
expect(Optimizely::Audience).to have_received(:user_meets_audience_conditions?)
495-
expect(spy_user_profile_service).to have_received(:lookup)
496-
expect(spy_user_profile_service).to have_received(:save)
497407
end
498408
end
499409
end

spec/project_spec.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4407,8 +4407,9 @@ def callback(_args); end
44074407
variation_to_return,
44084408
Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
44094409
)
4410+
decision_list_to_return = [[decision_to_return, []]]
44104411
allow(custom_project_instance.event_dispatcher).to receive(:dispatch_event).with(instance_of(Optimizely::Event))
4411-
allow(custom_project_instance.decision_service).to receive(:get_variation_for_feature).and_return(decision_to_return)
4412+
allow(custom_project_instance.decision_service).to receive(:get_variations_for_feature_list).and_return(decision_list_to_return)
44124413
user_context = custom_project_instance.create_user_context('user1')
44134414
decision = custom_project_instance.decide(user_context, 'multi_variate_feature')
44144415
expect(decision.as_json).to include(
@@ -4435,8 +4436,9 @@ def callback(_args); end
44354436
variation_to_return,
44364437
Optimizely::DecisionService::DECISION_SOURCES['FEATURE_TEST']
44374438
)
4439+
decision_list_to_return = [[decision_to_return, []]]
44384440
allow(custom_project_instance.event_dispatcher).to receive(:dispatch_event).with(instance_of(Optimizely::Event))
4439-
allow(custom_project_instance.decision_service).to receive(:get_variation_for_feature).and_return(decision_to_return)
4441+
allow(custom_project_instance.decision_service).to receive(:get_variations_for_feature_list).and_return(decision_list_to_return)
44404442
user_context = custom_project_instance.create_user_context('user1')
44414443
decision = custom_project_instance.decide(user_context, 'multi_variate_feature')
44424444
expect(decision.as_json).to include(

spec/user_profile_tracker_spec.rb

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
require 'rspec'
5+
6+
RSpec.describe Optimizely::UserProfileTracker do
7+
let(:user_id) { 'test_user' }
8+
let(:mock_user_profile_service) { instance_double('UserProfileService') }
9+
let(:mock_logger) { instance_double('Logger') }
10+
let(:user_profile_tracker) { described_class.new(user_id, mock_user_profile_service, mock_logger) }
11+
12+
describe '#initialize' do
13+
it 'initializes with a user ID and default values' do
14+
tracker = described_class.new(user_id)
15+
expect(tracker.user_profile[:user_id]).to eq(user_id)
16+
expect(tracker.user_profile[:experiment_bucket_map]).to eq({})
17+
end
18+
19+
it 'accepts a user profile service and logger' do
20+
expect(user_profile_tracker.instance_variable_get(:@user_profile_service)).to eq(mock_user_profile_service)
21+
expect(user_profile_tracker.instance_variable_get(:@logger)).to eq(mock_logger)
22+
end
23+
end
24+
25+
describe '#load_user_profile' do
26+
it 'loads the user profile from the service if provided' do
27+
expected_profile = {
28+
user_id: user_id,
29+
experiment_bucket_map: { '111127' => { variation_id: '111128' } }
30+
}
31+
allow(mock_user_profile_service).to receive(:lookup).with(user_id).and_return(expected_profile)
32+
user_profile_tracker.load_user_profile
33+
expect(user_profile_tracker.user_profile).to eq(expected_profile)
34+
end
35+
36+
it 'handles errors during lookup and logs them' do
37+
allow(mock_user_profile_service).to receive(:lookup).with(user_id).and_raise(StandardError.new('lookup error'))
38+
allow(mock_logger).to receive(:log)
39+
40+
reasons = []
41+
user_profile_tracker.load_user_profile(reasons)
42+
43+
expect(reasons).to include('lookup error')
44+
expect(mock_logger).to have_received(:log).with(Logger::ERROR, "Error while loading user profile in user profile tracker for user ID 'test_user': lookup error.")
45+
end
46+
47+
it 'does nothing if reasons array is nil' do
48+
expect(mock_user_profile_service).not_to receive(:lookup)
49+
user_profile_tracker.load_user_profile(nil)
50+
end
51+
end
52+
53+
describe '#update_user_profile' do
54+
let(:experiment_id) { '111127' }
55+
let(:variation_id) { '111128' }
56+
57+
before do
58+
allow(mock_logger).to receive(:log)
59+
end
60+
61+
it 'updates the experiment bucket map with the given experiment and variation IDs' do
62+
user_profile_tracker.update_user_profile(experiment_id, variation_id)
63+
64+
# Verify the experiment and variation were added
65+
expect(user_profile_tracker.user_profile[:experiment_bucket_map][experiment_id][:variation_id]).to eq(variation_id)
66+
# Verify the profile_updated flag was set
67+
expect(user_profile_tracker.instance_variable_get(:@profile_updated)).to eq(true)
68+
# Verify a log message was recorded
69+
expect(mock_logger).to have_received(:log).with(Logger::INFO, "Updated variation ID #{variation_id} of experiment ID #{experiment_id} for user 'test_user'.")
70+
end
71+
end
72+
73+
describe '#save_user_profile' do
74+
it 'saves the user profile if updates were made and service is available' do
75+
allow(mock_user_profile_service).to receive(:save)
76+
allow(mock_logger).to receive(:log)
77+
78+
user_profile_tracker.update_user_profile('111127', '111128')
79+
user_profile_tracker.save_user_profile
80+
81+
expect(mock_user_profile_service).to have_received(:save).with(user_profile_tracker.user_profile)
82+
expect(mock_logger).to have_received(:log).with(Logger::INFO, "Saved user profile for user 'test_user'.")
83+
end
84+
85+
it 'does not save the user profile if no updates were made' do
86+
allow(mock_user_profile_service).to receive(:save)
87+
88+
user_profile_tracker.save_user_profile
89+
expect(mock_user_profile_service).not_to have_received(:save)
90+
end
91+
92+
it 'handles errors during save and logs them' do
93+
allow(mock_user_profile_service).to receive(:save).and_raise(StandardError.new('save error'))
94+
allow(mock_logger).to receive(:log)
95+
96+
user_profile_tracker.update_user_profile('111127', '111128')
97+
user_profile_tracker.save_user_profile
98+
99+
expect(mock_logger).to have_received(:log).with(Logger::ERROR, "Failed to save user profile for user 'test_user': save error.")
100+
end
101+
end
102+
end

0 commit comments

Comments
 (0)