split 1.4.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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