split 1.4.0 → 1.4.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d5ce44cc3d861186dd8d1acce66eea15acb2f5af
4
- data.tar.gz: 85bd7d57f7e56d0c4ac06ab8eecfbce40ec96060
3
+ metadata.gz: 07f7886845453ce92ac81c33d32927c4c0528d39
4
+ data.tar.gz: 26a46a18c8a8e743ea532be9f9e2e3c63f516f4e
5
5
  SHA512:
6
- metadata.gz: 9aa91bd0caab6c179b0393bb25f075efa1782bee1ce6cc238b470dac58d76b9819d0e5534e18a68e872ee88d81f01f6fba9ac97e89bc0ede5add5dc00b5a16a6
7
- data.tar.gz: 2aca58592f7a381482dbcfaec0ba8b490ccb2f032ad68a3e4a495a8dd56a26a337066929954380188da53fef7a21b21520a0601adb5a7125b3f62aae6dcc9340
6
+ metadata.gz: 1beb35f67693fa2073877c112cda7ba7abec3dfeff05699fdf13275d79ba0b424c982cc7c2811fdde826fcbced38f4e6b34d1e8db56786bf43aa7edff6d0bb92
7
+ data.tar.gz: d5956789a67b6d92d46e86e55b340900f806951a3dfea7419e1889df6844e21ef20a6777420fbb1362a8001f15c30e9114eb6020ec0de8b3bb508c8c58dfc6af
data/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ ## 1.4.1 (April 21st, 2016)
2
+
3
+ Bugfixes:
4
+
5
+ - respect manual start configuration after an experiment has been deleted (@mtyeh411, #372)
6
+
7
+ Misc:
8
+
9
+ - Introduce goals collection to reduce complexity of Experiment#save (@pakallis, #365)
10
+ - Revise specs according to http://betterspecs.org/ (@hkliya, #369)
11
+
1
12
  ## 1.4.0 (April 2nd, 2016)
2
13
 
3
14
  Features:
data/README.md CHANGED
@@ -151,7 +151,7 @@ It is not required to send `SPLIT_DISABLE=false` to activate Split.
151
151
  By default new AB tests will be active right after deployment. In case you would like to start new test a while after
152
152
  the deploy, you can do it by setting the `start_manually` configuration option to `true`.
153
153
 
154
- After choosing this option tests won't be started right after deploy, but after pressing the `Start` button in Split admin dashboard.
154
+ After choosing this option tests won't be started right after deploy, but after pressing the `Start` button in Split admin dashboard. If a test is deleted from the Split dashboard, then it can only be started after pressing the `Start` button whenever being re-initialized.
155
155
 
156
156
  ### Reset after completion
157
157
 
@@ -284,8 +284,12 @@ For example:
284
284
 
285
285
  ``` ruby
286
286
  Split.configure do |config|
287
+ # after experiment reset or deleted
287
288
  config.on_experiment_reset = -> (example) { # Do something on reset }
288
289
  config.on_experiment_delete = -> (experiment) { # Do something else on delete }
290
+ # before experiment reset or deleted
291
+ config.on_before_experiment_reset = -> (example) { # Do something on reset }
292
+ config.on_before_experiment_delete = -> (experiment) { # Do something else on delete }
289
293
  end
290
294
  ```
291
295
 
@@ -445,9 +449,9 @@ Split.configure do |config|
445
449
  alternatives: ["a", "b"],
446
450
  metadata: {
447
451
  "a" => {"text" => "Have a fantastic day"},
448
- "b" => {"text" => "Don't get hit by a bus"},
452
+ "b" => {"text" => "Don't get hit by a bus"}
449
453
  }
450
- },
454
+ }
451
455
  }
452
456
  end
453
457
  ```
@@ -493,7 +497,7 @@ Split.configure do |config|
493
497
  config.experiments = {
494
498
  my_first_experiment: {
495
499
  alternatives: ["a", "b"],
496
- metric: :my_metric,
500
+ metric: :my_metric
497
501
  }
498
502
  }
499
503
  end
@@ -628,7 +632,7 @@ trial.choose!
628
632
  trial.alternative.name
629
633
 
630
634
  # if the goal has been achieved, increment the successful completions for this alternative.
631
- if goal_acheived?
635
+ if goal_achieved?
632
636
  trial.complete!
633
637
  end
634
638
 
data/lib/split.rb CHANGED
@@ -6,6 +6,7 @@
6
6
  experiment
7
7
  experiment_catalog
8
8
  extensions
9
+ goals_collection
9
10
  helper
10
11
  metric
11
12
  persistence
@@ -19,6 +19,8 @@ module Split
19
19
  attr_accessor :on_trial_complete
20
20
  attr_accessor :on_experiment_reset
21
21
  attr_accessor :on_experiment_delete
22
+ attr_accessor :on_before_experiment_reset
23
+ attr_accessor :on_before_experiment_delete
22
24
  attr_accessor :include_rails_helper
23
25
  attr_accessor :beta_probability_simulations
24
26
  attr_accessor :redis_url
@@ -199,6 +201,8 @@ module Split
199
201
  @db_failover_on_db_error = proc{|error|} # e.g. use Rails logger here
200
202
  @on_experiment_reset = proc{|experiment|}
201
203
  @on_experiment_delete = proc{|experiment|}
204
+ @on_before_experiment_reset = proc{|experiment|}
205
+ @on_before_experiment_delete = proc{|experiment|}
202
206
  @db_failover_allow_parameter_override = false
203
207
  @allow_multiple_experiments = false
204
208
  @enabled = true
@@ -23,7 +23,7 @@ module Split
23
23
  if alternatives.empty? && (exp_config = Split.configuration.experiment_for(name))
24
24
  set_alternatives_and_options(
25
25
  alternatives: load_alternatives_from_configuration,
26
- goals: load_goals_from_configuration,
26
+ goals: Split::GoalsCollection.new(@name).load_from_configuration,
27
27
  metadata: load_metadata_from_configuration,
28
28
  resettable: exp_config[:resettable],
29
29
  algorithm: exp_config[:algorithm]
@@ -60,7 +60,7 @@ module Split
60
60
  exp_config = Split.configuration.experiment_for(name)
61
61
  if exp_config
62
62
  alts = load_alternatives_from_configuration
63
- options[:goals] = load_goals_from_configuration
63
+ options[:goals] = Split::GoalsCollection.new(@name).load_from_configuration
64
64
  options[:metadata] = load_metadata_from_configuration
65
65
  options[:resettable] = exp_config[:resettable]
66
66
  options[:algorithm] = exp_config[:algorithm]
@@ -84,21 +84,21 @@ module Split
84
84
  Split.redis.sadd(:experiments, name)
85
85
  start unless Split.configuration.start_manually
86
86
  @alternatives.reverse.each {|a| Split.redis.lpush(name, a.name)}
87
- save_goals
87
+ goals_collection.save
88
88
  save_metadata
89
89
  Split.redis.set(metadata_key, @metadata.to_json) unless @metadata.nil?
90
90
  else
91
91
  existing_alternatives = load_alternatives_from_redis
92
- existing_goals = load_goals_from_redis
92
+ existing_goals = Split::GoalsCollection.new(@name).load_from_redis
93
93
  existing_metadata = load_metadata_from_redis
94
94
  unless existing_alternatives == @alternatives.map(&:name) && existing_goals == @goals && existing_metadata == @metadata
95
95
  reset
96
96
  @alternatives.each(&:delete)
97
- delete_goals
97
+ goals_collection.delete
98
98
  delete_metadata
99
99
  Split.redis.del(@name)
100
100
  @alternatives.reverse.each {|a| Split.redis.lpush(name, a.name)}
101
- save_goals
101
+ goals_collection.save
102
102
  save_metadata
103
103
  end
104
104
  end
@@ -113,9 +113,7 @@ module Split
113
113
  raise ExperimentNotFound.new("Experiment #{@name} not found")
114
114
  end
115
115
  @alternatives.each {|a| a.validate! }
116
- unless @goals.nil? || goals.kind_of?(Array)
117
- raise ArgumentError, 'Goals must be an array'
118
- end
116
+ goals_collection.validate!
119
117
  end
120
118
 
121
119
  def new_record?
@@ -241,6 +239,7 @@ module Split
241
239
  end
242
240
 
243
241
  def reset
242
+ Split.configuration.on_before_experiment_reset.call(self)
244
243
  alternatives.each(&:reset)
245
244
  reset_winner
246
245
  Split.configuration.on_experiment_reset.call(self)
@@ -248,20 +247,20 @@ module Split
248
247
  end
249
248
 
250
249
  def delete
250
+ Split.configuration.on_before_experiment_delete.call(self)
251
+ if Split.configuration.start_manually
252
+ Split.redis.hdel(:experiment_start_times, @name)
253
+ end
251
254
  alternatives.each(&:delete)
252
255
  reset_winner
253
256
  Split.redis.srem(:experiments, name)
254
257
  Split.redis.del(name)
255
- delete_goals
258
+ goals_collection.delete
256
259
  delete_metadata
257
260
  Split.configuration.on_experiment_delete.call(self)
258
261
  increment_version
259
262
  end
260
263
 
261
- def delete_goals
262
- Split.redis.del(goals_key)
263
- end
264
-
265
264
  def delete_metadata
266
265
  Split.redis.del(metadata_key)
267
266
  end
@@ -271,7 +270,7 @@ module Split
271
270
  self.resettable = exp_config['resettable']
272
271
  self.algorithm = exp_config['algorithm']
273
272
  self.alternatives = load_alternatives_from_redis
274
- self.goals = load_goals_from_redis
273
+ self.goals = Split::GoalsCollection.new(@name).load_from_redis
275
274
  self.metadata = load_metadata_from_redis
276
275
  end
277
276
 
@@ -419,19 +418,6 @@ module Split
419
418
  metadata = Split.configuration.experiment_for(@name)[:metadata]
420
419
  end
421
420
 
422
- def load_goals_from_configuration
423
- goals = Split.configuration.experiment_for(@name)[:goals]
424
- if goals.nil?
425
- goals = []
426
- else
427
- goals.flatten
428
- end
429
- end
430
-
431
- def load_goals_from_redis
432
- Split.redis.lrange(goals_key, 0, -1)
433
- end
434
-
435
421
  def load_metadata_from_redis
436
422
  meta = Split.redis.get(metadata_key)
437
423
  JSON.parse(meta) unless meta.nil?
@@ -459,13 +445,14 @@ module Split
459
445
  end
460
446
  end
461
447
 
462
- def save_goals
463
- @goals.reverse.each {|a| Split.redis.lpush(goals_key, a)} unless @goals.nil?
464
- end
465
-
466
448
  def save_metadata
467
449
  Split.redis.set(metadata_key, @metadata.to_json) unless @metadata.nil?
468
450
  end
469
451
 
452
+ private
453
+
454
+ def goals_collection
455
+ Split::GoalsCollection.new(@name, @goals)
456
+ end
470
457
  end
471
458
  end
@@ -0,0 +1,44 @@
1
+ module Split
2
+ class GoalsCollection
3
+
4
+ def initialize(experiment_name, goals=nil)
5
+ @experiment_name = experiment_name
6
+ @goals = goals
7
+ end
8
+
9
+ def load_from_redis
10
+ Split.redis.lrange(goals_key, 0, -1)
11
+ end
12
+
13
+ def load_from_configuration
14
+ goals = Split.configuration.experiment_for(@experiment_name)[:goals]
15
+
16
+ if goals.nil?
17
+ goals = []
18
+ else
19
+ goals.flatten
20
+ end
21
+ end
22
+
23
+ def save
24
+ return false if @goals.nil?
25
+ @goals.reverse.each { |goal| Split.redis.lpush(goals_key, goal) }
26
+ end
27
+
28
+ def validate!
29
+ unless @goals.nil? || @goals.kind_of?(Array)
30
+ raise ArgumentError, 'Goals must be an array'
31
+ end
32
+ end
33
+
34
+ def delete
35
+ Split.redis.del(goals_key)
36
+ end
37
+
38
+ private
39
+
40
+ def goals_key
41
+ "#{@experiment_name}:goals"
42
+ end
43
+ end
44
+ end
data/lib/split/trial.rb CHANGED
@@ -55,7 +55,7 @@ module Split
55
55
 
56
56
  if @options[:override]
57
57
  self.alternative = @options[:override]
58
- elsif @options[:disabled] || !Split.configuration.enabled
58
+ elsif @options[:disabled] || Split.configuration.disabled?
59
59
  self.alternative = @experiment.control
60
60
  elsif @experiment.has_winner?
61
61
  self.alternative = @experiment.winner
data/lib/split/version.rb CHANGED
@@ -2,6 +2,6 @@
2
2
  module Split
3
3
  MAJOR = 1
4
4
  MINOR = 4
5
- PATCH = 0
5
+ PATCH = 1
6
6
  VERSION = [MAJOR, MINOR, PATCH].join('.')
7
7
  end
@@ -35,4 +35,19 @@ describe Split::ExperimentCatalog do
35
35
  expect(subject.find_or_create('my_exp', 'control me').control.to_s).to eq('control me')
36
36
  end
37
37
  end
38
+
39
+ describe '.find' do
40
+ it "should return an existing experiment" do
41
+ experiment = Split::Experiment.new('basket_text', alternatives: ['blue', 'red', 'green'])
42
+ experiment.save
43
+
44
+ experiment = subject.find('basket_text')
45
+
46
+ expect(experiment.name).to eq('basket_text')
47
+ end
48
+
49
+ it "should return nil if experiment not exist" do
50
+ expect(subject.find('non_existent_experiment')).to be_nil
51
+ end
52
+ end
38
53
  end
@@ -104,18 +104,6 @@ describe Split::Experiment do
104
104
  end
105
105
  end
106
106
 
107
- describe 'find' do
108
- it "should return an existing experiment" do
109
- experiment.save
110
- experiment = Split::ExperimentCatalog.find('basket_text')
111
- expect(experiment.name).to eq('basket_text')
112
- end
113
-
114
- it "should return an existing experiment" do
115
- expect(Split::ExperimentCatalog.find('non_existent_experiment')).to be_nil
116
- end
117
- end
118
-
119
107
  describe 'control' do
120
108
  it 'should be the first alternative' do
121
109
  experiment.save
@@ -210,6 +198,18 @@ describe Split::Experiment do
210
198
  expect(Split.configuration.on_experiment_delete).to receive(:call)
211
199
  experiment.delete
212
200
  end
201
+
202
+ it "should call the on_before_experiment_delete hook" do
203
+ expect(Split.configuration.on_before_experiment_delete).to receive(:call)
204
+ experiment.delete
205
+ end
206
+
207
+ it 'should reset the start time if the experiment should be manually started' do
208
+ Split.configuration.start_manually = true
209
+ experiment.start
210
+ experiment.delete
211
+ expect(experiment.start_time).to be_nil
212
+ end
213
213
  end
214
214
 
215
215
 
@@ -276,6 +276,11 @@ describe Split::Experiment do
276
276
  expect(Split.configuration.on_experiment_reset).to receive(:call)
277
277
  experiment.reset
278
278
  end
279
+
280
+ it "should call the on_before_experiment_reset hook" do
281
+ expect(Split.configuration.on_before_experiment_reset).to receive(:call)
282
+ experiment.reset
283
+ end
279
284
  end
280
285
 
281
286
  describe 'algorithm' do
@@ -291,31 +296,38 @@ describe Split::Experiment do
291
296
  end
292
297
  end
293
298
 
294
- describe 'next_alternative' do
295
- let(:experiment) { Split::ExperimentCatalog.find_or_create('link_color', 'blue', 'red', 'green') }
299
+ describe '#next_alternative' do
300
+ context 'with multiple alternatives' do
301
+ let(:experiment) { Split::ExperimentCatalog.find_or_create('link_color', 'blue', 'red', 'green') }
296
302
 
297
- it "should always return the winner if one exists" do
298
- green = Split::Alternative.new('green', 'link_color')
299
- experiment.winner = 'green'
303
+ context 'with winner' do
304
+ it "should always return the winner" do
305
+ green = Split::Alternative.new('green', 'link_color')
306
+ experiment.winner = 'green'
300
307
 
301
- expect(experiment.next_alternative.name).to eq('green')
302
- green.increment_participation
308
+ expect(experiment.next_alternative.name).to eq('green')
309
+ green.increment_participation
303
310
 
304
- expect(experiment.next_alternative.name).to eq('green')
305
- end
311
+ expect(experiment.next_alternative.name).to eq('green')
312
+ end
313
+ end
306
314
 
307
- it "should use the specified algorithm if a winner does not exist" do
308
- experiment.algorithm = Split::Algorithms::Whiplash
309
- expect(experiment.algorithm).to receive(:choose_alternative).and_return(Split::Alternative.new('green', 'link_color'))
310
- expect(experiment.next_alternative.name).to eq('green')
315
+ context 'without winner' do
316
+ it "should use the specified algorithm" do
317
+ experiment.algorithm = Split::Algorithms::Whiplash
318
+ expect(experiment.algorithm).to receive(:choose_alternative).and_return(Split::Alternative.new('green', 'link_color'))
319
+ expect(experiment.next_alternative.name).to eq('green')
320
+ end
321
+ end
311
322
  end
312
- end
313
323
 
314
- describe 'single alternative' do
315
- let(:experiment) { Split::ExperimentCatalog.find_or_create('link_color', 'blue') }
324
+ context 'with single alternative' do
325
+ let(:experiment) { Split::ExperimentCatalog.find_or_create('link_color', 'blue') }
316
326
 
317
- it "should always return the color blue" do
318
- expect(experiment.next_alternative.name).to eq('blue')
327
+ it "should always return the only alternative" do
328
+ expect(experiment.next_alternative.name).to eq('blue')
329
+ expect(experiment.next_alternative.name).to eq('blue')
330
+ end
319
331
  end
320
332
  end
321
333
 
@@ -432,8 +444,7 @@ describe Split::Experiment do
432
444
 
433
445
  it "should return nil and not re-calculate probabilities if they have already been calculated today" do
434
446
  experiment = Split::ExperimentCatalog.find_or_create({'link_color3' => ["purchase", "refund"]}, 'blue', 'red', 'green')
435
- experiment_calc_time = Time.now.utc.to_i / 86400
436
- experiment.calc_time = experiment_calc_time
447
+ expect(experiment.calc_winning_alternatives).not_to be nil
437
448
  expect(experiment.calc_winning_alternatives).to be nil
438
449
  end
439
450
  end
@@ -0,0 +1,80 @@
1
+ require 'spec_helper'
2
+ require 'split/goals_collection'
3
+ require 'time'
4
+
5
+ describe Split::GoalsCollection do
6
+ let(:experiment_name) { 'experiment_name' }
7
+
8
+ describe 'initialization' do
9
+ let(:goals_collection) {
10
+ Split::GoalsCollection.new('experiment_name', ['goal1', 'goal2'])
11
+ }
12
+
13
+ it "should have an experiment_name" do
14
+ expect(goals_collection.instance_variable_get(:@experiment_name)).
15
+ to eq('experiment_name')
16
+ end
17
+
18
+ it "should have a list of goals" do
19
+ expect(goals_collection.instance_variable_get(:@goals)).
20
+ to eq(['goal1', 'goal2'])
21
+ end
22
+ end
23
+
24
+ describe "#validate!" do
25
+ it "should't raise ArgumentError if @goals is nil?" do
26
+ goals_collection = Split::GoalsCollection.new('experiment_name')
27
+ expect { goals_collection.validate! }.not_to raise_error(ArgumentError)
28
+ end
29
+
30
+ it "should raise ArgumentError if @goals is not an Array" do
31
+ goals_collection = Split::GoalsCollection.
32
+ new('experiment_name', 'not an array')
33
+ expect { goals_collection.validate! }.to raise_error(ArgumentError)
34
+ end
35
+
36
+ it "should't raise ArgumentError if @goals is an array" do
37
+ goals_collection = Split::GoalsCollection.
38
+ new('experiment_name', ['an array'])
39
+ expect { goals_collection.validate! }.not_to raise_error(ArgumentError)
40
+ end
41
+ end
42
+
43
+ describe "#delete" do
44
+ let(:goals_key) { "#{experiment_name}:goals" }
45
+
46
+ it "should delete goals from redis" do
47
+ goals_collection = Split::GoalsCollection.new(experiment_name, ['goal1'])
48
+ goals_collection.save
49
+
50
+ goals_collection.delete
51
+ expect(Split.redis.exists(goals_key)).to be false
52
+ end
53
+ end
54
+
55
+ describe "#save" do
56
+ let(:goals_key) { "#{experiment_name}:goals" }
57
+
58
+ it "should return false if @goals is nil" do
59
+ goals_collection = Split::GoalsCollection.
60
+ new(experiment_name, nil)
61
+
62
+ expect(goals_collection.save).to be false
63
+ end
64
+
65
+ it "should save goals to redis if @goals is valid" do
66
+ goals = ['valid goal 1', 'valid goal 2']
67
+ collection = Split::GoalsCollection.new(experiment_name, goals)
68
+ collection.save
69
+
70
+ expect(Split.redis.lrange(goals_key, 0, -1)).to eq goals
71
+ end
72
+
73
+ it "should return @goals if @goals is valid" do
74
+ goals_collection = Split::GoalsCollection.
75
+ new(experiment_name, ['valid goal'])
76
+
77
+ expect(goals_collection.save).to eq(['valid goal'])
78
+ end
79
+ end
80
+ end
data/split.gemspec CHANGED
@@ -30,4 +30,5 @@ Gem::Specification.new do |s|
30
30
  s.add_development_dependency 'rack-test', '~> 0.6'
31
31
  s.add_development_dependency 'rake', '~> 11.1'
32
32
  s.add_development_dependency 'rspec', '~> 3.4'
33
+ s.add_development_dependency 'pry', '~> 0.10'
33
34
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: split
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 1.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Nesbitt
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-04-02 00:00:00.000000000 Z
11
+ date: 2016-04-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis
@@ -136,6 +136,20 @@ dependencies:
136
136
  - - "~>"
137
137
  - !ruby/object:Gem::Version
138
138
  version: '3.4'
139
+ - !ruby/object:Gem::Dependency
140
+ name: pry
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '0.10'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '0.10'
139
153
  description:
140
154
  email:
141
155
  - andrewnez@gmail.com
@@ -181,6 +195,7 @@ files:
181
195
  - lib/split/extensions.rb
182
196
  - lib/split/extensions/array.rb
183
197
  - lib/split/extensions/string.rb
198
+ - lib/split/goals_collection.rb
184
199
  - lib/split/helper.rb
185
200
  - lib/split/metric.rb
186
201
  - lib/split/persistence.rb
@@ -199,6 +214,7 @@ files:
199
214
  - spec/encapsulated_helper_spec.rb
200
215
  - spec/experiment_catalog_spec.rb
201
216
  - spec/experiment_spec.rb
217
+ - spec/goals_collection_spec.rb
202
218
  - spec/helper_spec.rb
203
219
  - spec/metric_spec.rb
204
220
  - spec/persistence/cookie_adapter_spec.rb
@@ -243,6 +259,7 @@ test_files:
243
259
  - spec/encapsulated_helper_spec.rb
244
260
  - spec/experiment_catalog_spec.rb
245
261
  - spec/experiment_spec.rb
262
+ - spec/goals_collection_spec.rb
246
263
  - spec/helper_spec.rb
247
264
  - spec/metric_spec.rb
248
265
  - spec/persistence/cookie_adapter_spec.rb