Skip to content

Commit a58748c

Browse files
committed
Merge pull request #373 from andreibondarev/auto_finish
Clean up experiments that are gone, stopped or a winner has been chosen
2 parents 0871647 + a1bc236 commit a58748c

File tree

11 files changed

+104
-27
lines changed

11 files changed

+104
-27
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,8 @@ finished(:experiment_name, reset: false)
165165

166166
The user will then always see the alternative they started with.
167167

168+
Any old unfinished experiment key will be deleted from the user's data storage if the experiment had been removed or is over and a winner had been chosen. This allows a user to enroll into any new experiment in cases when the `allow_multiple_experiments` config option is set to `false`.
169+
168170
### Multiple experiments at once
169171

170172
By default Split will avoid users participating in multiple experiments at once. This means you are less likely to skew results by adding in more variation to your tests.

lib/split.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
persistence
1313
encapsulated_helper
1414
trial
15+
user
1516
version
1617
zscore].each do |f|
1718
require "split/#{f}"

lib/split/encapsulated_helper.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def initialize(context)
2222
end
2323

2424
def ab_user
25-
@ab_user ||= Split::Persistence.adapter.new(@context)
25+
@ab_user ||= Split::User.new(@context)
2626
end
2727
end
2828

lib/split/helper.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ module Helper
66
def ab_test(metric_descriptor, control = nil, *alternatives)
77
begin
88
experiment = ExperimentCatalog.find_or_initialize(metric_descriptor, control, *alternatives)
9-
109
alternative = if Split.configuration.enabled
1110
experiment.save
1211
trial = Trial.new(:user => ab_user, :experiment => experiment,
@@ -94,7 +93,7 @@ def begin_experiment(experiment, alternative_name = nil)
9493
end
9594

9695
def ab_user
97-
@ab_user ||= Split::Persistence.adapter.new(self)
96+
@ab_user ||= User.new(self)
9897
end
9998

10099
def exclude_visitor?

lib/split/trial.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ def complete!(goals=[], context = nil)
5050
# method is guaranteed to only run once, and will skip the alternative choosing process if run
5151
# a second time.
5252
def choose!(context = nil)
53+
@user.cleanup_old_experiments
5354
# Only run the process once
5455
return alternative if @alternative_choosen
5556

lib/split/user.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
module Split
2+
class User
3+
extend Forwardable
4+
def_delegators :@user, :keys, :[], :[]=, :delete
5+
attr_reader :user
6+
7+
def initialize(context)
8+
@user = Split::Persistence.adapter.new(context)
9+
end
10+
11+
def cleanup_old_experiments
12+
user.keys.each do |key|
13+
experiment = ExperimentCatalog.find key_without_version(key)
14+
if experiment.nil? || experiment.has_winner? || experiment.start_time.nil?
15+
user.delete key
16+
end
17+
end
18+
end
19+
20+
def key_without_version(key)
21+
key.split(/\:\d(?!\:)/)[0]
22+
end
23+
end
24+
end

spec/encapsulated_helper_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
before do
88
allow_any_instance_of(Split::EncapsulatedHelper::ContextShim).to receive(:ab_user)
9-
.and_return({})
9+
.and_return(mock_user)
1010
end
1111

1212
def params

spec/helper_spec.rb

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@
172172
it "should only let a user participate in one experiment at a time" do
173173
link_color = ab_test('link_color', 'blue', 'red')
174174
ab_test('button_size', 'small', 'big')
175-
expect(ab_user).to eq({'link_color' => link_color})
175+
expect(ab_user['link_color']).to eq(link_color)
176176
big = Split::Alternative.new('big', 'button_size')
177177
expect(big.participant_count).to eq(0)
178178
small = Split::Alternative.new('small', 'button_size')
@@ -185,17 +185,18 @@
185185
end
186186
link_color = ab_test('link_color', 'blue', 'red')
187187
button_size = ab_test('button_size', 'small', 'big')
188-
expect(ab_user).to eq({'link_color' => link_color, 'button_size' => button_size})
188+
expect(ab_user['link_color']).to eq(link_color)
189+
expect(ab_user['button_size']).to eq(button_size)
189190
button_size_alt = Split::Alternative.new(button_size, 'button_size')
190191
expect(button_size_alt.participant_count).to eq(1)
191192
end
192193

193194
it "should not over-write a finished key when an experiment is on a later version" do
194195
experiment.increment_version
195196
ab_user = { experiment.key => 'blue', experiment.finished_key => true }
196-
finshed_session = ab_user.dup
197+
finished_session = ab_user.dup
197198
ab_test('link_color', 'blue', 'red')
198-
expect(ab_user).to eq(finshed_session)
199+
expect(ab_user).to eq(finished_session)
199200
end
200201
end
201202

@@ -247,7 +248,8 @@
247248

248249
it "should set experiment's finished key if reset is false" do
249250
finished(@experiment_name, {:reset => false})
250-
expect(ab_user).to eq(@experiment.key => @alternative_name, @experiment.finished_key => true)
251+
expect(ab_user[@experiment.key]).to eq(@alternative_name)
252+
expect(ab_user[@experiment.finished_key]).to eq(true)
251253
end
252254

253255
it 'should not increment the counter if reset is false and the experiment has been already finished' do
@@ -279,30 +281,31 @@
279281
end
280282

281283
it "should clear out the user's participation from their session" do
282-
expect(ab_user).to eq(@experiment.key => @alternative_name)
284+
expect(ab_user[@experiment.key]).to eq(@alternative_name)
283285
finished(@experiment_name)
284-
expect(ab_user).to eq({})
286+
expect(ab_user.keys).to be_empty
285287
end
286288

287289
it "should not clear out the users session if reset is false" do
288-
expect(ab_user).to eq(@experiment.key => @alternative_name)
290+
expect(ab_user[@experiment.key]).to eq(@alternative_name)
289291
finished(@experiment_name, {:reset => false})
290-
expect(ab_user).to eq(@experiment.key => @alternative_name, @experiment.finished_key => true)
292+
expect(ab_user[@experiment.key]).to eq(@alternative_name)
293+
expect(ab_user[@experiment.finished_key]).to eq(true)
291294
end
292295

293296
it "should reset the users session when experiment is not versioned" do
294-
expect(ab_user).to eq(@experiment.key => @alternative_name)
297+
expect(ab_user[@experiment.key]).to eq(@alternative_name)
295298
finished(@experiment_name)
296-
expect(ab_user).to eq({})
299+
expect(ab_user.keys).to be_empty
297300
end
298301

299302
it "should reset the users session when experiment is versioned" do
300303
@experiment.increment_version
301304
@alternative_name = ab_test(@experiment_name, *@alternatives)
302305

303-
expect(ab_user).to eq(@experiment.key => @alternative_name)
306+
expect(ab_user[@experiment.key]).to eq(@alternative_name)
304307
finished(@experiment_name)
305-
expect(ab_user).to eq({})
308+
expect(ab_user.keys).to be_empty
306309
end
307310

308311
it "should do nothing where the experiment was not started by this user" do
@@ -337,7 +340,8 @@
337340
experiment = Split::ExperimentCatalog.find :my_experiment
338341

339342
finished :my_experiment
340-
expect(ab_user).to eq(experiment.key => alternative, experiment.finished_key => true)
343+
expect(ab_user[experiment.key]).to eq(alternative)
344+
expect(ab_user[experiment.finished_key]).to eq(true)
341345
end
342346
end
343347

@@ -618,28 +622,28 @@ def should_finish_experiment(experiment_name, should_finish=true)
618622
it "should use version zero if no version is present" do
619623
alternative_name = ab_test('link_color', 'blue', 'red')
620624
expect(experiment.version).to eq(0)
621-
expect(ab_user).to eq({'link_color' => alternative_name})
625+
expect(ab_user['link_color']).to eq(alternative_name)
622626
end
623627

624628
it "should save the version of the experiment to the session" do
625629
experiment.reset
626630
expect(experiment.version).to eq(1)
627631
alternative_name = ab_test('link_color', 'blue', 'red')
628-
expect(ab_user).to eq({'link_color:1' => alternative_name})
632+
expect(ab_user['link_color:1']).to eq(alternative_name)
629633
end
630634

631635
it "should load the experiment even if the version is not 0" do
632636
experiment.reset
633637
expect(experiment.version).to eq(1)
634638
alternative_name = ab_test('link_color', 'blue', 'red')
635-
expect(ab_user).to eq({'link_color:1' => alternative_name})
639+
expect(ab_user['link_color:1']).to eq(alternative_name)
636640
return_alternative_name = ab_test('link_color', 'blue', 'red')
637641
expect(return_alternative_name).to eq(alternative_name)
638642
end
639643

640644
it "should reset the session of a user on an older version of the experiment" do
641645
alternative_name = ab_test('link_color', 'blue', 'red')
642-
expect(ab_user).to eq({'link_color' => alternative_name})
646+
expect(ab_user['link_color']).to eq(alternative_name)
643647
alternative = Split::Alternative.new(alternative_name, 'link_color')
644648
expect(alternative.participant_count).to eq(1)
645649

@@ -656,7 +660,7 @@ def should_finish_experiment(experiment_name, should_finish=true)
656660

657661
it "should cleanup old versions of experiments from the session" do
658662
alternative_name = ab_test('link_color', 'blue', 'red')
659-
expect(ab_user).to eq({'link_color' => alternative_name})
663+
expect(ab_user['link_color']).to eq(alternative_name)
660664
alternative = Split::Alternative.new(alternative_name, 'link_color')
661665
expect(alternative.participant_count).to eq(1)
662666

@@ -666,12 +670,12 @@ def should_finish_experiment(experiment_name, should_finish=true)
666670
expect(alternative.participant_count).to eq(0)
667671

668672
new_alternative_name = ab_test('link_color', 'blue', 'red')
669-
expect(ab_user).to eq({'link_color:1' => new_alternative_name})
673+
expect(ab_user['link_color:1']).to eq(new_alternative_name)
670674
end
671675

672676
it "should only count completion of users on the current version" do
673677
alternative_name = ab_test('link_color', 'blue', 'red')
674-
expect(ab_user).to eq({'link_color' => alternative_name})
678+
expect(ab_user['link_color']).to eq(alternative_name)
675679
alternative = Split::Alternative.new(alternative_name, 'link_color')
676680

677681
experiment.reset

spec/spec_helper.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,15 @@
1919
config.before(:each) do
2020
Split.configuration = Split::Configuration.new
2121
Split.redis.flushall
22-
@ab_user = {}
22+
@ab_user = mock_user
2323
params = nil
2424
end
2525
end
2626

27+
def mock_user
28+
Split::User.new(double(session: {}))
29+
end
30+
2731
def session
2832
@session ||= {}
2933
end

spec/trial_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
require 'split/trial'
44

55
describe Split::Trial do
6-
let(:user) { Split::Persistence.adapter.new(double(session: {})) }
6+
let(:user) { mock_user }
77
let(:experiment) do
88
Split::Experiment.new('basket_text', :alternatives => ['basket', 'cart']).save
99
end

0 commit comments

Comments
 (0)