split 1.1.0 → 1.2.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: ea348c09640ba8869242588929d2d337856fc834
4
- data.tar.gz: 0bb555acfabcaf683051058097da01b803c56a50
3
+ metadata.gz: b771c2496cd674b0512e956fcdb3bf67768ba4a8
4
+ data.tar.gz: cd3ac62720dbaa5968d58be74ebc689854fd0fac
5
5
  SHA512:
6
- metadata.gz: f411a5636ee2fcc0c296f2e3e1e24515af7524be73eec1fe8bd86998564eb61fcc60419d52dc02588a92ac85ecb28c6525e9a007cc5607efe1942a56c1fa3262
7
- data.tar.gz: 6ff0d6a3be6295653be81d34e0c2479c2d38bd8e1ac6a2fb0aa20e5796329add32cf465b884d5ede25ba9121343ae888de62e78f9aff5e468604f635c2efabb6
6
+ metadata.gz: f94c6eca1509241995790fefc2407ae8cdc546ceae8aad2a4fa4975c47dd4370f5cc835b630a9c91a1cd7682a24bf49c34af422a31f576af70162003717f396e
7
+ data.tar.gz: 9e47de4c77fa41c39d525c2c79448c3d1e5fdc564dfa340d00857a91cedd11517fc3e871b7f4bb208eab9a5dfe29e2a026dcd40139569d471e146d68bfbc982c
data/.travis.yml CHANGED
@@ -1,11 +1,12 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 2.1.5
3
+ - 2.2.0
4
4
 
5
5
  gemfile:
6
6
  - gemfiles/3.2.gemfile
7
7
  - gemfiles/4.0.gemfile
8
8
  - gemfiles/4.1.gemfile
9
+ - gemfiles/4.2.gemfile
9
10
 
10
11
  services:
11
12
  - redis-server
data/Appraisals CHANGED
@@ -1,11 +1,15 @@
1
1
  appraise "3.2" do
2
- gem "rails", "~> 3.2.13"
2
+ gem "rails", "~> 3.2.21"
3
3
  end
4
4
 
5
5
  appraise "4.0" do
6
- gem "rails", "~> 4.0.12"
6
+ gem "rails", "~> 4.0.13"
7
7
  end
8
8
 
9
9
  appraise "4.1" do
10
- gem "rails", "~> 4.1.8"
10
+ gem "rails", "~> 4.1.10"
11
+ end
12
+
13
+ appraise "4.2" do
14
+ gem "rails", "~> 4.2.1"
11
15
  end
