split 1.1.0 → 1.2.0

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: ea348c09640ba8869242588929d2d337856fc834
4
- data.tar.gz: 0bb555acfabcaf683051058097da01b803c56a50
3
+ metadata.gz: acc9cc61f2fbaacd23c4101ce3533baad6fc2f5e
4
+ data.tar.gz: 0f1608e29221a58215ee240ac01ac921256e8466
5
5
  SHA512:
6
- metadata.gz: f411a5636ee2fcc0c296f2e3e1e24515af7524be73eec1fe8bd86998564eb61fcc60419d52dc02588a92ac85ecb28c6525e9a007cc5607efe1942a56c1fa3262
7
- data.tar.gz: 6ff0d6a3be6295653be81d34e0c2479c2d38bd8e1ac6a2fb0aa20e5796329add32cf465b884d5ede25ba9121343ae888de62e78f9aff5e468604f635c2efabb6
6
+ metadata.gz: 5e4cd8b3e649929c5c3a9f406a8a587c7cc23121146f0c0002ff05d6c86b7c61e508a05acab5d70172864d73f4c544263e72d1476a8d5afe134deb34b6012172
7
+ data.tar.gz: f9be6dc468f3fe7941e07b3734d1fae28a4a05209501cb093769d8f14001ec596d5b3627f808444523b303fe4d44de0946a328e04a1b9afe64434ba5c54c0493
@@ -1,3 +1,14 @@
1
+ ## 1.2.0 (January 24th, 2015)
2
+
3
+ Features
4
+
5
+ - Configure redis using environment variables if available (@saratovsource , #293)
6
+ - Store metadata on experiment configuration (@dekz, #291)
7
+
8
+ Bugfixes:
9
+
10
+ - Revert the Trial#complete! public API to support noargs (@dekz, #292)
11
+
1
12
  ## 1.1.0 (January 9th, 2015)
2
13
 
3
14
  Features:
data/README.md CHANGED
@@ -119,7 +119,7 @@ You can find more examples, tutorials and guides on the [wiki](https://github.co
119
119
 
120
120
  ## Statistical Validity
121
121
 
122
- Split has two options for you to use to determine which alternative is the best.
122
+ Split has two options for you to use to determine which alternative is the best.
123
123
 
124
124
  The first option (default on the dashboard) uses a z test (n>30) for the difference between your control and alternative conversion rates to calculate statistical significance. This test will tell you whether an alternative is better or worse than your control, but it will not distinguish between which alternative is the best in an experiment with multiple alternatives. Split will only tell you if your experiment is 90%, 95%, or 99% significant, and this test only works if you have more than 30 participants and 5 conversions for each branch.
125
125
 
@@ -364,6 +364,8 @@ Split.configure do |config|
364
364
  end
365
365
  ```
366
366
 
367
+ You can set different Redis host via environment variable ```REDIS_URL```.
368
+
367
369
  ### Filtering
368
370
 
369
371
  In most scenarios you don't want to have AB-Testing enabled for web spiders, robots or special groups of users.
@@ -443,6 +445,28 @@ and:
443
445
  finished("my_first_experiment")
444
446
  ```
445
447
 
448
+ You can also add meta data for each experiment, very useful when you need more than an alternative name to change behaviour:
449
+
450
+ ```yaml
451
+ my_first_experiment:
452
+ alternatives:
453
+ - a
454
+ - b
455
+ meta:
456
+ a:
457
+ text: "Have a fantastic day"
458
+ b:
459
+ text: "Don't get hit by a bus"
460
+ ```
461
+
462
+ This allows for some advanced experiment configuration using methods like:
463
+
464
+ ```ruby
465
+ trial.alternative.name # => "a"
466
+
467
+ trial.metadata['text'] # => "Have a fantastic day"
468
+ ```
469
+
446
470
  #### Metrics
447
471
 
448
472
  You might wish to track generic metrics, such as conversions, and use
@@ -503,7 +527,7 @@ To complete a goal conversion, you do it like:
503
527
  finished("link_color" => "purchase")
504
528
  ```
505
529
 
506
- **NOTE:** This does not mean that a single experiment can have/complete progressive goals.
530
+ **NOTE:** This does not mean that a single experiment can have/complete progressive goals.
507
531
 
508
532
  **Good Example**: Test if listing Plan A first result in more conversions to Plan A (goal: "plana_conversion") or Plan B (goal: "planb_conversion").
509
533
 
@@ -549,11 +573,8 @@ production: redis1.example.com:6379
549
573
  And our initializer:
550
574
 
551
575
  ```ruby
552
- rails_root = ENV['RAILS_ROOT'] || File.dirname(__FILE__) + '/../..'
553
- rails_env = ENV['RAILS_ENV'] || 'development'
554
-
555
- split_config = YAML.load_file(rails_root + '/config/split.yml')
556
- Split.redis = split_config[rails_env]
576
+ split_config = YAML.load_file(Rails.root.join('config', 'split.yml'))
577
+ Split.redis = split_config[Rails.env]
557
578
  ```
558
579
 
559
580
  ## Namespaces
@@ -601,11 +622,11 @@ end
601
622
 
602
623
  ## Algorithms
603
624
 
604
- By default, Split ships with `Split::Algorithms::WeightedSample` that randomly selects from possible alternatives for a traditional a/b test.
625
+ By default, Split ships with `Split::Algorithms::WeightedSample` that randomly selects from possible alternatives for a traditional a/b test.
605
626
  It is possible to specify static weights to favor certain alternatives.
606
627
 
607
- `Split::Algorithms::Whiplash` is an implementation of a [multi-armed bandit algorithm](http://stevehanov.ca/blog/index.php?id=132).
608
- This algorithm will automatically weight the alternatives based on their relative performance,
628
+ `Split::Algorithms::Whiplash` is an implementation of a [multi-armed bandit algorithm](http://stevehanov.ca/blog/index.php?id=132).
629
+ This algorithm will automatically weight the alternatives based on their relative performance,
609
630
  choosing the better-performing ones more often as trials are completed.
610
631
 
611
632
  Users may also write their own algorithms. The default algorithm may be specified globally in the configuration file, or on a per experiment basis using the experiments hash of the configuration file.
@@ -53,7 +53,7 @@ module Split
53
53
  # create a new one.
54
54
  def redis
55
55
  return @redis if @redis
56
- self.redis = 'localhost:6379'
56
+ self.redis = ENV.fetch('REDIS_URL', 'localhost:6379')
57
57
  self.redis
58
58
  end
59
59
 
@@ -141,6 +141,10 @@ module Split
141
141
  experiment_config[experiment_name.to_sym][:goals] = goals
142
142
  end
143
143
 
144
+ if metadata = value_for(settings, :metadata)
145
+ experiment_config[experiment_name.to_sym][:metadata] = metadata
146
+ end
147
+
144
148
  if (resettable = value_for(settings, :resettable)) != nil
145
149
  experiment_config[experiment_name.to_sym][:resettable] = resettable
146
150
  end
@@ -6,6 +6,7 @@ module Split
6
6
  attr_accessor :goals
7
7
  attr_accessor :alternatives
8
8
  attr_accessor :alternative_probabilities
9
+ attr_accessor :metadata
9
10
 
10
11
  DEFAULT_OPTIONS = {
11
12
  :resettable => true
@@ -22,6 +23,7 @@ module Split
22
23
  set_alternatives_and_options(
23
24
  alternatives: load_alternatives_from_configuration,
24
25
  goals: load_goals_from_configuration,
26
+ metadata: load_metadata_from_configuration,
25
27
  resettable: exp_config[:resettable],
26
28
  algorithm: exp_config[:algorithm]
27
29
  )
@@ -29,6 +31,7 @@ module Split
29
31
  set_alternatives_and_options(
30
32
  alternatives: alternatives,
31
33
  goals: options[:goals],
34
+ metadata: options[:metadata],
32
35
  resettable: options[:resettable],
33
36
  algorithm: options[:algorithm]
34
37
  )
@@ -40,6 +43,7 @@ module Split
40
43
  self.goals = options[:goals]
41
44
  self.resettable = options[:resettable]
42
45
  self.algorithm = options[:algorithm]
46
+ self.metadata = options[:metadata]
43
47
  end
44
48
 
45
49
  def extract_alternatives_from_options(options)
@@ -56,6 +60,7 @@ module Split
56
60
  if exp_config
57
61
  alts = load_alternatives_from_configuration
58
62
  options[:goals] = load_goals_from_configuration
63
+ options[:metadata] = load_metadata_from_configuration
59
64
  options[:resettable] = exp_config[:resettable]
60
65
  options[:algorithm] = exp_config[:algorithm]
61
66
  end
@@ -79,16 +84,20 @@ module Split
79
84
  start unless Split.configuration.start_manually
80
85
  @alternatives.reverse.each {|a| Split.redis.lpush(name, a.name)}
81
86
  @goals.reverse.each {|a| Split.redis.lpush(goals_key, a)} unless @goals.nil?
87
+ Split.redis.set(metadata_key, @metadata.to_json) unless @metadata.nil?
82
88
  else
83
89
  existing_alternatives = load_alternatives_from_redis
84
90
  existing_goals = load_goals_from_redis
85
- unless existing_alternatives == @alternatives.map(&:name) && existing_goals == @goals
91
+ existing_metadata = load_metadata_from_redis
92
+ unless existing_alternatives == @alternatives.map(&:name) && existing_goals == @goals && existing_metadata == @metadata
86
93
  reset
87
94
  @alternatives.each(&:delete)
88
95
  delete_goals
96
+ delete_metadata
89
97
  Split.redis.del(@name)
90
98
  @alternatives.reverse.each {|a| Split.redis.lpush(name, a.name)}
91
99
  @goals.reverse.each {|a| Split.redis.lpush(goals_key, a)} unless @goals.nil?
100
+ Split.redis.set(metadata_key, @metadata.to_json) unless @metadata.nil?
92
101
  end
93
102
  end
94
103
 
@@ -221,6 +230,10 @@ module Split
221
230
  "#{key}:finished"
222
231
  end
223
232
 
233
+ def metadata_key
234
+ "#{name}:metadata"
235
+ end
236
+
224
237
  def resettable?
225
238
  resettable
226
239
  end
@@ -238,6 +251,7 @@ module Split
238
251
  Split.redis.srem(:experiments, name)
239
252
  Split.redis.del(name)
240
253
  delete_goals
254
+ delete_metadata
241
255
  Split.configuration.on_experiment_delete.call(self)
242
256
  increment_version
243
257
  end
@@ -246,12 +260,17 @@ module Split
246
260
  Split.redis.del(goals_key)
247
261
  end
248
262
 
263
+ def delete_metadata
264
+ Split.redis.del(metadata_key)
265
+ end
266
+
249
267
  def load_from_redis
250
268
  exp_config = Split.redis.hgetall(experiment_config_key)
251
269
  self.resettable = exp_config['resettable']
252
270
  self.algorithm = exp_config['algorithm']
253
271
  self.alternatives = load_alternatives_from_redis
254
272
  self.goals = load_goals_from_redis
273
+ self.metadata = load_metadata_from_redis
255
274
  end
256
275
 
257
276
  def calc_winning_alternatives
@@ -388,6 +407,10 @@ module Split
388
407
  "experiment_configurations/#{@name}"
389
408
  end
390
409
 
410
+ def load_metadata_from_configuration
411
+ metadata = Split.configuration.experiment_for(@name)[:metadata]
412
+ end
413
+
391
414
  def load_goals_from_configuration
392
415
  goals = Split.configuration.experiment_for(@name)[:goals]
393
416
  if goals.nil?
@@ -401,6 +424,11 @@ module Split
401
424
  Split.redis.lrange(goals_key, 0, -1)
402
425
  end
403
426
 
427
+ def load_metadata_from_redis
428
+ meta = Split.redis.get(metadata_key)
429
+ JSON.parse(meta) unless meta.nil?
430
+ end
431
+
404
432
  def load_alternatives_from_configuration
405
433
  alts = Split.configuration.experiment_for(@name)[:alternatives]
406
434
  raise ArgumentError, "Experiment configuration is missing :alternatives array" unless alts
@@ -1,10 +1,12 @@
1
1
  module Split
2
2
  class Trial
3
3
  attr_accessor :experiment
4
+ attr_accessor :metadata
4
5
 
5
6
  def initialize(attrs = {})
6
7
  self.experiment = attrs.delete(:experiment)
7
8
  self.alternative = attrs.delete(:alternative)
9
+ self.metadata = attrs.delete(:metadata)
8
10
 
9
11
  @user = attrs.delete(:user)
10
12
  @options = attrs
@@ -12,6 +14,10 @@ module Split
12
14
  @alternative_choosen = false
13
15
  end
14
16
 
17
+ def metadata
18
+ @metadata ||= experiment.metadata[alternative.name]
19
+ end
20
+
15
21
  def alternative
16
22
  @alternative ||= if @experiment.has_winner?
17
23
  @experiment.winner
@@ -26,14 +32,12 @@ module Split
26
32
  end
27
33
  end
28
34
 
29
- def complete!(goals, context = nil)
30
- goals = goals || []
31
-
35
+ def complete!(goals=[], context = nil)
32
36
  if alternative
33
- if goals.empty?
37
+ if Array(goals).empty?
34
38
  alternative.increment_completion
35
39
  else
36
- goals.each {|g| alternative.increment_completion(g) }
40
+ Array(goals).each {|g| alternative.increment_completion(g) }
37
41
  end
38
42
 
39
43
  context.send(Split.configuration.on_trial_complete, self) \
@@ -1,6 +1,6 @@
1
1
  module Split
2
2
  MAJOR = 1
3
- MINOR = 1
3
+ MINOR = 2
4
4
  PATCH = 0
5
5
  VERSION = [MAJOR, MINOR, PATCH].join('.')
6
6
  end
@@ -90,6 +90,36 @@ describe Split::Configuration do
90
90
  end
91
91
  end
92
92
 
93
+ context "in a configuration with metadata" do
94
+ before do
95
+ experiments_yaml = <<-eos
96
+ my_experiment:
97
+ alternatives:
98
+ - name: Control Opt
99
+ percent: 67
100
+ - name: Alt One
101
+ percent: 10
102
+ - name: Alt Two
103
+ percent: 23
104
+ metadata:
105
+ Control Opt:
106
+ text: 'Control Option'
107
+ Alt One:
108
+ text: 'Alternative One'
109
+ Alt Two:
110
+ text: 'Alternative Two'
111
+ resettable: false
112
+ eos
113
+ @config.experiments = YAML.load(experiments_yaml)
114
+ end
115
+
116
+ it 'should have metadata on the experiment' do
117
+ meta = @config.normalized_experiments[:my_experiment][:metadata]
118
+ expect(meta).to_not be nil
119
+ expect(meta['Control Opt']['text']).to eq('Control Option')
120
+ end
121
+ end
122
+
93
123
  context "in a complex configuration" do
94
124
  before do
95
125
  experiments_yaml = <<-eos
@@ -120,6 +150,7 @@ describe Split::Configuration do
120
150
  expect(@config.metrics).not_to be_nil
121
151
  expect(@config.metrics.keys).to eq([:my_metric])
122
152
  end
153
+
123
154
  end
124
155
  end
125
156
 
@@ -147,6 +147,29 @@ describe Split::Experiment do
147
147
 
148
148
  end
149
149
 
150
+ describe '#metadata' do
151
+ let(:experiment) { Split::Experiment.new('basket_text', :alternatives => ['Basket', "Cart"], :algorithm => Split::Algorithms::Whiplash, :metadata => meta) }
152
+ context 'simple hash' do
153
+ let(:meta) { { 'basket' => 'a', 'cart' => 'b' } }
154
+ it "should persist metadata in redis" do
155
+ experiment.save
156
+ e = Split::ExperimentCatalog.find('basket_text')
157
+ expect(e).to eq(experiment)
158
+ expect(e.metadata).to eq(meta)
159
+ end
160
+ end
161
+
162
+ context 'nested hash' do
163
+ let(:meta) { { 'basket' => { 'one' => 'two' }, 'cart' => 'b' } }
164
+ it "should persist metadata in redis" do
165
+ experiment.save
166
+ e = Split::ExperimentCatalog.find('basket_text')
167
+ expect(e).to eq(experiment)
168
+ expect(e.metadata).to eq(meta)
169
+ end
170
+ end
171
+ end
172
+
150
173
  it "should persist algorithm in redis" do
151
174
  experiment = Split::Experiment.new('basket_text', :alternatives => ['Basket', "Cart"], :algorithm => Split::Algorithms::Whiplash)
152
175
  experiment.save
@@ -33,6 +33,27 @@ describe Split::Trial do
33
33
  end
34
34
  end
35
35
 
36
+ describe "metadata" do
37
+ let(:alternatives) { ['basket', 'cart'] }
38
+ let(:metadata) { Hash[alternatives.map { |k| [k, "Metadata for #{k}"] }] }
39
+ let(:experiment) do
40
+ Split::Experiment.new('basket_text', :alternatives => alternatives, :metadata => metadata).save
41
+ end
42
+
43
+ it 'has metadata on each trial' do
44
+ trial = Split::Trial.new(:experiment => experiment, :user => user, :metadata => metadata['cart'],
45
+ :override => 'cart')
46
+ expect(trial.metadata).to eq(metadata['cart'])
47
+ end
48
+
49
+ it 'has metadata on each trial from the experiment' do
50
+ trial = Split::Trial.new(:experiment => experiment, :user => user)
51
+ trial.choose!
52
+ expect(trial.metadata).to eq(metadata[trial.alternative.name])
53
+ expect(trial.metadata).to match /#{trial.alternative.name}/
54
+ end
55
+ end
56
+
36
57
  describe "#choose!" do
37
58
  def expect_alternative(trial, alternative_name)
38
59
  3.times do
@@ -109,6 +130,42 @@ describe Split::Trial do
109
130
  end
110
131
  end
111
132
 
133
+ describe "#complete!" do
134
+ let(:trial) { Split::Trial.new(:user => user, :experiment => experiment) }
135
+ context 'when there are no goals' do
136
+ it 'should complete the trial' do
137
+ trial.choose!
138
+ old_completed_count = trial.alternative.completed_count
139
+ trial.complete!
140
+ expect(trial.alternative.completed_count).to be(old_completed_count+1)
141
+ end
142
+ end
143
+
144
+ context 'when there are many goals' do
145
+ let(:goals) { ['first', 'second'] }
146
+ let(:trial) { Split::Trial.new(:user => user, :experiment => experiment, :goals => goals) }
147
+ shared_examples_for "goal completion" do
148
+ it 'should not complete the trial' do
149
+ trial.choose!
150
+ old_completed_count = trial.alternative.completed_count
151
+ trial.complete!(goal)
152
+ expect(trial.alternative.completed_count).to_not be(old_completed_count+1)
153
+ end
154
+ end
155
+
156
+ describe 'Array of Goals' do
157
+ let(:goal) { [goals.first] }
158
+ it_behaves_like 'goal completion'
159
+ end
160
+
161
+ describe 'String of Goal' do
162
+ let(:goal) { goals.first }
163
+ it_behaves_like 'goal completion'
164
+ end
165
+
166
+ end
167
+ end
168
+
112
169
  describe "alternative recording" do
113
170
  before(:each) { Split.configuration.store_override = false }
114
171
 
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.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Nesbitt
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-01-09 00:00:00.000000000 Z
11
+ date: 2015-01-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis