split 3.2.0 → 4.0.5

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 (87) hide show
  1. checksums.yaml +5 -5
  2. data/.eslintrc +1 -1
  3. data/.github/FUNDING.yml +1 -0
  4. data/.github/ISSUE_TEMPLATE/bug_report.md +24 -0
  5. data/.github/dependabot.yml +7 -0
  6. data/.github/workflows/ci.yml +63 -0
  7. data/.rspec +1 -0
  8. data/.rubocop.yml +67 -1043
  9. data/CHANGELOG.md +174 -0
  10. data/CODE_OF_CONDUCT.md +3 -3
  11. data/CONTRIBUTING.md +1 -1
  12. data/Gemfile +6 -1
  13. data/README.md +79 -33
  14. data/Rakefile +6 -5
  15. data/lib/split/algorithms/block_randomization.rb +7 -6
  16. data/lib/split/algorithms/weighted_sample.rb +2 -1
  17. data/lib/split/algorithms/whiplash.rb +17 -18
  18. data/lib/split/algorithms.rb +14 -0
  19. data/lib/split/alternative.rb +25 -25
  20. data/lib/split/cache.rb +27 -0
  21. data/lib/split/combined_experiments_helper.rb +6 -5
  22. data/lib/split/configuration.rb +94 -91
  23. data/lib/split/dashboard/helpers.rb +9 -9
  24. data/lib/split/dashboard/pagination_helpers.rb +86 -0
  25. data/lib/split/dashboard/paginator.rb +17 -0
  26. data/lib/split/dashboard/public/dashboard.js +10 -0
  27. data/lib/split/dashboard/public/style.css +19 -2
  28. data/lib/split/dashboard/views/_controls.erb +13 -0
  29. data/lib/split/dashboard/views/_experiment.erb +2 -1
  30. data/lib/split/dashboard/views/index.erb +24 -5
  31. data/lib/split/dashboard/views/layout.erb +1 -1
  32. data/lib/split/dashboard.rb +47 -20
  33. data/lib/split/encapsulated_helper.rb +15 -8
  34. data/lib/split/engine.rb +7 -4
  35. data/lib/split/exceptions.rb +1 -0
  36. data/lib/split/experiment.rb +160 -122
  37. data/lib/split/experiment_catalog.rb +7 -8
  38. data/lib/split/extensions/string.rb +2 -1
  39. data/lib/split/goals_collection.rb +10 -10
  40. data/lib/split/helper.rb +56 -24
  41. data/lib/split/metric.rb +6 -6
  42. data/lib/split/persistence/cookie_adapter.rb +52 -15
  43. data/lib/split/persistence/dual_adapter.rb +53 -12
  44. data/lib/split/persistence/redis_adapter.rb +8 -4
  45. data/lib/split/persistence/session_adapter.rb +1 -2
  46. data/lib/split/persistence.rb +8 -6
  47. data/lib/split/redis_interface.rb +16 -31
  48. data/lib/split/trial.rb +48 -41
  49. data/lib/split/user.rb +30 -15
  50. data/lib/split/version.rb +2 -4
  51. data/lib/split/zscore.rb +2 -3
  52. data/lib/split.rb +39 -25
  53. data/spec/algorithms/block_randomization_spec.rb +6 -5
  54. data/spec/algorithms/weighted_sample_spec.rb +6 -5
  55. data/spec/algorithms/whiplash_spec.rb +4 -5
  56. data/spec/alternative_spec.rb +35 -36
  57. data/spec/cache_spec.rb +84 -0
  58. data/spec/combined_experiments_helper_spec.rb +18 -17
  59. data/spec/configuration_spec.rb +41 -45
  60. data/spec/dashboard/pagination_helpers_spec.rb +202 -0
  61. data/spec/dashboard/paginator_spec.rb +38 -0
  62. data/spec/dashboard_helpers_spec.rb +19 -18
  63. data/spec/dashboard_spec.rb +153 -48
  64. data/spec/encapsulated_helper_spec.rb +47 -23
  65. data/spec/experiment_catalog_spec.rb +14 -13
  66. data/spec/experiment_spec.rb +224 -111
  67. data/spec/goals_collection_spec.rb +18 -16
  68. data/spec/helper_spec.rb +539 -419
  69. data/spec/metric_spec.rb +14 -14
  70. data/spec/persistence/cookie_adapter_spec.rb +105 -27
  71. data/spec/persistence/dual_adapter_spec.rb +158 -66
  72. data/spec/persistence/redis_adapter_spec.rb +35 -27
  73. data/spec/persistence/session_adapter_spec.rb +2 -3
  74. data/spec/persistence_spec.rb +1 -2
  75. data/spec/redis_interface_spec.rb +25 -82
  76. data/spec/spec_helper.rb +38 -24
  77. data/spec/split_spec.rb +18 -18
  78. data/spec/support/cookies_mock.rb +1 -2
  79. data/spec/trial_spec.rb +117 -70
  80. data/spec/user_spec.rb +69 -27
  81. data/split.gemspec +26 -22
  82. metadata +85 -37
  83. data/.travis.yml +0 -41
  84. data/Appraisals +0 -13
  85. data/gemfiles/4.2.gemfile +0 -9
  86. data/gemfiles/5.0.gemfile +0 -10
  87. data/gemfiles/5.1.gemfile +0 -10
@@ -2,7 +2,20 @@
2
2
  <form action="<%= url "/reopen?experiment=#{experiment.name}" %>" method='post' onclick="return confirmReopen()">
3
3
  <input type="submit" value="Reopen Experiment">
4
4
  </form>
5
+ <% else %>
6
+ <% if experiment.cohorting_disabled? %>
7
+ <form action="<%= url "/update_cohorting?experiment=#{experiment.name}" %>" method='post' onclick="return confirmEnableCohorting()">
8
+ <input type="hidden" name="cohorting_action" value="enable">
9
+ <input type="submit" value="Enable Cohorting" class="green">
10
+ </form>
11
+ <% else %>
12
+ <form action="<%= url "/update_cohorting?experiment=#{experiment.name}" %>" method='post' onclick="return confirmDisableCohorting()">
13
+ <input type="hidden" name="cohorting_action" value="disable">
14
+ <input type="submit" value="Disable Cohorting" class="red">
15
+ </form>
16
+ <% end %>
5
17
  <% end %>
18
+ <span class="divider">|</span>
6
19
  <% if experiment.start_time %>
7
20
  <form action="<%= url "/reset?experiment=#{experiment.name}" %>" method='post' onclick="return confirmReset()">
8
21
  <input type="submit" value="Reset Data">
@@ -16,7 +16,8 @@
16
16
  summary_texts = {}
17
17
  extra_columns.each do |column|
18
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)
19
+
20
+ if extra_infos.length > 0 && extra_infos.all? { |extra_info| extra_info[column].kind_of?(Numeric) }
20
21
  summary_texts[column] = extra_infos.inject(0){|sum, extra_info| sum += extra_info[column]}
21
22
  else
22
23
  summary_texts[column] = "N/A"
@@ -1,12 +1,14 @@
1
1
  <% if @experiments.any? %>
2
2
  <p class="intro">The list below contains all the registered experiments along with the number of test participants, completed and conversion rate currently in the system.</p>
3
3
 
4
- <input type="text" placeholder="Begin typing to filter" id="filter" />
5
- <input type="button" id="toggle-completed" value="Hide completed" />
6
- <input type="button" id="toggle-active" value="Hide active" />
7
- <input type="button" id="clear-filter" value="Clear filters" />
4
+ <div class="dashboard-controls">
5
+ <input type="text" placeholder="Begin typing to filter" id="filter" />
6
+ <input type="button" id="toggle-completed" value="Hide completed" />
7
+ <input type="button" id="toggle-active" value="Hide active" />
8
+ <input type="button" id="clear-filter" value="Clear filters" />
9
+ </div>
8
10
 
9
- <% @experiments.each do |experiment| %>
11
+ <% paginated(@experiments).each do |experiment| %>
10
12
  <% if experiment.goals.empty? %>
11
13
  <%= erb :_experiment, :locals => {:goal => nil, :experiment => experiment} %>
12
14
  <% else %>
@@ -16,7 +18,24 @@
16
18
  <% end %>
