split 3.4.1 → 4.0.4

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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/dependabot.yml +7 -0
  4. data/.github/workflows/ci.yml +76 -0
  5. data/.rubocop.yml +177 -4
  6. data/CHANGELOG.md +87 -0
  7. data/CONTRIBUTING.md +1 -1
  8. data/Gemfile +2 -1
  9. data/README.md +37 -9
  10. data/Rakefile +5 -5
  11. data/gemfiles/5.2.gemfile +1 -3
  12. data/gemfiles/6.0.gemfile +1 -3
  13. data/gemfiles/{5.0.gemfile → 6.1.gemfile} +2 -4
  14. data/gemfiles/{5.1.gemfile → 7.0.gemfile} +2 -4
  15. data/lib/split/algorithms/block_randomization.rb +6 -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 +22 -22
  20. data/lib/split/cache.rb +27 -0
  21. data/lib/split/combined_experiments_helper.rb +5 -4
  22. data/lib/split/configuration.rb +89 -94
  23. data/lib/split/dashboard/helpers.rb +7 -7
  24. data/lib/split/dashboard/pagination_helpers.rb +54 -54
  25. data/lib/split/dashboard/paginator.rb +1 -0
  26. data/lib/split/dashboard/public/dashboard.js +10 -0
  27. data/lib/split/dashboard/public/style.css +10 -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 +19 -4
  31. data/lib/split/dashboard.rb +42 -21
  32. data/lib/split/encapsulated_helper.rb +15 -8
  33. data/lib/split/engine.rb +1 -0
  34. data/lib/split/exceptions.rb +1 -0
  35. data/lib/split/experiment.rb +151 -124
  36. data/lib/split/experiment_catalog.rb +7 -8
  37. data/lib/split/extensions/string.rb +2 -1
  38. data/lib/split/goals_collection.rb +9 -10
  39. data/lib/split/helper.rb +50 -23
  40. data/lib/split/metric.rb +6 -6
  41. data/lib/split/persistence/cookie_adapter.rb +46 -44
  42. data/lib/split/persistence/dual_adapter.rb +7 -8
  43. data/lib/split/persistence/redis_adapter.rb +8 -4
  44. data/lib/split/persistence/session_adapter.rb +1 -2
  45. data/lib/split/persistence.rb +8 -6
  46. data/lib/split/redis_interface.rb +15 -29
  47. data/lib/split/trial.rb +43 -34
  48. data/lib/split/user.rb +25 -14
  49. data/lib/split/version.rb +2 -4
  50. data/lib/split/zscore.rb +2 -3
  51. data/lib/split.rb +34 -27
  52. data/spec/algorithms/block_randomization_spec.rb +6 -5
  53. data/spec/algorithms/weighted_sample_spec.rb +6 -5
  54. data/spec/algorithms/whiplash_spec.rb +4 -5
  55. data/spec/alternative_spec.rb +35 -36
  56. data/spec/cache_spec.rb +84 -0
  57. data/spec/combined_experiments_helper_spec.rb +18 -17
  58. data/spec/configuration_spec.rb +41 -45
  59. data/spec/dashboard/pagination_helpers_spec.rb +69 -67
  60. data/spec/dashboard/paginator_spec.rb +10 -9
  61. data/spec/dashboard_helpers_spec.rb +19 -18
  62. data/spec/dashboard_spec.rb +122 -38
  63. data/spec/encapsulated_helper_spec.rb +46 -22
  64. data/spec/experiment_catalog_spec.rb +14 -13
  65. data/spec/experiment_spec.rb +198 -118
  66. data/spec/goals_collection_spec.rb +18 -16
  67. data/spec/helper_spec.rb +454 -385
  68. data/spec/metric_spec.rb +14 -14
  69. data/spec/persistence/cookie_adapter_spec.rb +26 -11
  70. data/spec/persistence/dual_adapter_spec.rb +71 -71
  71. data/spec/persistence/redis_adapter_spec.rb +35 -27
  72. data/spec/persistence/session_adapter_spec.rb +2 -3
  73. data/spec/persistence_spec.rb +1 -2
  74. data/spec/redis_interface_spec.rb +25 -82
  75. data/spec/spec_helper.rb +35 -24
  76. data/spec/split_spec.rb +11 -11
  77. data/spec/support/cookies_mock.rb +1 -2
  78. data/spec/trial_spec.rb +102 -75
  79. data/spec/user_spec.rb +60 -29
  80. data/split.gemspec +22 -21
  81. metadata +43 -40
  82. data/.rubocop_todo.yml +0 -679
  83. data/.travis.yml +0 -60
  84. data/Appraisals +0 -19
  85. data/gemfiles/4.2.gemfile +0 -9