data/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
+ ## 1.2.1 (May 17th, 2015)
2
+
3
+ Features
4
+
5
+ - Handle redis DNS resolution failures gracefully (@fusion2004, #310)
6
+ - Push metadata to ab_test block (@ekorneeff, #296)
7
+ - Helper methods are now private when included in controllers (@ipoval, #303)
8
+
9
+ Bugfixes:
10
+
11
+ - Return an empty hash as metadata when Split is disabled (@tomasdundacek, #313)
12
+ - Don't use capture helper from ActionView (@tomasdundacek, #312)
13
+
14
+ Misc:
15
+
16
+ - Remove body "max-width" from dashboard (@xicreative, #299)
17
+ - fix private for class methods (@ipoval, #301)
18
+ - minor memoization fix in spec (@ipoval, #304)
19
+ - Minor documentation fixes (#295, #297, #305, #308)
20
+
21
+ ## 1.2.0 (January 24th, 2015)
22
+
23
+ Features
24
+
25
+ - Configure redis using environment variables if available (@saratovsource , #293)
26
+ - Store metadata on experiment configuration (@dekz, #291)
27
+
28
+ Bugfixes:
29
+
30
+ - Revert the Trial#complete! public API to support noargs (@dekz, #292)
31
+
1
32
  ## 1.1.0 (January 9th, 2015)
2
33
 
34
+ Changes:
35
+
36
+ - Public class methods on `Split::Experiment` (e.g., `find_or_create`)
37
+ have been moved to `Split::ExperimentCatalog`.
38
+
3
39
  Features:
4
40
 
5
41
  - Decouple trial from Split::Helper (@joshdover, #286)
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Split
1
+ # [Split](http://libraries.io/rubygems/split)
2
2
 
3
3
  Split is a rack based ab testing framework designed to work with Rails, Sinatra or any other rack based app.
4
4
 
@@ -7,10 +7,10 @@ Split is heavily inspired by the Abingo and Vanity rails ab testing plugins and
7
7
  Split is designed to be hacker friendly, allowing for maximum customisation and extensibility.
8
8
 
9
9
  [![Gem Version](https://badge.fury.io/rb/split.svg)](http://badge.fury.io/rb/split)
10
- [![Build Status](https://secure.travis-ci.org/andrew/split.svg?branch=master)](http://travis-ci.org/andrew/split)
11
- [![Dependency Status](https://gemnasium.com/andrew/split.svg)](https://gemnasium.com/andrew/split)
12
- [![Code Climate](https://codeclimate.com/github/andrew/split.svg)](https://codeclimate.com/github/andrew/split)
13
- [![Coverage Status](http://img.shields.io/coveralls/andrew/split.svg)](https://coveralls.io/r/andrew/split)
10
+ [![Build Status](https://secure.travis-ci.org/splitrb/split.svg?branch=master)](http://travis-ci.org/splitrb/split)
11
+ [![Dependency Status](https://gemnasium.com/splitrb/split.svg)](https://gemnasium.com/splitrb/split)
12
+ [![Code Climate](https://codeclimate.com/github/splitrb/split.svg)](https://codeclimate.com/github/splitrb/split)
13
+ [![Coverage Status](http://img.shields.io/coveralls/splitrb/split.svg)](https://coveralls.io/r/splitrb/split)
14
14
 
15
15
  ## Requirements
16
16
 
@@ -87,7 +87,7 @@ Example: View
87
87
 
88
88
  ```erb
89
89
  <% ab_test("login_button", "/images/button1.jpg", "/images/button2.jpg") do |button_file| %>
90
- <%= img_tag(button_file, :alt => "Login!") %>
90
+ <%= image_tag(button_file, :alt => "Login!") %>
91
91
  <% end %>
92
92
  ```
93
93
 
@@ -115,11 +115,11 @@ Example: Conversion tracking (in a view)
115
115
  Thanks for signing up, dude! <% finished("signup_page_redesign") %>
116
116
  ```
117
117
 
118
- You can find more examples, tutorials and guides on the [wiki](https://github.com/andrew/split/wiki).
118
+ You can find more examples, tutorials and guides on the [wiki](https://github.com/splitrb/split/wiki).
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
 
@@ -250,7 +250,7 @@ For example:
250
250
 
251
251
  ``` ruby
252
252
  Split.configure do |config|
253
- config.on_trial_choose = :log_trial_choice
253
+ config.on_trial_choose = :log_trial_choose
254
254
  config.on_trial_complete = :log_trial_complete
255
255
  end
256
256
  ```
@@ -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,37 @@ 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
+
470
+ or in views:
471
+
472
+ ```erb
473
+ <% ab_test("my_first_experiment") do |alternative, meta| %>
474
+ <%= alternative %>
475
+ <small><%= meta['text'] %></small>
476
+ <% end %>
477
+ ```
478
+
446
479
  #### Metrics
447
480
 
448
481
  You might wish to track generic metrics, such as conversions, and use
@@ -503,7 +536,7 @@ To complete a goal conversion, you do it like:
503
536
  finished("link_color" => "purchase")
504
537
  ```
505
538
 
506
- **NOTE:** This does not mean that a single experiment can have/complete progressive goals.
539
+ **NOTE:** This does not mean that a single experiment can have/complete progressive goals.
507
540
 
508
541
  **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
542
 
@@ -549,11 +582,8 @@ production: redis1.example.com:6379
549
582
  And our initializer:
550
583
 
551
584
  ```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]
585
+ split_config = YAML.load_file(Rails.root.join('config', 'split.yml'))
586
+ Split.redis = split_config[Rails.env]
557
587
  ```
558
588
 
559
589
  ## Namespaces
@@ -584,7 +614,7 @@ conduct experiments that are not tied to a web session.
584
614
 
585
615
  ```ruby
586
616
  # create a new experiment
587
- experiment = Split::Experiment.find_or_create('color', 'red', 'blue')
617
+ experiment = Split::ExperimentCatalog.find_or_create('color', 'red', 'blue')
588
618
  # create a new trial
589
619
  trial = Split::Trial.new(:experiment => experiment)
590
620
  # run trial
@@ -601,11 +631,11 @@ end
601
631
 
602
632
  ## Algorithms
603
633
 
604
- By default, Split ships with `Split::Algorithms::WeightedSample` that randomly selects from possible alternatives for a traditional a/b test.
634
+ By default, Split ships with `Split::Algorithms::WeightedSample` that randomly selects from possible alternatives for a traditional a/b test.
605
635
  It is possible to specify static weights to favor certain alternatives.
606
636
 
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,
637
+ `Split::Algorithms::Whiplash` is an implementation of a [multi-armed bandit algorithm](http://stevehanov.ca/blog/index.php?id=132).
638
+ This algorithm will automatically weight the alternatives based on their relative performance,
609
639
  choosing the better-performing ones more often as trials are completed.
610
640
 
611
641
  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.
@@ -620,8 +650,8 @@ end
620
650
 
621
651
  ## Extensions
622
652
 
623
- - [Split::Export](http://github.com/andrew/split-export) - easily export ab test data out of Split
624
- - [Split::Analytics](http://github.com/andrew/split-analytics) - push test data to google analytics
653
+ - [Split::Export](http://github.com/splitrb/split-export) - easily export ab test data out of Split
654
+ - [Split::Analytics](http://github.com/splitrb/split-analytics) - push test data to google analytics
625
655
  - [Split::Mongoid](https://github.com/MongoHQ/split-mongoid) - store experiment data in mongoid (still uses redis)
626
656
  - [Split::Cacheable](https://github.com/harrystech/split_cacheable) - automatically create cache buckets per test
627
657
  - [Split::Counters](https://github.com/bernardkroes/split-counters) - add counters per experiment and alternative
@@ -638,12 +668,12 @@ Ryan bates has produced an excellent 10 minute screencast about split on the Rai
638
668
 
639
669
  ## Contributors
640
670
 
641
- Over 70 different people have contributed to the project, you can see them all here: https://github.com/andrew/split/graphs/contributors
671
+ Over 70 different people have contributed to the project, you can see them all here: https://github.com/splitrb/split/graphs/contributors
642
672
 
643
673
  ## Development
644
674
 
645
- Source hosted at [GitHub](http://github.com/andrew/split).
646
- Report Issues/Feature requests on [GitHub Issues](http://github.com/andrew/split/issues).
675
+ Source hosted at [GitHub](http://github.com/splitrb/split).
676
+ Report Issues/Feature requests on [GitHub Issues](http://github.com/splitrb/split/issues).
647
677
  Discussion at [Google Groups](https://groups.google.com/d/forum/split-ruby)
648
678
 
649
679
  Tests can be ran with `rake spec`
@@ -661,4 +691,4 @@ Tests can be ran with `rake spec`
661
691
 
662
692
  ## Copyright
663
693
 
664
- Copyright (c) 2015 Andrew Nesbitt. See [LICENSE](https://github.com/andrew/split/blob/master/LICENSE) for details.
694
+ Copyright (c) 2015 Andrew Nesbitt. See [LICENSE](https://github.com/splitrb/split/blob/master/LICENSE) for details.
data/gemfiles/3.2.gemfile CHANGED
@@ -3,6 +3,6 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  gem "appraisal"
6
- gem "rails", "~> 3.2.13"
6
+ gem "rails", "~> 3.2.21"
7
7
 
8
8
  gemspec :path => "../"
data/gemfiles/4.0.gemfile CHANGED
@@ -3,6 +3,6 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  gem "appraisal"
6
- gem "rails", "~> 4.0.12"
6
+ gem "rails", "~> 4.0.13"
7
7
 
8
8
  gemspec :path => "../"
data/gemfiles/4.1.gemfile CHANGED
@@ -3,6 +3,6 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  gem "appraisal"
6
- gem "rails", "~> 4.1.8"
6
+ gem "rails", "~> 4.1.10"
7
7
 
8
8
  gemspec :path => "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "appraisal"
6
+ gem "rails", "~> 4.2.1"
7
+
8
+ gemspec :path => "../"
@@ -5,30 +5,32 @@ require 'simple-random'
5
5
  module Split
6
6
  module Algorithms
7
7
  module Whiplash
8
- def self.choose_alternative(experiment)
9
- experiment[best_guess(experiment.alternatives)]
10
- end
8
+ class << self
9
+ def choose_alternative(experiment)
10
+ experiment[best_guess(experiment.alternatives)]
11
+ end
11
12
 
12
- private
13
+ private
13
14
 
14
- def self.arm_guess(participants, completions)
15
- a = [participants, 0].max
16
- b = [participants-completions, 0].max
17
- s = SimpleRandom.new; s.set_seed; s.beta(a+fairness_constant, b+fairness_constant)
18
- end
15
+ def arm_guess(participants, completions)
16
+ a = [participants, 0].max
17
+ b = [participants-completions, 0].max
18
+ s = SimpleRandom.new; s.set_seed; s.beta(a+fairness_constant, b+fairness_constant)
19
+ end
19
20
 
20
- def self.best_guess(alternatives)
21
- guesses = {}
22
- alternatives.each do |alternative|
23
- guesses[alternative.name] = arm_guess(alternative.participant_count, alternative.all_completed_count)
21
+ def best_guess(alternatives)
22
+ guesses = {}
23
+ alternatives.each do |alternative|
24
+ guesses[alternative.name] = arm_guess(alternative.participant_count, alternative.all_completed_count)
25
+ end
26
+ gmax = guesses.values.max
27
+ best = guesses.keys.select { |name| guesses[name] == gmax }
28
+ best.sample
24
29
  end
25
- gmax = guesses.values.max
26
- best = guesses.keys.select {|name| guesses[name] == gmax }
27
- return best.sample
28
- end
29
30
 
30
- def self.fairness_constant
31
- 7
31
+ def fairness_constant
32
+ 7
33
+ end
32
34
  end
33
35
  end
34
36
  end
@@ -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
@@ -7,7 +7,6 @@ html {
7
7
  body {
8
8
  padding: 0 10px;
9
9
  margin: 10px auto 0;
10
- max-width:800px;
11
10
  }
12
11
 
13
12
  .header {
@@ -13,5 +13,5 @@
13
13
  <% end %>
14
14
  <% else %>
15
15
  <p class="intro">No experiments have started yet, you need to define them in your code and introduce them to your users.</p>
16
- <p class="intro">Check out the <a href='https://github.com/andrew/split#readme'>Readme</a> for more help getting started.</p>
16
+ <p class="intro">Check out the <a href='https://github.com/splitrb/split#readme'>Readme</a> for more help getting started.</p>
17
17
  <% end %>
@@ -20,7 +20,7 @@
20
20
  </div>
21
21
 
22
22
  <div id="footer">
23
- <p>Powered by <a href="http://github.com/andrew/split">Split</a> v<%=Split::VERSION %></p>
23
+ <p>Powered by <a href="http://github.com/splitrb/split">Split</a> v<%=Split::VERSION %></p>
24
24
  </div>
25
25
  </body>
26
26
  </html>
@@ -2,7 +2,7 @@
2
2
  # mix into our model classes.
3
3
  #
4
4
  # This module exposes only two methods
5
- # - ab_test and
5
+ # - ab_test and
6
6
  # - ab_test_finished
7
7
  # that can safely be mixed into any class.
8
8
  #
@@ -14,9 +14,12 @@ module Split
14
14
 
15
15
  class ContextShim
16
16
  include Split::Helper
17
+ public :ab_test, :finished
18
+
17
19
  def initialize(context)
18
20
  @context = context
19
21
  end
22
+
20
23
  def ab_user
21
24
  @ab_user ||= Split::Persistence.adapter.new(@context)
22
25
  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
@@ -12,13 +12,8 @@ module Split
12
12
  end
13
13
 
14
14
  def self.find(name)
15
- if Split.redis.exists(name)
16
- obj = Experiment.new name
17
- obj.load_from_redis
18
- else
19
- obj = nil
20
- end
21
- obj
15
+ return unless Split.redis.exists(name)
16
+ Experiment.new(name).tap { |exp| exp.load_from_redis }
22
17
  end
23
18
 
24
19
  def self.find_or_initialize(metric_descriptor, control = nil, *alternatives)
@@ -39,8 +34,6 @@ module Split
39
34
  experiment.save
40
35
  end
41
36
 
42
- private
43
-
44
37
  def self.normalize_experiment(metric_descriptor)
45
38
  if Hash === metric_descriptor
46
39
  experiment_name = metric_descriptor.keys.first
@@ -51,6 +44,7 @@ module Split
51
44
  end
52
45
  return experiment_name, goals
53
46
  end
47
+ private_class_method :normalize_experiment
54
48
 
55
49
  end
56
50
  end
data/lib/split/helper.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  module Split
2
2
  module Helper
3
+ module_function
3
4
 
4
5
  def ab_test(metric_descriptor, control = nil, *alternatives)
5
6
  begin
@@ -15,7 +16,7 @@ module Split
15
16
  else
16
17
  control_variable(experiment.control)
17
18
  end
18
- rescue Errno::ECONNREFUSED, Redis::CannotConnectError => e
19
+ rescue Errno::ECONNREFUSED, Redis::CannotConnectError, SocketError => e
19
20
  raise(e) unless Split.configuration.db_failover
20
21
  Split.configuration.db_failover_on_db_error.call(e)
21
22
 
@@ -28,13 +29,8 @@ module Split
28
29
  end
29
30
 
30
31
  if block_given?
31
- if defined?(capture) # a block in a rails view
32
- block = Proc.new { yield(alternative) }
33
- concat(capture(alternative, &block))
34
- false
35
- else
36
- yield(alternative)
37
- end
32
+ metadata = trial ? trial.metadata : {}
33
+ yield(alternative, metadata)
38
34
  else
39
35
  alternative
40
36
  end
@@ -63,7 +59,6 @@ module Split
63
59
  end
64
60
  end
65
61
 
66
-
67
62
  def finished(metric_descriptor, options = {:reset => true})
68
63
  return if exclude_visitor? || Split.configuration.disabled?
69
64
  metric_descriptor, goals = normalize_metric(metric_descriptor)
@@ -130,8 +125,6 @@ module Split
130
125
  return experiment_pairs
131
126
  end
132
127
 
133
- protected
134
-
135
128
  def normalize_metric(metric_descriptor)
136
129
  if Hash === metric_descriptor
137
130
  experiment_name = metric_descriptor.keys.first
data/lib/split/metric.rb CHANGED
@@ -84,8 +84,6 @@ module Split
84
84
  end
85
85
  end
86
86
 
87
- private
88
-
89
87
  def self.normalize_metric(label)
90
88
  if Hash === label
91
89
  metric_name = label.keys.first
@@ -96,5 +94,7 @@ module Split
96
94
  end
97
95
  return metric_name, goals
98
96
  end
97
+ private_class_method :normalize_metric
98
+
99
99
  end
100
100
  end
@@ -7,22 +7,19 @@ module Split
7
7
  ADAPTERS = {
8
8
  :cookie => Split::Persistence::CookieAdapter,
9
9
  :session => Split::Persistence::SessionAdapter
10
- }
10
+ }.freeze
11
11
 
12
12
  def self.adapter
13
13
  if persistence_config.is_a?(Symbol)
14
- adapter_class = ADAPTERS[persistence_config]
15
- raise Split::InvalidPersistenceAdapterError unless adapter_class
14
+ ADAPTERS.fetch(persistence_config) { raise Split::InvalidPersistenceAdapterError }
16
15
  else
17
- adapter_class = persistence_config
16
+ persistence_config
18
17
  end
19
- adapter_class
20
18
  end
21
19
 
22
- private
23
-
24
20
  def self.persistence_config
25
21
  Split.configuration.persistence
26
22
  end
23
+ private_class_method :persistence_config
27
24
  end
28
25
  end
data/lib/split/trial.rb CHANGED
@@ -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] if experiment.metadata
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) \
data/lib/split/version.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  module Split
2
2
  MAJOR = 1
3
- MINOR = 1
4
- PATCH = 0
3
+ MINOR = 2
4
+ PATCH = 1
5
5
  VERSION = [MAJOR, MINOR, PATCH].join('.')
6
6
  end
data/lib/split/zscore.rb CHANGED
@@ -34,7 +34,7 @@ module Split
34
34
 
35
35
  # Formula for pooled error of the difference of the means: root(π*(1-π)*(1/na+1/nc)
36
36
  # π = (xa + xc) / (na + nc)
37
- pi = (p_1*n_1 + p_2*n_2)/(n_1 + n_2)
37
+ pi = (p_1*n_1 + p_2*n_2)/(n_1 + n_2)
38
38
  s_p = Math.sqrt(pi*(1-pi)*(1/n_1 + 1/n_2))
39
39
 
40
40
  # Formula for unpooled error of the difference of the means: root(sa**2/na + sc**2/nc)
data/lib/split.rb CHANGED
@@ -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
 
@@ -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
data/spec/helper_spec.rb CHANGED
@@ -198,6 +198,37 @@ describe Split::Helper do
198
198
  end
199
199
  end
200
200
 
201
+ describe 'metadata' do
202
+ before do
203
+ Split.configuration.experiments = {
204
+ :my_experiment => {
205
+ :alternatives => ["one", "two"],
206
+ :resettable => false,
207
+ :metadata => { 'one' => 'Meta1', 'two' => 'Meta2' }
208
+ }
209
+ }
210
+ end
211
+
212
+ it 'should be passed to helper block' do
213
+ @params = {'my_experiment' => 'one'}
214
+ expect(ab_test('my_experiment')).to eq 'one'
215
+ expect(ab_test('my_experiment') do |alternative, meta|
216
+ meta
217
+ end).to eq('Meta1')
218
+ end
219
+
220
+ it 'should pass empty hash to helper block if library disabled' do
221
+ Split.configure do |config|
222
+ config.enabled = false
223
+ end
224
+
225
+ expect(ab_test('my_experiment')).to eq 'one'
226
+ expect(ab_test('my_experiment') do |_, meta|
227
+ meta
228
+ end).to eq({})
229
+ end
230
+ end
231
+
201
232
  describe 'finished' do
202
233
  before(:each) do
203
234
  @experiment_name = 'link_color'
data/spec/spec_helper.rb CHANGED
@@ -32,8 +32,10 @@ def params
32
32
  end
33
33
 
34
34
  def request(ua = 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; de-de) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27')
35
- r = OpenStruct.new
36
- r.user_agent = ua
37
- r.ip = '192.168.1.1'
38
- @request ||= r
35
+ @request ||= begin
36
+ r = OpenStruct.new
37
+ r.user_agent = ua
38
+ r.ip = '192.168.1.1'
39
+ r
40
+ end
39
41
  end
data/spec/trial_spec.rb CHANGED
@@ -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
 
data/split.gemspec CHANGED
@@ -9,7 +9,7 @@ Gem::Specification.new do |s|
9
9
  s.authors = ["Andrew Nesbitt"]
10
10
  s.licenses = ['MIT']
11
11
  s.email = ["andrewnez@gmail.com"]
12
- s.homepage = "https://github.com/andrew/split"
12
+ s.homepage = "https://github.com/splitrb/split"
13
13
  s.summary = %q{Rack based split testing framework}
14
14
 
15
15
  s.required_ruby_version = '>= 1.9.2'
@@ -25,9 +25,9 @@ Gem::Specification.new do |s|
25
25
  s.add_dependency 'sinatra', '>= 1.2.6'
26
26
  s.add_dependency 'simple-random'
27
27
 
28
- s.add_development_dependency 'bundler', '~> 1.6'
28
+ s.add_development_dependency 'bundler', '~> 1.7'
29
29
  s.add_development_dependency 'coveralls'
30
30
  s.add_development_dependency 'rack-test'
31
31
  s.add_development_dependency 'rake'
32
- s.add_development_dependency 'rspec', '~> 3.0'
32
+ s.add_development_dependency 'rspec', '~> 3.1.0'
33
33
  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.1.0
4
+ version: 1.2.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: 2015-01-09 00:00:00.000000000 Z
11
+ date: 2015-05-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis
@@ -72,14 +72,14 @@ dependencies:
72
72
  requirements:
73
73
  - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: '1.6'
75
+ version: '1.7'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: '1.6'
82
+ version: '1.7'
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: coveralls
85
85
  requirement: !ruby/object:Gem::Requirement
@@ -128,14 +128,14 @@ dependencies:
128
128
  requirements:
129
129
  - - "~>"
130
130
  - !ruby/object:Gem::Version
131
- version: '3.0'
131
+ version: 3.1.0
132
132
  type: :development
133
133
  prerelease: false
134
134
  version_requirements: !ruby/object:Gem::Requirement
135
135
  requirements:
136
136
  - - "~>"
137
137
  - !ruby/object:Gem::Version
138
- version: '3.0'
138
+ version: 3.1.0
139
139
  description:
140
140
  email:
141
141
  - andrewnez@gmail.com
@@ -155,6 +155,7 @@ files:
155
155
  - gemfiles/3.2.gemfile
156
156
  - gemfiles/4.0.gemfile
157
157
  - gemfiles/4.1.gemfile
158
+ - gemfiles/4.2.gemfile
158
159
  - lib/split.rb
159
160
  - lib/split/algorithms.rb
160
161
  - lib/split/algorithms/weighted_sample.rb
@@ -208,7 +209,7 @@ files:
208
209
  - spec/support/cookies_mock.rb
209
210
  - spec/trial_spec.rb
210
211
  - split.gemspec
211
- homepage: https://github.com/andrew/split
212
+ homepage: https://github.com/splitrb/split
212
213
  licenses:
213
214
  - MIT
214
215
  metadata: {}