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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +30 -0
  3. data/.csslintrc +2 -0
  4. data/.eslintignore +1 -0
  5. data/.eslintrc +213 -0
  6. data/.rubocop.yml +1156 -0
  7. data/.travis.yml +5 -2
  8. data/Appraisals +5 -4
  9. data/CHANGELOG.md +46 -0
  10. data/Gemfile +1 -0
  11. data/README.md +123 -20
  12. data/gemfiles/4.2.gemfile +1 -0
  13. data/gemfiles/5.0.gemfile +10 -0
  14. data/lib/split/algorithms/block_randomization.rb +22 -0
  15. data/lib/split/alternative.rb +29 -8
  16. data/lib/split/configuration.rb +13 -2
  17. data/lib/split/dashboard/helpers.rb +5 -1
  18. data/lib/split/dashboard/public/dashboard-filtering.js +3 -3
  19. data/lib/split/dashboard/views/_experiment.erb +35 -1
  20. data/lib/split/dashboard.rb +5 -0
  21. data/lib/split/encapsulated_helper.rb +4 -15
  22. data/lib/split/experiment.rb +63 -54
  23. data/lib/split/goals_collection.rb +1 -1
  24. data/lib/split/helper.rb +20 -0
  25. data/lib/split/persistence/dual_adapter.rb +3 -0
  26. data/lib/split/persistence.rb +5 -3
  27. data/lib/split/redis_interface.rb +51 -0
  28. data/lib/split/user.rb +3 -1
  29. data/lib/split/version.rb +1 -1
  30. data/lib/split/zscore.rb +1 -1
  31. data/lib/split.rb +34 -41
  32. data/spec/algorithms/block_randomization_spec.rb +32 -0
  33. data/spec/alternative_spec.rb +31 -0
  34. data/spec/configuration_spec.rb +19 -5
  35. data/spec/dashboard_helpers_spec.rb +14 -0
  36. data/spec/dashboard_spec.rb +15 -0
  37. data/spec/encapsulated_helper_spec.rb +35 -4
  38. data/spec/experiment_spec.rb +38 -7
  39. data/spec/helper_spec.rb +59 -16
  40. data/spec/persistence/dual_adapter_spec.rb +102 -0
  41. data/spec/redis_interface_spec.rb +111 -0
  42. data/spec/spec_helper.rb +11 -10
  43. data/spec/split_spec.rb +43 -0
  44. data/split.gemspec +2 -3
  45. metadata +23 -26
  46. data/gemfiles/4.1.gemfile +0 -8
  47. data/lib/split/algorithms.rb +0 -4
  48. data/lib/split/extensions/array.rb +0 -5
  49. 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.1
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
@@ -1,7 +1,8 @@
1
- appraise "4.1" do
2
- gem "rails", "~> 4.1"
3
- end
4
-
5
1
  appraise "4.2" do
6
2
  gem "rails", "~> 4.2"
7
3
  end
4
+
5
+ appraise "5.0" do
6
+ gem "rails", "~> 5.0"
7
+ gem "sinatra", git: "https://github.com/sinatra/sinatra"
8
+ end
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
@@ -3,3 +3,4 @@ source "https://rubygems.org"
3
3
  gemspec
4
4
 
5
5
  gem "appraisal"
6
+ gem "codeclimate-test-reporter"
data/README.md CHANGED
@@ -8,8 +8,82 @@ Split is designed to be hacker friendly, allowing for maximum customisation and
8
8
 
9
9
  [![Gem Version](https://badge.fury.io/rb/split.svg)](http://badge.fury.io/rb/split)
10
10
  [![Build Status](https://secure.travis-ci.org/splitrb/split.svg?branch=master)](http://travis-ci.org/splitrb/split)
11
- [![Code Climate](https://codeclimate.com/github/splitrb/split.svg)](https://codeclimate.com/github/splitrb/split)
12
- [![Coverage Status](http://img.shields.io/coveralls/splitrb/split.svg)](https://coveralls.io/r/splitrb/split)
11
+ [![Code Climate](https://codeclimate.com/github/splitrb/split/badges/gpa.svg)](https://codeclimate.com/github/splitrb/split)
12
+ [![Test Coverage](https://codeclimate.com/github/splitrb/split/badges/coverage.svg)](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
- username == 'admin' && password == 'p4s5w0rd'
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.redis_url = "custom.redis.url:6380"
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 ```REDIS_URL``` then defaults to ```localhost:6379``` if not specified by configure block.
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 is provided by the [redis-namespace](https://github.com/defunkt/redis-namespace) library, which
654
- Split uses by default to separate the keys it manages from other keys
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
- Simply use the `Split.redis.namespace` accessor:
749
+ 1. Add `redis-namespace` to your Gemfile:
658
750
 
659
- ```ruby
660
- Split.redis.namespace = "split:blog"
661
- ```
751
+ ```ruby
752
+ gem 'redis-namespace'
753
+ ```
662
754
 
663
- We recommend sticking this in your initializer somewhere after Redis
664
- is configured.
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
@@ -3,6 +3,7 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  gem "appraisal"
6
+ gem "codeclimate-test-reporter"
6
7
  gem "rails", "~> 4.2"
7
8
 
8
9
  gemspec :path => "../"
@@ -0,0 +1,10 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "appraisal"
6
+ gem "codeclimate-test-reporter"
7
+ gem "rails", "~> 5.0"
8
+ gem "sinatra", :git => "https://github.com/sinatra/sinatra"
9
+
10
+ gemspec :path => "../"
@@ -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
@@ -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}"
@@ -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 :redis_url
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
- @redis_url = ENV.fetch('REDIS_URL', 'localhost:6379')
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
- BigDecimal.new(number.to_s).round(precision).to_f
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() == 'Hide active') {
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() == 'Hide completed') {
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>
@@ -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
- ret = split_context_shim.ab_test(*arguments)
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