@@ -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,10 +1,12 @@
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
11
  <% paginated(@experiments).each do |experiment| %>
10
12
  <% if experiment.goals.empty? %>
@@ -24,3 +26,16 @@
24
26
  <p class="intro">No experiments have started yet, you need to define them in your code and introduce them to your users.</p>
25
27
  <p class="intro">Check out the <a href='https://github.com/splitrb/split#readme'>Readme</a> for more help getting started.</p>
26
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>
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
- require 'sinatra/base'
3
- require 'split'
4
- require 'bigdecimal'
5
- require 'split/dashboard/helpers'
6
- require 'split/dashboard/pagination_helpers'
2
+
3
+ require "sinatra/base"
4
+ require "split"
5
+ require "bigdecimal"
6
+ require "split/dashboard/helpers"
7
+ require "split/dashboard/pagination_helpers"
7
8
 
8
9
  module Split
9
10
  class Dashboard < Sinatra::Base
@@ -17,14 +18,15 @@ module Split
17
18
  helpers Split::DashboardHelpers
18
19
  helpers Split::DashboardPaginationHelpers
19
20
 
20
- get '/' do
21
+ get "/" do
21
22
  # Display experiments without a winner at the top of the dashboard
22
23
  @experiments = Split::ExperimentCatalog.all_active_first
24
+ @unintialized_experiments = Split.configuration.experiments.keys - @experiments.map(&:name)
23
25
 
24
26
  @metrics = Split::Metric.all
25
27
 
26
28
  # Display Rails Environment mode (or Rack version if not using Rails)
27
- if Object.const_defined?('Rails')
29
+ if Object.const_defined?("Rails") && Rails.respond_to?(:env)
28
30
  @current_env = Rails.env.titlecase
29
31
  else
30
32
  @current_env = "Rack: #{Rack.version}"
@@ -32,43 +34,62 @@ module Split
32
34
  erb :index
33
35
  end
34
36
 
35
- post '/force_alternative' do
37
+ post "/initialize_experiment" do
38
+ Split::ExperimentCatalog.find_or_create(params[:experiment]) unless params[:experiment].nil? || params[:experiment].empty?
39
+ redirect url("/")
40
+ end
41
+
42
+ post "/force_alternative" do
36
43
  experiment = Split::ExperimentCatalog.find(params[:experiment])
37
44
  alternative = Split::Alternative.new(params[:alternative], experiment.name)
38
- alternative.increment_participation
39
- Split::User.new(self)[experiment.key] = alternative.name
40
- redirect url('/')
45
+
46
+ cookies = JSON.parse(request.cookies["split_override"]) rescue {}
47
+ cookies[experiment.name] = alternative.name
48
+ response.set_cookie("split_override", { value: cookies.to_json, path: "/" })
49
+
50
+ redirect url("/")
41
51
  end
42
52
 
43
- post '/experiment' do
53
+ post "/experiment" do
44
54
  @experiment = Split::ExperimentCatalog.find(params[:experiment])
45
55
  @alternative = Split::Alternative.new(params[:alternative], params[:experiment])
46
56
  @experiment.winner = @alternative.name
47
- redirect url('/')
57
+ redirect url("/")
48
58
  end
49
59
 
50
- post '/start' do
60
+ post "/start" do
51
61
  @experiment = Split::ExperimentCatalog.find(params[:experiment])
52
62
  @experiment.start
53
- redirect url('/')
63
+ redirect url("/")
54
64
  end
55
65
 
56
- post '/reset' do
66
+ post "/reset" do
57
67
  @experiment = Split::ExperimentCatalog.find(params[:experiment])
58
68
  @experiment.reset
59
- redirect url('/')
69
+ redirect url("/")
60
70
  end
61
71
 
62
- post '/reopen' do
72
+ post "/reopen" do
63
73
  @experiment = Split::ExperimentCatalog.find(params[:experiment])
64
74
  @experiment.reset_winner
65
- redirect url('/')
75
+ redirect url("/")
76
+ end
77
+
78
+ post "/update_cohorting" do
79
+ @experiment = Split::ExperimentCatalog.find(params[:experiment])
80
+ case params[:cohorting_action].downcase
81
+ when "enable"
82
+ @experiment.enable_cohorting
83
+ when "disable"
84
+ @experiment.disable_cohorting
85
+ end
86
+ redirect url("/")
66
87
  end
67
88
 
68
- delete '/experiment' do
89
+ delete "/experiment" do
69
90
  @experiment = Split::ExperimentCatalog.find(params[:experiment])
70
91
  @experiment.delete
71
- redirect url('/')
92
+ redirect url("/")
72
93
  end
73
94
  end
74
95
  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,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Split
3
4
  class Engine < ::Rails::Engine
4
5
  initializer "split" do |app|
@@ -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