17
19
  <% end %>
18
20
  <% end %>
21
+
22
+ <div class="pagination">
23
+ <%= pagination(@experiments) %>
24
+ </div>
19
25
  <% else %>
20
26
  <p class="intro">No experiments have started yet, you need to define them in your code and introduce them to your users.</p>
21
27
  <p class="intro">Check out the <a href='https://github.com/splitrb/split#readme'>Readme</a> for more help getting started.</p>
22
28
  <% end %>
29
+
30
+ <div class="dashboard-controls dashboard-controls-bottom">
31
+ <form action="<%= url "/initialize_experiment" %>" method='post'>
32
+ <label>Add unregistered experiment: </label>
33
+ <select name="experiment" id="experiment-select">
34
+ <option selected disabled>experiment</option>
35
+ <% @unintialized_experiments.sort.each do |experiment_name| %>
36
+ <option value="<%= experiment_name %>"><%= experiment_name %></option>
37
+ <% end %>
38
+ </select>
39
+ <input type="submit" id="register-experiment-btn" value="register experiment"/>
40
+ </form>
41
+ <div>
@@ -21,7 +21,7 @@
21
21
  </div>
22
22
 
23
23
  <div id="footer">
24
- <p>Powered by <a href="http://github.com/splitrb/split">Split</a> v<%=Split::VERSION %></p>
24
+ <p>Powered by <a href="https://github.com/splitrb/split">Split</a> v<%=Split::VERSION %></p>
25
25
  </div>
26
26
  </body>
27
27
  </html>
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
- require 'sinatra/base'
3
- require 'split'
4
- require 'bigdecimal'
5
- require 'split/dashboard/helpers'
2
+
3
+ require "sinatra/base"
4
+ require "split"
5
+ require "bigdecimal"
6
+ require "split/dashboard/helpers"
7
+ require "split/dashboard/pagination_helpers"
6
8
 
7
9
  module Split
8
10
  class Dashboard < Sinatra::Base
@@ -14,56 +16,81 @@ module Split
14
16
  set :method_override, true
15
17
 
16
18
  helpers Split::DashboardHelpers
19
+ helpers Split::DashboardPaginationHelpers
17
20
 
18
- get '/' do
21
+ get "/" do
19
22
  # Display experiments without a winner at the top of the dashboard
20
23
  @experiments = Split::ExperimentCatalog.all_active_first
24
+ @unintialized_experiments = Split.configuration.experiments.keys - @experiments.map(&:name)
21
25
 
22
26
  @metrics = Split::Metric.all
23
27
 
24
28
  # Display Rails Environment mode (or Rack version if not using Rails)
25
- if Object.const_defined?('Rails')
29
+ if Object.const_defined?("Rails") && Rails.respond_to?(:env)
26
30
  @current_env = Rails.env.titlecase
27
31
  else
28
- @current_env = "Rack: #{Rack.version}"
32
+ rack_version = Rack.respond_to?(:version) ? Rack.version : Rack.release
33
+ @current_env = "Rack: #{rack_version}"
29
34
  end
30
35
  erb :index
31
36
  end
32
37
 
33
- post '/force_alternative' do
34
- Split::User.new(self)[params[:experiment]] = params[:alternative]
35
- redirect url('/')
38
+ post "/initialize_experiment" do
39
+ Split::ExperimentCatalog.find_or_create(params[:experiment]) unless params[:experiment].nil? || params[:experiment].empty?
40
+ redirect url("/")
41
+ end
42
+
43
+ post "/force_alternative" do
44
+ experiment = Split::ExperimentCatalog.find(params[:experiment])
45
+ alternative = Split::Alternative.new(params[:alternative], experiment.name)
46
+
47
+ cookies = JSON.parse(request.cookies["split_override"]) rescue {}
48
+ cookies[experiment.name] = alternative.name
49
+ response.set_cookie("split_override", { value: cookies.to_json, path: "/" })
50
+
51
+ redirect url("/")
36
52
  end
37
53
 
38
- post '/experiment' do
54
+ post "/experiment" do
39
55
  @experiment = Split::ExperimentCatalog.find(params[:experiment])
