split 2.0.0 → 3.0.0
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/.codeclimate.yml +30 -0
- data/.csslintrc +2 -0
- data/.eslintignore +1 -0
- data/.eslintrc +213 -0
- data/.rubocop.yml +1156 -0
- data/.travis.yml +5 -2
- data/Appraisals +5 -4
- data/CHANGELOG.md +46 -0
- data/Gemfile +1 -0
- data/README.md +123 -20
- data/gemfiles/4.2.gemfile +1 -0
- data/gemfiles/5.0.gemfile +10 -0
- data/lib/split/algorithms/block_randomization.rb +22 -0
- data/lib/split/alternative.rb +29 -8
- data/lib/split/configuration.rb +13 -2
- data/lib/split/dashboard/helpers.rb +5 -1
- data/lib/split/dashboard/public/dashboard-filtering.js +3 -3
- data/lib/split/dashboard/views/_experiment.erb +35 -1
- data/lib/split/dashboard.rb +5 -0
- data/lib/split/encapsulated_helper.rb +4 -15
- data/lib/split/experiment.rb +63 -54
- data/lib/split/goals_collection.rb +1 -1
- data/lib/split/helper.rb +20 -0
- data/lib/split/persistence/dual_adapter.rb +3 -0
- data/lib/split/persistence.rb +5 -3
- data/lib/split/redis_interface.rb +51 -0
- data/lib/split/user.rb +3 -1
- data/lib/split/version.rb +1 -1
- data/lib/split/zscore.rb +1 -1
- data/lib/split.rb +34 -41
- data/spec/algorithms/block_randomization_spec.rb +32 -0
- data/spec/alternative_spec.rb +31 -0
- data/spec/configuration_spec.rb +19 -5
- data/spec/dashboard_helpers_spec.rb +14 -0
- data/spec/dashboard_spec.rb +15 -0
- data/spec/encapsulated_helper_spec.rb +35 -4
- data/spec/experiment_spec.rb +38 -7
- data/spec/helper_spec.rb +59 -16
- data/spec/persistence/dual_adapter_spec.rb +102 -0
- data/spec/redis_interface_spec.rb +111 -0
- data/spec/spec_helper.rb +11 -10
- data/spec/split_spec.rb +43 -0
- data/split.gemspec +2 -3
- metadata +23 -26
- data/gemfiles/4.1.gemfile +0 -8
- data/lib/split/algorithms.rb +0 -4
- data/lib/split/extensions/array.rb +0 -5
- data/lib/split/extensions.rb +0 -4
data/.travis.yml
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
language: ruby
|
|
2
2
|
rvm:
|
|
3
|
-
- 2.
|
|
3
|
+
- 2.4.0
|
|
4
4
|
|
|
5
5
|
gemfile:
|
|
6
|
-
- gemfiles/4.1.gemfile
|
|
7
6
|
- gemfiles/4.2.gemfile
|
|
7
|
+
- gemfiles/5.0.gemfile
|
|
8
8
|
|
|
9
9
|
before_install:
|
|
10
10
|
- gem install bundler
|
|
11
11
|
|
|
12
|
+
script:
|
|
13
|
+
- RAILS_ENV=test bundle exec rake spec && bundle exec codeclimate-test-reporter
|
|
14
|
+
|
|
12
15
|
cache: bundler
|
|
13
16
|
sudo: false
|
data/Appraisals
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,49 @@
|
|
|
1
|
+
## 3.0.0 (March 30th, 2017)
|
|
2
|
+
|
|
3
|
+
Features:
|
|
4
|
+
|
|
5
|
+
- added block randomization algorithm and specs (@hulleywood, #475)
|
|
6
|
+
- Add ab_record_extra_info to allow record extra info to alternative and display on dashboard. (@tranngocsam, #460)
|
|
7
|
+
|
|
8
|
+
Bugfixes:
|
|
9
|
+
|
|
10
|
+
- Avoid crashing on Ruby 2.4 for numeric strings (@flori, #470)
|
|
11
|
+
- Fix issue where redis isn't required (@tomciopp , #466)
|
|
12
|
+
|
|
13
|
+
Misc:
|
|
14
|
+
|
|
15
|
+
- Avoid variable_size_secure_compare private method (@eliotsykes, #465)
|
|
16
|
+
|
|
17
|
+
## 2.2.0 (November 11th, 2016)
|
|
18
|
+
|
|
19
|
+
**Backwards incompatible!** Redis keys are renamed. Please make sure all running tests are completed before you upgrade, as they will reset.
|
|
20
|
+
|
|
21
|
+
Features:
|
|
22
|
+
|
|
23
|
+
- Remove dependency on Redis::Namespace (@bschaeffer, #425)
|
|
24
|
+
- Make resetting on experiment change optional (@moggyboy, #430)
|
|
25
|
+
- Add ability to force alternative on dashboard (@ccallebs, #437)
|
|
26
|
+
|
|
27
|
+
Bugfixes:
|
|
28
|
+
|
|
29
|
+
- Fix variations reset across page loads for multiple=control and improve coverage (@Vasfed, #432)
|
|
30
|
+
|
|
31
|
+
Misc:
|
|
32
|
+
|
|
33
|
+
- Remove Explicit Return (@BradHudson, #441)
|
|
34
|
+
- Update Redis config docs (@bschaeffer, #422)
|
|
35
|
+
- Harden HTTP Basic snippet against timing attacks (@eliotsykes, #443)
|
|
36
|
+
- Removed a couple old ruby 1.8 hacks (@andrew, #456)
|
|
37
|
+
- Run tests on rails 5 (@andrew, #457)
|
|
38
|
+
- Fixed a few codeclimate warnings (@andrew, #458)
|
|
39
|
+
- Use codeclimate for test coverage (@andrew #455)
|
|
40
|
+
|
|
41
|
+
## 2.1.0 (August 8th, 2016)
|
|
42
|
+
|
|
43
|
+
Features:
|
|
44
|
+
|
|
45
|
+
- Support REDIS_PROVIDER variable used in Heroku (@kartikluke, #426)
|
|
46
|
+
|
|
1
47
|
## 2.0.0 (July 17th, 2016)
|
|
2
48
|
|
|
3
49
|
Breaking changes:
|
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -8,8 +8,82 @@ Split is designed to be hacker friendly, allowing for maximum customisation and
|
|
|
8
8
|
|
|
9
9
|
[](http://badge.fury.io/rb/split)
|
|
10
10
|
[](http://travis-ci.org/splitrb/split)
|
|
11
|
-
[](https://codeclimate.com/github/splitrb/split)
|
|
12
|
-
[](https://codeclimate.com/github/splitrb/split)
|
|
12
|
+
[](https://codeclimate.com/github/splitrb/split/coverage)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Backers
|
|
16
|
+
|
|
17
|
+
Support us with a monthly donation and help us continue our activities. [[Become a backer](https://opencollective.com/split#backer)]
|
|
18
|
+
|
|
19
|
+
<a href="https://opencollective.com/split/backer/0/website" target="_blank"><img src="https://opencollective.com/split/backer/0/avatar.svg"></a>
|
|
20
|
+
<a href="https://opencollective.com/split/backer/1/website" target="_blank"><img src="https://opencollective.com/split/backer/1/avatar.svg"></a>
|
|
21
|
+
<a href="https://opencollective.com/split/backer/2/website" target="_blank"><img src="https://opencollective.com/split/backer/2/avatar.svg"></a>
|
|
22
|
+
<a href="https://opencollective.com/split/backer/3/website" target="_blank"><img src="https://opencollective.com/split/backer/3/avatar.svg"></a>
|
|
23
|
+
<a href="https://opencollective.com/split/backer/4/website" target="_blank"><img src="https://opencollective.com/split/backer/4/avatar.svg"></a>
|
|
24
|
+
<a href="https://opencollective.com/split/backer/5/website" target="_blank"><img src="https://opencollective.com/split/backer/5/avatar.svg"></a>
|
|
25
|
+
<a href="https://opencollective.com/split/backer/6/website" target="_blank"><img src="https://opencollective.com/split/backer/6/avatar.svg"></a>
|
|
26
|
+
<a href="https://opencollective.com/split/backer/7/website" target="_blank"><img src="https://opencollective.com/split/backer/7/avatar.svg"></a>
|
|
27
|
+
<a href="https://opencollective.com/split/backer/8/website" target="_blank"><img src="https://opencollective.com/split/backer/8/avatar.svg"></a>
|
|
28
|
+
<a href="https://opencollective.com/split/backer/9/website" target="_blank"><img src="https://opencollective.com/split/backer/9/avatar.svg"></a>
|
|
29
|
+
<a href="https://opencollective.com/split/backer/10/website" target="_blank"><img src="https://opencollective.com/split/backer/10/avatar.svg"></a>
|
|
30
|
+
<a href="https://opencollective.com/split/backer/11/website" target="_blank"><img src="https://opencollective.com/split/backer/11/avatar.svg"></a>
|
|
31
|
+
<a href="https://opencollective.com/split/backer/12/website" target="_blank"><img src="https://opencollective.com/split/backer/12/avatar.svg"></a>
|
|
32
|
+
<a href="https://opencollective.com/split/backer/13/website" target="_blank"><img src="https://opencollective.com/split/backer/13/avatar.svg"></a>
|
|
33
|
+
<a href="https://opencollective.com/split/backer/14/website" target="_blank"><img src="https://opencollective.com/split/backer/14/avatar.svg"></a>
|
|
34
|
+
<a href="https://opencollective.com/split/backer/15/website" target="_blank"><img src="https://opencollective.com/split/backer/15/avatar.svg"></a>
|
|
35
|
+
<a href="https://opencollective.com/split/backer/16/website" target="_blank"><img src="https://opencollective.com/split/backer/16/avatar.svg"></a>
|
|
36
|
+
<a href="https://opencollective.com/split/backer/17/website" target="_blank"><img src="https://opencollective.com/split/backer/17/avatar.svg"></a>
|
|
37
|
+
<a href="https://opencollective.com/split/backer/18/website" target="_blank"><img src="https://opencollective.com/split/backer/18/avatar.svg"></a>
|
|
38
|
+
<a href="https://opencollective.com/split/backer/19/website" target="_blank"><img src="https://opencollective.com/split/backer/19/avatar.svg"></a>
|
|
39
|
+
<a href="https://opencollective.com/split/backer/20/website" target="_blank"><img src="https://opencollective.com/split/backer/20/avatar.svg"></a>
|
|
40
|
+
<a href="https://opencollective.com/split/backer/21/website" target="_blank"><img src="https://opencollective.com/split/backer/21/avatar.svg"></a>
|
|
41
|
+
<a href="https://opencollective.com/split/backer/22/website" target="_blank"><img src="https://opencollective.com/split/backer/22/avatar.svg"></a>
|
|
42
|
+
<a href="https://opencollective.com/split/backer/23/website" target="_blank"><img src="https://opencollective.com/split/backer/23/avatar.svg"></a>
|
|
43
|
+
<a href="https://opencollective.com/split/backer/24/website" target="_blank"><img src="https://opencollective.com/split/backer/24/avatar.svg"></a>
|
|
44
|
+
<a href="https://opencollective.com/split/backer/25/website" target="_blank"><img src="https://opencollective.com/split/backer/25/avatar.svg"></a>
|
|
45
|
+
<a href="https://opencollective.com/split/backer/26/website" target="_blank"><img src="https://opencollective.com/split/backer/26/avatar.svg"></a>
|
|
46
|
+
<a href="https://opencollective.com/split/backer/27/website" target="_blank"><img src="https://opencollective.com/split/backer/27/avatar.svg"></a>
|
|
47
|
+
<a href="https://opencollective.com/split/backer/28/website" target="_blank"><img src="https://opencollective.com/split/backer/28/avatar.svg"></a>
|
|
48
|
+
<a href="https://opencollective.com/split/backer/29/website" target="_blank"><img src="https://opencollective.com/split/backer/29/avatar.svg"></a>
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# Sponsors
|
|
52
|
+
|
|
53
|
+
Become a sponsor and get your logo on our README on Github with a link to your site. [[Become a sponsor](https://opencollective.com/split#sponsor)]
|
|
54
|
+
|
|
55
|
+
<a href="https://opencollective.com/split/sponsor/0/website" target="_blank"><img src="https://opencollective.com/split/sponsor/0/avatar.svg"></a>
|
|
56
|
+
<a href="https://opencollective.com/split/sponsor/1/website" target="_blank"><img src="https://opencollective.com/split/sponsor/1/avatar.svg"></a>
|
|
57
|
+
<a href="https://opencollective.com/split/sponsor/2/website" target="_blank"><img src="https://opencollective.com/split/sponsor/2/avatar.svg"></a>
|
|
58
|
+
<a href="https://opencollective.com/split/sponsor/3/website" target="_blank"><img src="https://opencollective.com/split/sponsor/3/avatar.svg"></a>
|
|
59
|
+
<a href="https://opencollective.com/split/sponsor/4/website" target="_blank"><img src="https://opencollective.com/split/sponsor/4/avatar.svg"></a>
|
|
60
|
+
<a href="https://opencollective.com/split/sponsor/5/website" target="_blank"><img src="https://opencollective.com/split/sponsor/5/avatar.svg"></a>
|
|
61
|
+
<a href="https://opencollective.com/split/sponsor/6/website" target="_blank"><img src="https://opencollective.com/split/sponsor/6/avatar.svg"></a>
|
|
62
|
+
<a href="https://opencollective.com/split/sponsor/7/website" target="_blank"><img src="https://opencollective.com/split/sponsor/7/avatar.svg"></a>
|
|
63
|
+
<a href="https://opencollective.com/split/sponsor/8/website" target="_blank"><img src="https://opencollective.com/split/sponsor/8/avatar.svg"></a>
|
|
64
|
+
<a href="https://opencollective.com/split/sponsor/9/website" target="_blank"><img src="https://opencollective.com/split/sponsor/9/avatar.svg"></a>
|
|
65
|
+
<a href="https://opencollective.com/split/sponsor/10/website" target="_blank"><img src="https://opencollective.com/split/sponsor/10/avatar.svg"></a>
|
|
66
|
+
<a href="https://opencollective.com/split/sponsor/11/website" target="_blank"><img src="https://opencollective.com/split/sponsor/11/avatar.svg"></a>
|
|
67
|
+
<a href="https://opencollective.com/split/sponsor/12/website" target="_blank"><img src="https://opencollective.com/split/sponsor/12/avatar.svg"></a>
|
|
68
|
+
<a href="https://opencollective.com/split/sponsor/13/website" target="_blank"><img src="https://opencollective.com/split/sponsor/13/avatar.svg"></a>
|
|
69
|
+
<a href="https://opencollective.com/split/sponsor/14/website" target="_blank"><img src="https://opencollective.com/split/sponsor/14/avatar.svg"></a>
|
|
70
|
+
<a href="https://opencollective.com/split/sponsor/15/website" target="_blank"><img src="https://opencollective.com/split/sponsor/15/avatar.svg"></a>
|
|
71
|
+
<a href="https://opencollective.com/split/sponsor/16/website" target="_blank"><img src="https://opencollective.com/split/sponsor/16/avatar.svg"></a>
|
|
72
|
+
<a href="https://opencollective.com/split/sponsor/17/website" target="_blank"><img src="https://opencollective.com/split/sponsor/17/avatar.svg"></a>
|
|
73
|
+
<a href="https://opencollective.com/split/sponsor/18/website" target="_blank"><img src="https://opencollective.com/split/sponsor/18/avatar.svg"></a>
|
|
74
|
+
<a href="https://opencollective.com/split/sponsor/19/website" target="_blank"><img src="https://opencollective.com/split/sponsor/19/avatar.svg"></a>
|
|
75
|
+
<a href="https://opencollective.com/split/sponsor/20/website" target="_blank"><img src="https://opencollective.com/split/sponsor/20/avatar.svg"></a>
|
|
76
|
+
<a href="https://opencollective.com/split/sponsor/21/website" target="_blank"><img src="https://opencollective.com/split/sponsor/21/avatar.svg"></a>
|
|
77
|
+
<a href="https://opencollective.com/split/sponsor/22/website" target="_blank"><img src="https://opencollective.com/split/sponsor/22/avatar.svg"></a>
|
|
78
|
+
<a href="https://opencollective.com/split/sponsor/23/website" target="_blank"><img src="https://opencollective.com/split/sponsor/23/avatar.svg"></a>
|
|
79
|
+
<a href="https://opencollective.com/split/sponsor/24/website" target="_blank"><img src="https://opencollective.com/split/sponsor/24/avatar.svg"></a>
|
|
80
|
+
<a href="https://opencollective.com/split/sponsor/25/website" target="_blank"><img src="https://opencollective.com/split/sponsor/25/avatar.svg"></a>
|
|
81
|
+
<a href="https://opencollective.com/split/sponsor/26/website" target="_blank"><img src="https://opencollective.com/split/sponsor/26/avatar.svg"></a>
|
|
82
|
+
<a href="https://opencollective.com/split/sponsor/27/website" target="_blank"><img src="https://opencollective.com/split/sponsor/27/avatar.svg"></a>
|
|
83
|
+
<a href="https://opencollective.com/split/sponsor/28/website" target="_blank"><img src="https://opencollective.com/split/sponsor/28/avatar.svg"></a>
|
|
84
|
+
<a href="https://opencollective.com/split/sponsor/29/website" target="_blank"><img src="https://opencollective.com/split/sponsor/29/avatar.svg"></a>
|
|
85
|
+
|
|
86
|
+
|
|
13
87
|
|
|
14
88
|
## Requirements
|
|
15
89
|
|
|
@@ -361,8 +435,22 @@ mount Split::Dashboard, at: 'split'
|
|
|
361
435
|
You may want to password protect that page, you can do so with `Rack::Auth::Basic` (in your split initializer file)
|
|
362
436
|
|
|
363
437
|
```ruby
|
|
438
|
+
# Rails apps or apps that already depend on activesupport
|
|
439
|
+
Split::Dashboard.use Rack::Auth::Basic do |username, password|
|
|
440
|
+
# Protect against timing attacks:
|
|
441
|
+
# - Use & (do not use &&) so that it doesn't short circuit.
|
|
442
|
+
# - Use digests to stop length information leaking
|
|
443
|
+
ActiveSupport::SecurityUtils.secure_compare(::Digest::SHA256.hexdigest(username), ::Digest::SHA256.hexdigest(ENV["SPLIT_USERNAME"])) &
|
|
444
|
+
ActiveSupport::SecurityUtils.secure_compare(::Digest::SHA256.hexdigest(password), ::Digest::SHA256.hexdigest(ENV["SPLIT_PASSWORD"]))
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
# Apps without activesupport
|
|
364
448
|
Split::Dashboard.use Rack::Auth::Basic do |username, password|
|
|
365
|
-
|
|
449
|
+
# Protect against timing attacks:
|
|
450
|
+
# - Use & (do not use &&) so that it doesn't short circuit.
|
|
451
|
+
# - Use digests to stop length information leaking
|
|
452
|
+
Rack::Utils.secure_compare(::Digest::SHA256.hexdigest(username), ::Digest::SHA256.hexdigest(ENV["SPLIT_USERNAME"])) &
|
|
453
|
+
Rack::Utils.secure_compare(::Digest::SHA256.hexdigest(password), ::Digest::SHA256.hexdigest(ENV["SPLIT_PASSWORD"]))
|
|
366
454
|
end
|
|
367
455
|
```
|
|
368
456
|
|
|
@@ -394,11 +482,16 @@ Split.configure do |config|
|
|
|
394
482
|
config.persistence = Split::Persistence::SessionAdapter
|
|
395
483
|
#config.start_manually = false ## new test will have to be started manually from the admin panel. default false
|
|
396
484
|
config.include_rails_helper = true
|
|
397
|
-
config.
|
|
485
|
+
config.redis = "redis://custom.redis.url:6380"
|
|
398
486
|
end
|
|
399
487
|
```
|
|
400
488
|
|
|
401
|
-
Split looks for the Redis host in the environment variable
|
|
489
|
+
Split looks for the Redis host in the environment variable `REDIS_URL` then
|
|
490
|
+
defaults to `redis://localhost:6379` if not specified by configure block.
|
|
491
|
+
|
|
492
|
+
On platforms like Heroku, Split will use the value of `REDIS_PROVIDER` to
|
|
493
|
+
determine which env variable key to use when retrieving the host config. This
|
|
494
|
+
defaults to `REDIS_URL`.
|
|
402
495
|
|
|
403
496
|
### Filtering
|
|
404
497
|
|
|
@@ -619,7 +712,7 @@ Split has a `redis` setter which can be given a string or a Redis
|
|
|
619
712
|
object. This means if you're already using Redis in your app, Split
|
|
620
713
|
can re-use the existing connection.
|
|
621
714
|
|
|
622
|
-
String: `Split.redis = 'localhost:6379'`
|
|
715
|
+
String: `Split.redis = 'redis://localhost:6379'`
|
|
623
716
|
|
|
624
717
|
Redis: `Split.redis = $redis`
|
|
625
718
|
|
|
@@ -630,11 +723,11 @@ appropriately.
|
|
|
630
723
|
Here's our `config/split.yml`:
|
|
631
724
|
|
|
632
725
|
```yml
|
|
633
|
-
development: localhost:6379
|
|
634
|
-
test: localhost:6379
|
|
635
|
-
staging: redis1.example.com:6379
|
|
636
|
-
fi: localhost:6379
|
|
637
|
-
production: redis1.example.com:6379
|
|
726
|
+
development: redis://localhost:6379
|
|
727
|
+
test: redis://localhost:6379
|
|
728
|
+
staging: redis://redis1.example.com:6379
|
|
729
|
+
fi: redis://localhost:6379
|
|
730
|
+
production: redis://redis1.example.com:6379
|
|
638
731
|
```
|
|
639
732
|
|
|
640
733
|
And our initializer:
|
|
@@ -650,18 +743,22 @@ If you're running multiple, separate instances of Split you may want
|
|
|
650
743
|
to namespace the keyspaces so they do not overlap. This is not unlike
|
|
651
744
|
the approach taken by many memcached clients.
|
|
652
745
|
|
|
653
|
-
This feature
|
|
654
|
-
|
|
655
|
-
in your Redis server.
|
|
746
|
+
This feature can be provided by the [redis-namespace](https://github.com/defunkt/redis-namespace)
|
|
747
|
+
library. To configure Split to use `Redis::Namespace`, do the following:
|
|
656
748
|
|
|
657
|
-
|
|
749
|
+
1. Add `redis-namespace` to your Gemfile:
|
|
658
750
|
|
|
659
|
-
```ruby
|
|
660
|
-
|
|
661
|
-
```
|
|
751
|
+
```ruby
|
|
752
|
+
gem 'redis-namespace'
|
|
753
|
+
```
|
|
662
754
|
|
|
663
|
-
|
|
664
|
-
|
|
755
|
+
2. Configure `Split.redis` to use a `Redis::Namespace` instance (possible in an
|
|
756
|
+
intializer):
|
|
757
|
+
|
|
758
|
+
```ruby
|
|
759
|
+
redis = Redis.new(url: ENV['REDIS_URL']) # or whatever config you want
|
|
760
|
+
Split.redis = Redis::Namespace.new(:your_namespace, redis: redis)
|
|
761
|
+
```
|
|
665
762
|
|
|
666
763
|
## Outside of a Web Session
|
|
667
764
|
|
|
@@ -696,6 +793,12 @@ It is possible to specify static weights to favor certain alternatives.
|
|
|
696
793
|
This algorithm will automatically weight the alternatives based on their relative performance,
|
|
697
794
|
choosing the better-performing ones more often as trials are completed.
|
|
698
795
|
|
|
796
|
+
`Split::Algorithms::BlockRandomization` is an algorithm that ensures equal
|
|
797
|
+
participation across all alternatives. This algorithm will choose the alternative
|
|
798
|
+
with the fewest participants. In the event of multiple minimum participant alternatives
|
|
799
|
+
(i.e. starting a new "Block") the algorithm will choose a random alternative from
|
|
800
|
+
those minimum participant alternatives.
|
|
801
|
+
|
|
699
802
|
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.
|
|
700
803
|
|
|
701
804
|
To change the algorithm globally for all experiments, use the following in your initializer:
|
data/gemfiles/4.2.gemfile
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Selects alternative with minimum count of participants
|
|
2
|
+
# If all counts are even (i.e. all are minimum), samples from all possible alternatives
|
|
3
|
+
|
|
4
|
+
module Split
|
|
5
|
+
module Algorithms
|
|
6
|
+
module BlockRandomization
|
|
7
|
+
class << self
|
|
8
|
+
def choose_alternative(experiment)
|
|
9
|
+
minimum_participant_alternatives(experiment.alternatives).sample
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def minimum_participant_alternatives(alternatives)
|
|
15
|
+
alternatives_by_count = alternatives.group_by(&:participant_count)
|
|
16
|
+
min_group = alternatives_by_count.min_by { |k, v| k }
|
|
17
|
+
min_group.last
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
data/lib/split/alternative.rb
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
require 'split/zscore'
|
|
3
|
-
|
|
4
|
-
# TODO - take out require and implement using file paths?
|
|
5
|
-
|
|
6
2
|
module Split
|
|
7
3
|
class Alternative
|
|
8
4
|
attr_accessor :name
|
|
9
5
|
attr_accessor :experiment_name
|
|
10
6
|
attr_accessor :weight
|
|
11
|
-
|
|
12
|
-
include Zscore
|
|
7
|
+
attr_accessor :recorded_info
|
|
13
8
|
|
|
14
9
|
def initialize(name, experiment_name)
|
|
15
10
|
@experiment_name = experiment_name
|
|
@@ -34,7 +29,6 @@ module Split
|
|
|
34
29
|
def p_winner(goal = nil)
|
|
35
30
|
field = set_prob_field(goal)
|
|
36
31
|
@p_winner = Split.redis.hget(key, field).to_f
|
|
37
|
-
return @p_winner
|
|
38
32
|
end
|
|
39
33
|
|
|
40
34
|
def set_p_winner(prob, goal = nil)
|
|
@@ -128,10 +122,37 @@ module Split
|
|
|
128
122
|
z_score = Split::Zscore.calculate(p_a, n_a, p_c, n_c)
|
|
129
123
|
end
|
|
130
124
|
|
|
125
|
+
def extra_info
|
|
126
|
+
data = Split.redis.hget(key, 'recorded_info')
|
|
127
|
+
if data && data.length > 1
|
|
128
|
+
begin
|
|
129
|
+
JSON.parse(data)
|
|
130
|
+
rescue
|
|
131
|
+
{}
|
|
132
|
+
end
|
|
133
|
+
else
|
|
134
|
+
{}
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def record_extra_info(k, value = 1)
|
|
139
|
+
@recorded_info = self.extra_info || {}
|
|
140
|
+
|
|
141
|
+
if value.kind_of?(Numeric)
|
|
142
|
+
@recorded_info[k] ||= 0
|
|
143
|
+
@recorded_info[k] += value
|
|
144
|
+
else
|
|
145
|
+
@recorded_info[k] = value
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
Split.redis.hset key, 'recorded_info', (@recorded_info || {}).to_json
|
|
149
|
+
end
|
|
150
|
+
|
|
131
151
|
def save
|
|
132
152
|
Split.redis.hsetnx key, 'participant_count', 0
|
|
133
153
|
Split.redis.hsetnx key, 'completed_count', 0
|
|
134
154
|
Split.redis.hsetnx key, 'p_winner', p_winner
|
|
155
|
+
Split.redis.hsetnx key, 'recorded_info', (@recorded_info || {}).to_json
|
|
135
156
|
end
|
|
136
157
|
|
|
137
158
|
def validate!
|
|
@@ -141,7 +162,7 @@ module Split
|
|
|
141
162
|
end
|
|
142
163
|
|
|
143
164
|
def reset
|
|
144
|
-
Split.redis.hmset key, 'participant_count', 0, 'completed_count', 0
|
|
165
|
+
Split.redis.hmset key, 'participant_count', 0, 'completed_count', 0, 'recorded_info', nil
|
|
145
166
|
unless goals.empty?
|
|
146
167
|
goals.each do |g|
|
|
147
168
|
field = "completed_count:#{g}"
|
data/lib/split/configuration.rb
CHANGED
|
@@ -15,6 +15,7 @@ module Split
|
|
|
15
15
|
attr_accessor :algorithm
|
|
16
16
|
attr_accessor :store_override
|
|
17
17
|
attr_accessor :start_manually
|
|
18
|
+
attr_accessor :reset_manually
|
|
18
19
|
attr_accessor :on_trial
|
|
19
20
|
attr_accessor :on_trial_choose
|
|
20
21
|
attr_accessor :on_trial_complete
|
|
@@ -24,7 +25,7 @@ module Split
|
|
|
24
25
|
attr_accessor :on_before_experiment_delete
|
|
25
26
|
attr_accessor :include_rails_helper
|
|
26
27
|
attr_accessor :beta_probability_simulations
|
|
27
|
-
attr_accessor :
|
|
28
|
+
attr_accessor :redis
|
|
28
29
|
|
|
29
30
|
attr_reader :experiments
|
|
30
31
|
|
|
@@ -211,7 +212,17 @@ module Split
|
|
|
211
212
|
@algorithm = Split::Algorithms::WeightedSample
|
|
212
213
|
@include_rails_helper = true
|
|
213
214
|
@beta_probability_simulations = 10000
|
|
214
|
-
@
|
|
215
|
+
@redis = ENV.fetch(ENV.fetch('REDIS_PROVIDER', 'REDIS_URL'), 'redis://localhost:6379')
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def redis_url=(value)
|
|
219
|
+
warn '[DEPRECATED] `redis_url=` is deprecated in favor of `redis=`'
|
|
220
|
+
self.redis = value
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def redis_url
|
|
224
|
+
warn '[DEPRECATED] `redis_url` is deprecated in favor of `redis`'
|
|
225
|
+
self.redis
|
|
215
226
|
end
|
|
216
227
|
|
|
217
228
|
private
|
|
@@ -18,7 +18,11 @@ module Split
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def round(number, precision = 2)
|
|
21
|
-
|
|
21
|
+
begin
|
|
22
|
+
BigDecimal.new(number.to_s)
|
|
23
|
+
rescue ArgumentError
|
|
24
|
+
BigDecimal.new(0)
|
|
25
|
+
end.round(precision).to_f
|
|
22
26
|
end
|
|
23
27
|
|
|
24
28
|
def confidence_level(z_score)
|
|
@@ -2,7 +2,7 @@ $(function() {
|
|
|
2
2
|
$('#filter').on('keyup', function() {
|
|
3
3
|
$input = $(this);
|
|
4
4
|
|
|
5
|
-
if ($input.val()
|
|
5
|
+
if ($input.val() === '') {
|
|
6
6
|
$('div.experiment').show();
|
|
7
7
|
return false;
|
|
8
8
|
}
|
|
@@ -21,7 +21,7 @@ $(function() {
|
|
|
21
21
|
|
|
22
22
|
$('#toggle-active').on('click', function() {
|
|
23
23
|
$button = $(this);
|
|
24
|
-
if ($button.val()
|
|
24
|
+
if ($button.val() === 'Hide active') {
|
|
25
25
|
$button.val('Show active');
|
|
26
26
|
} else {
|
|
27
27
|
$button.val('Hide active');
|
|
@@ -32,7 +32,7 @@ $(function() {
|
|
|
32
32
|
|
|
33
33
|
$('#toggle-completed').on('click', function() {
|
|
34
34
|
$button = $(this);
|
|
35
|
-
if ($button.val()
|
|
35
|
+
if ($button.val() === 'Hide completed') {
|
|
36
36
|
$button.val('Show completed');
|
|
37
37
|
} else {
|
|
38
38
|
$button.val('Hide completed');
|
|
@@ -5,6 +5,25 @@
|
|
|
5
5
|
<% end %>
|
|
6
6
|
|
|
7
7
|
<% experiment.calc_winning_alternatives %>
|
|
8
|
+
<%
|
|
9
|
+
extra_columns = []
|
|
10
|
+
experiment.alternatives.each do |alternative|
|
|
11
|
+
extra_info = alternative.extra_info || {}
|
|
12
|
+
extra_columns += extra_info.keys
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
extra_columns.uniq!
|
|
16
|
+
summary_texts = {}
|
|
17
|
+
extra_columns.each do |column|
|
|
18
|
+
extra_infos = experiment.alternatives.map(&:extra_info).select{|extra_info| extra_info && extra_info[column] }
|
|
19
|
+
if extra_infos[0][column].kind_of?(Numeric)
|
|
20
|
+
summary_texts[column] = extra_infos.inject(0){|sum, extra_info| sum += extra_info[column]}
|
|
21
|
+
else
|
|
22
|
+
summary_texts[column] = "N/A"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
%>
|
|
26
|
+
|
|
8
27
|
|
|
9
28
|
<div class="<%= experiment_class %>" data-name="<%= experiment.name %>" data-complete="<%= experiment.has_winner? %>">
|
|
10
29
|
<div class="experiment-header">
|
|
@@ -32,6 +51,9 @@
|
|
|
32
51
|
<th>Non-finished</th>
|
|
33
52
|
<th>Completed</th>
|
|
34
53
|
<th>Conversion Rate</th>
|
|
54
|
+
<% extra_columns.each do |column| %>
|
|
55
|
+
<th><%= column %></th>
|
|
56
|
+
<% end %>
|
|
35
57
|
<th>
|
|
36
58
|
<form>
|
|
37
59
|
<select id="dropdown-<%=experiment.jstring(goal)%>" name="dropdown-<%=experiment.jstring(goal)%>">
|
|
@@ -51,6 +73,10 @@
|
|
|
51
73
|
<% if alternative.control? %>
|
|
52
74
|
<em>control</em>
|
|
53
75
|
<% end %>
|
|
76
|
+
<form action="<%= url('force_alternative') + '?experiment=' + experiment.name %>" method='post'>
|
|
77
|
+
<input type='hidden' name='alternative' value='<%= h alternative.name %>'>
|
|
78
|
+
<input type="submit" value="Force for current user" class="green">
|
|
79
|
+
</form>
|
|
54
80
|
</td>
|
|
55
81
|
<td><%= alternative.participant_count %></td>
|
|
56
82
|
<td><%= alternative.unfinished_count %></td>
|
|
@@ -78,6 +104,9 @@
|
|
|
78
104
|
});
|
|
79
105
|
});
|
|
80
106
|
</script>
|
|
107
|
+
<% extra_columns.each do |column| %>
|
|
108
|
+
<td><%= alternative.extra_info && alternative.extra_info[column] %></td>
|
|
109
|
+
<% end %>
|
|
81
110
|
<td>
|
|
82
111
|
<div class="box-<%=experiment.jstring(goal)%> confidence-<%=experiment.jstring(goal)%>">
|
|
83
112
|
<span title='z-score: <%= round(alternative.z_score(goal), 3) %>'><%= confidence_level(alternative.z_score(goal)) %></span>
|
|
@@ -86,7 +115,7 @@
|
|
|
86
115
|
<div class="box-<%=experiment.jstring(goal)%> probability-<%=experiment.jstring(goal)%>">
|
|
87
116
|
<span title="p_winner: <%= round(alternative.p_winner(goal), 3) %>"><%= number_to_percentage(round(alternative.p_winner(goal), 3)) %>%</span>
|
|
88
117
|
</div>
|
|
89
|
-
</td>
|
|
118
|
+
</td>
|
|
90
119
|
<td>
|
|
91
120
|
<% if experiment.has_winner? %>
|
|
92
121
|
<% if experiment.winner.name == alternative.name %>
|
|
@@ -114,6 +143,11 @@
|
|
|
114
143
|
<td><%= total_unfinished %></td>
|
|
115
144
|
<td><%= total_completed %></td>
|
|
116
145
|
<td>N/A</td>
|
|
146
|
+
<% extra_columns.each do |column| %>
|
|
147
|
+
<td>
|
|
148
|
+
<%= summary_texts[column] %>
|
|
149
|
+
</td>
|
|
150
|
+
<% end %>
|
|
117
151
|
<td>N/A</td>
|
|
118
152
|
<td>N/A</td>
|
|
119
153
|
</tr>
|
data/lib/split/dashboard.rb
CHANGED
|
@@ -30,6 +30,11 @@ module Split
|
|
|
30
30
|
erb :index
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
post '/force_alternative' do
|
|
34
|
+
Split::User.new(self)[params[:experiment]] = params[:alternative]
|
|
35
|
+
redirect url('/')
|
|
36
|
+
end
|
|
37
|
+
|
|
33
38
|
post '/experiment' do
|
|
34
39
|
@experiment = Split::ExperimentCatalog.find(params[:experiment])
|
|
35
40
|
@alternative = Split::Alternative.new(params[:alternative], params[:experiment])
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
require "split/helper"
|
|
3
|
+
|
|
2
4
|
# Split's helper exposes all kinds of methods we don't want to
|
|
3
5
|
# mix into our model classes.
|
|
4
6
|
#
|
|
@@ -26,21 +28,8 @@ module Split
|
|
|
26
28
|
end
|
|
27
29
|
end
|
|
28
30
|
|
|
29
|
-
def ab_test(*arguments)
|
|
30
|
-
|
|
31
|
-
# TODO there must be a better way to pass a block straight
|
|
32
|
-
# through to the original ab_test
|
|
33
|
-
if block_given?
|
|
34
|
-
if defined?(capture) # a block in a rails view
|
|
35
|
-
block = Proc.new { yield(ret) }
|
|
36
|
-
concat(capture(ret, &block))
|
|
37
|
-
false
|
|
38
|
-
else
|
|
39
|
-
yield(ret)
|
|
40
|
-
end
|
|
41
|
-
else
|
|
42
|
-
ret
|
|
43
|
-
end
|
|
31
|
+
def ab_test(*arguments,&block)
|
|
32
|
+
split_context_shim.ab_test(*arguments,&block)
|
|
44
33
|
end
|
|
45
34
|
|
|
46
35
|
private
|