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 +4 -4
- data/.travis.yml +2 -1
- data/Appraisals +7 -3
- data/CHANGELOG.md +36 -0
- data/README.md +55 -25
- data/gemfiles/3.2.gemfile +1 -1
- data/gemfiles/4.0.gemfile +1 -1
- data/gemfiles/4.1.gemfile +1 -1
- data/gemfiles/4.2.gemfile +8 -0
- data/lib/split/algorithms/whiplash.rb +21 -19
- data/lib/split/configuration.rb +4 -0
- data/lib/split/dashboard/public/style.css +0 -1
- data/lib/split/dashboard/views/index.erb +1 -1
- data/lib/split/dashboard/views/layout.erb +1 -1
- data/lib/split/encapsulated_helper.rb +4 -1
- data/lib/split/experiment.rb +29 -1
- data/lib/split/experiment_catalog.rb +3 -9
- data/lib/split/helper.rb +4 -11
- data/lib/split/metric.rb +2 -2
- data/lib/split/persistence.rb +4 -7
- data/lib/split/trial.rb +9 -5
- data/lib/split/version.rb +2 -2
- data/lib/split/zscore.rb +1 -1
- data/lib/split.rb +1 -1
- data/spec/configuration_spec.rb +31 -0
- data/spec/experiment_spec.rb +23 -0
- data/spec/helper_spec.rb +31 -0
- data/spec/spec_helper.rb +6 -4
- data/spec/trial_spec.rb +57 -0
- data/split.gemspec +3 -3
- metadata +8 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b771c2496cd674b0512e956fcdb3bf67768ba4a8
|
|
4
|
+
data.tar.gz: cd3ac62720dbaa5968d58be74ebc689854fd0fac
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f94c6eca1509241995790fefc2407ae8cdc546ceae8aad2a4fa4975c47dd4370f5cc835b630a9c91a1cd7682a24bf49c34af422a31f576af70162003717f396e
|
|
7
|
+
data.tar.gz: 9e47de4c77fa41c39d525c2c79448c3d1e5fdc564dfa340d00857a91cedd11517fc3e871b7f4bb208eab9a5dfe29e2a026dcd40139569d471e146d68bfbc982c
|
data/.travis.yml
CHANGED
data/Appraisals
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
appraise "3.2" do
|
|
2
|
-
gem "rails", "~> 3.2.
|
|
2
|
+
gem "rails", "~> 3.2.21"
|
|
3
3
|
end
|
|
4
4
|
|
|
5
5
|
appraise "4.0" do
|
|
6
|
-
gem "rails", "~> 4.0.
|
|
6
|
+
gem "rails", "~> 4.0.13"
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
appraise "4.1" do
|
|
10
|
-
gem "rails", "~> 4.1.
|
|
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
|
[](http://badge.fury.io/rb/split)
|
|
10
|
-
[](http://travis-ci.org/splitrb/split)
|
|
11
|
+
[](https://gemnasium.com/splitrb/split)
|
|
12
|
+
[](https://codeclimate.com/github/splitrb/split)
|
|
13
|
+
[](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
|
-
<%=
|
|
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/
|
|
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 = :
|
|
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
|
-
|
|
553
|
-
|
|
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::
|
|
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/
|
|
624
|
-
- [Split::Analytics](http://github.com/
|
|
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/
|
|
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/
|
|
646
|
-
Report Issues/Feature requests on [GitHub Issues](http://github.com/
|
|
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/
|
|
694
|
+
Copyright (c) 2015 Andrew Nesbitt. See [LICENSE](https://github.com/splitrb/split/blob/master/LICENSE) for details.
|
data/gemfiles/3.2.gemfile
CHANGED
data/gemfiles/4.0.gemfile
CHANGED
data/gemfiles/4.1.gemfile
CHANGED
|
@@ -5,30 +5,32 @@ require 'simple-random'
|
|
|
5
5
|
module Split
|
|
6
6
|
module Algorithms
|
|
7
7
|
module Whiplash
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
class << self
|
|
9
|
+
def choose_alternative(experiment)
|
|
10
|
+
experiment[best_guess(experiment.alternatives)]
|
|
11
|
+
end
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
private
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
31
|
-
|
|
31
|
+
def fairness_constant
|
|
32
|
+
7
|
|
33
|
+
end
|
|
32
34
|
end
|
|
33
35
|
end
|
|
34
36
|
end
|
data/lib/split/configuration.rb
CHANGED
|
@@ -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
|
|
@@ -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/
|
|
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 %>
|
|
@@ -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
|
data/lib/split/experiment.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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
|
data/lib/split/persistence.rb
CHANGED
|
@@ -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
|
-
|
|
15
|
-
raise Split::InvalidPersistenceAdapterError unless adapter_class
|
|
14
|
+
ADAPTERS.fetch(persistence_config) { raise Split::InvalidPersistenceAdapterError }
|
|
16
15
|
else
|
|
17
|
-
|
|
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
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
data/spec/configuration_spec.rb
CHANGED
|
@@ -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
|
|
data/spec/experiment_spec.rb
CHANGED
|
@@ -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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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/
|
|
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.
|
|
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
|
|
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-
|
|
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.
|
|
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.
|
|
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:
|
|
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:
|
|
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/
|
|
212
|
+
homepage: https://github.com/splitrb/split
|
|
212
213
|
licenses:
|
|
213
214
|
- MIT
|
|
214
215
|
metadata: {}
|