40
56
  @alternative = Split::Alternative.new(params[:alternative], params[:experiment])
41
57
  @experiment.winner = @alternative.name
42
- redirect url('/')
58
+ redirect url("/")
43
59
  end
44
60
 
45
- post '/start' do
61
+ post "/start" do
46
62
  @experiment = Split::ExperimentCatalog.find(params[:experiment])
47
63
  @experiment.start
48
- redirect url('/')
64
+ redirect url("/")
49
65
  end
50
66
 
51
- post '/reset' do
67
+ post "/reset" do
52
68
  @experiment = Split::ExperimentCatalog.find(params[:experiment])
53
69
  @experiment.reset
54
- redirect url('/')
70
+ redirect url("/")
55
71
  end
56
72
 
57
- post '/reopen' do
73
+ post "/reopen" do
58
74
  @experiment = Split::ExperimentCatalog.find(params[:experiment])
59
75
  @experiment.reset_winner
60
- redirect url('/')
76
+ redirect url("/")
77
+ end
78
+
79
+ post "/update_cohorting" do
80
+ @experiment = Split::ExperimentCatalog.find(params[:experiment])
81
+ case params[:cohorting_action].downcase
82
+ when "enable"
83
+ @experiment.enable_cohorting
84
+ when "disable"
85
+ @experiment.disable_cohorting
86
+ end
87
+ redirect url("/")
61
88
  end
62
89
 
63
- delete '/experiment' do
90
+ delete "/experiment" do
64
91
  @experiment = Split::ExperimentCatalog.find(params[:experiment])
65
92
  @experiment.delete
66
- redirect url('/')
93
+ redirect url("/")
67
94
  end
68
95
  end
69
96
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "split/helper"
3
4
 
4
5
  # Split's helper exposes all kinds of methods we don't want to
@@ -14,7 +15,6 @@ require "split/helper"
14
15
  #
15
16
  module Split
16
17
  module EncapsulatedHelper
17
-
18
18
  class ContextShim
19
19
  include Split::Helper
20
20
  public :ab_test, :ab_finished
@@ -23,20 +23,27 @@ module Split
23
23
  @context = context
24
24
  end
25
25
 
26
+ def params
27
+ request.params if request && request.respond_to?(:params)
28
+ end
29
+
30
+ def request
31
+ @context.request if @context.respond_to?(:request)
32
+ end
33
+
26
34
  def ab_user
27
35
  @ab_user ||= Split::User.new(@context)
28
36
  end
29
37
  end
30
38
 
31
- def ab_test(*arguments,&block)
32
- split_context_shim.ab_test(*arguments,&block)
39
+ def ab_test(*arguments, &block)
40
+ split_context_shim.ab_test(*arguments, &block)
33
41
  end
34
42
 
35
43
  private
36
-
37
- # instantiate and memoize a context shim in case of multiple ab_test* calls
38
- def split_context_shim
39
- @split_context_shim ||= ContextShim.new(self)
40
- end
44
+ # instantiate and memoize a context shim in case of multiple ab_test* calls
45
+ def split_context_shim
46
+ @split_context_shim ||= ContextShim.new(self)
47
+ end
41
48
  end
42
49
  end
data/lib/split/engine.rb CHANGED
@@ -1,12 +1,15 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Split
3
4
  class Engine < ::Rails::Engine
4
5
  initializer "split" do |app|
5
6
  if Split.configuration.include_rails_helper
6
- ActionController::Base.send :include, Split::Helper
7
- ActionController::Base.helper Split::Helper
8
- ActionController::Base.send :include, Split::CombinedExperimentsHelper
9
- ActionController::Base.helper Split::CombinedExperimentsHelper
7
+ ActiveSupport.on_load(:action_controller) do
8
+ ::ActionController::Base.send :include, Split::Helper
9
+ ::ActionController::Base.helper Split::Helper
10
+ ::ActionController::Base.send :include, Split::CombinedExperimentsHelper
11
+ ::ActionController::Base.helper Split::CombinedExperimentsHelper
12
+ end
10
13
  end
11
14
  end
12
15
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Split
3
4
  class InvalidPersistenceAdapterError < StandardError; end
4
5
  class ExperimentNotFound < StandardError; end