split 3.0.0 → 4.0.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.
Files changed (82) 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 +71 -0
  7. data/.rspec +1 -0
  8. data/.rubocop.yml +71 -1044
  9. data/.rubocop_todo.yml +226 -0
  10. data/Appraisals +12 -1
  11. data/CHANGELOG.md +157 -0
  12. data/CODE_OF_CONDUCT.md +3 -3
  13. data/CONTRIBUTING.md +54 -5
  14. data/Gemfile +2 -0
  15. data/LICENSE +1 -1
  16. data/README.md +232 -121
  17. data/Rakefile +2 -0
  18. data/gemfiles/5.0.gemfile +1 -2
  19. data/gemfiles/{4.2.gemfile → 5.1.gemfile} +2 -2
  20. data/gemfiles/5.2.gemfile +9 -0
  21. data/gemfiles/6.0.gemfile +9 -0
  22. data/gemfiles/6.1.gemfile +9 -0
  23. data/gemfiles/7.0.gemfile +9 -0
  24. data/lib/split/algorithms/block_randomization.rb +2 -0
  25. data/lib/split/algorithms/weighted_sample.rb +2 -1
  26. data/lib/split/algorithms/whiplash.rb +3 -2
  27. data/lib/split/alternative.rb +7 -3
  28. data/lib/split/cache.rb +28 -0
  29. data/lib/split/combined_experiments_helper.rb +38 -0
  30. data/lib/split/configuration.rb +24 -13
  31. data/lib/split/dashboard/helpers.rb +3 -2
  32. data/lib/split/dashboard/pagination_helpers.rb +87 -0
  33. data/lib/split/dashboard/paginator.rb +17 -0
  34. data/lib/split/dashboard/public/dashboard.js +10 -0
  35. data/lib/split/dashboard/public/style.css +14 -0
  36. data/lib/split/dashboard/views/_controls.erb +13 -0
  37. data/lib/split/dashboard/views/index.erb +5 -1
  38. data/lib/split/dashboard/views/layout.erb +1 -1
  39. data/lib/split/dashboard.rb +21 -1
  40. data/lib/split/encapsulated_helper.rb +3 -2
  41. data/lib/split/engine.rb +7 -2
  42. data/lib/split/exceptions.rb +1 -0
  43. data/lib/split/experiment.rb +103 -69
  44. data/lib/split/experiment_catalog.rb +1 -3
  45. data/lib/split/extensions/string.rb +1 -0
  46. data/lib/split/goals_collection.rb +2 -0
  47. data/lib/split/helper.rb +42 -9
  48. data/lib/split/metric.rb +2 -1
  49. data/lib/split/persistence/cookie_adapter.rb +58 -15
  50. data/lib/split/persistence/dual_adapter.rb +54 -12
  51. data/lib/split/persistence/redis_adapter.rb +5 -0
  52. data/lib/split/persistence/session_adapter.rb +1 -0
  53. data/lib/split/persistence.rb +4 -2
  54. data/lib/split/redis_interface.rb +9 -30
  55. data/lib/split/trial.rb +25 -17
  56. data/lib/split/user.rb +20 -4
  57. data/lib/split/version.rb +2 -4
  58. data/lib/split/zscore.rb +1 -0
  59. data/lib/split.rb +17 -3
  60. data/spec/alternative_spec.rb +13 -1
  61. data/spec/cache_spec.rb +88 -0
  62. data/spec/combined_experiments_helper_spec.rb +57 -0
  63. data/spec/configuration_spec.rb +17 -15
  64. data/spec/dashboard/pagination_helpers_spec.rb +200 -0
  65. data/spec/dashboard/paginator_spec.rb +37 -0
  66. data/spec/dashboard_helpers_spec.rb +2 -2
  67. data/spec/dashboard_spec.rb +78 -17
  68. data/spec/encapsulated_helper_spec.rb +2 -2
  69. data/spec/experiment_spec.rb +117 -13
  70. data/spec/goals_collection_spec.rb +1 -1
  71. data/spec/helper_spec.rb +211 -112
  72. data/spec/persistence/cookie_adapter_spec.rb +90 -23
  73. data/spec/persistence/dual_adapter_spec.rb +160 -68
  74. data/spec/persistence/redis_adapter_spec.rb +9 -0
  75. data/spec/redis_interface_spec.rb +0 -69
  76. data/spec/spec_helper.rb +5 -6
  77. data/spec/split_spec.rb +7 -7
  78. data/spec/trial_spec.rb +65 -19
  79. data/spec/user_spec.rb +45 -3
  80. data/split.gemspec +20 -10
  81. metadata +61 -35
  82. data/.travis.yml +0 -16
@@ -0,0 +1,9 @@
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", "~> 6.1"
8
+
9
+ gemspec path: "../"
@@ -0,0 +1,9 @@
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", "~> 7.0"
8
+
9
+ gemspec path: "../"
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Selects alternative with minimum count of participants
2
4
  # If all counts are even (i.e. all are minimum), samples from all possible alternatives
3
5
 
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Split
3
4
  module Algorithms
4
5
  module WeightedSample
@@ -8,7 +9,7 @@ module Split
8
9
  total = weights.inject(:+)
9
10
  point = rand * total
10
11
 
11
- experiment.alternatives.zip(weights).each do |n,w|
12
+ experiment.alternatives.zip(weights).each do |n, w|
12
13
  return n if w >= point
13
14
  point -= w
14
15
  end
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  # A multi-armed bandit implementation inspired by
3
4
  # @aaronsw and victorykit/whiplash
4
- require 'simple-random'
5
+ require 'rubystats'
5
6
 
6
7
  module Split
7
8
  module Algorithms
@@ -16,7 +17,7 @@ module Split
16
17
  def arm_guess(participants, completions)
17
18
  a = [participants, 0].max
18
19
  b = [participants-completions, 0].max
19
- s = SimpleRandom.new; s.set_seed; s.beta(a+fairness_constant, b+fairness_constant)
20
+ Rubystats::BetaDistribution.new(a+fairness_constant, b+fairness_constant).rng
20
21
  end
21
22
 
22
23
  def best_guess(alternatives)
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Split
3
4
  class Alternative
4
5
  attr_accessor :name
@@ -15,7 +16,7 @@ module Split
15
16
  @name = name
16
17
  @weight = 1
17
18
  end
18
- p_winner = 0.0
19
+ @p_winner = 0.0
19
20
  end
20
21
 
21
22
  def to_s
@@ -75,7 +76,7 @@ module Split
75
76
  return field
76
77
  end
77
78
 
78
- def set_completed_count (count, goal = nil)
79
+ def set_completed_count(count, goal = nil)
79
80
  field = set_field(goal)
80
81
  Split.redis.hset(key, field, count.to_i)
81
82
  end
@@ -119,7 +120,10 @@ module Split
119
120
  n_a = alternative.participant_count
120
121
  n_c = control.participant_count
121
122
 
122
- z_score = Split::Zscore.calculate(p_a, n_a, p_c, n_c)
123
+ # can't calculate zscore for P(x) > 1
124
+ return 'N/A' if p_a > 1 || p_c > 1
125
+
126
+ Split::Zscore.calculate(p_a, n_a, p_c, n_c)
123
127
  end
124
128
 
125
129
  def extra_info
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Split
4
+ class Cache
5
+
6
+ def self.clear
7
+ @cache = nil
8
+ end
9
+
10
+ def self.fetch(namespace, key)
11
+ return yield unless Split.configuration.cache
12
+
13
+ @cache ||= {}
14
+ @cache[namespace] ||= {}
15
+
16
+ value = @cache[namespace][key]
17
+ return value if value
18
+
19
+ @cache[namespace][key] = yield
20
+ end
21
+
22
+ def self.clear_key(key)
23
+ @cache&.keys&.each do |namespace|
24
+ @cache[namespace]&.delete(key)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Split
4
+ module CombinedExperimentsHelper
5
+ def ab_combined_test(metric_descriptor, control = nil, *alternatives)
6
+ return nil unless experiment = find_combined_experiment(metric_descriptor)
7
+ raise(Split::InvalidExperimentsFormatError, "Unable to find experiment #{metric_descriptor} in configuration") if experiment[:combined_experiments].nil?
8
+
9
+ alternative = nil
10
+ weighted_alternatives = nil
11
+ experiment[:combined_experiments].each do |combined_experiment|
12
+ if alternative.nil?
13
+ if control
14
+ alternative = ab_test(combined_experiment, control, alternatives)
15
+ else
16
+ normalized_alternatives = Split::Configuration.new.normalize_alternatives(experiment[:alternatives])
17
+ alternative = ab_test(combined_experiment, normalized_alternatives[0], *normalized_alternatives[1])
18
+ end
19
+ else
20
+ weighted_alternatives ||= experiment[:alternatives].each_with_object({}) do |alt, memo|
21
+ alt = Alternative.new(alt, experiment[:name]).name
22
+ memo[alt] = (alt == alternative ? 1 : 0)
23
+ end
24
+
25
+ ab_test(combined_experiment, [weighted_alternatives])
26
+ end
27
+ end
28
+ alternative
29
+ end
30
+
31
+ def find_combined_experiment(metric_descriptor)
32
+ raise(Split::InvalidExperimentsFormatError, 'Invalid descriptor class (String or Symbol required)') unless metric_descriptor.class == String || metric_descriptor.class == Symbol
33
+ raise(Split::InvalidExperimentsFormatError, 'Enable configuration') unless Split.configuration.enabled
34
+ raise(Split::InvalidExperimentsFormatError, 'Enable `allow_multiple_experiments`') unless Split.configuration.allow_multiple_experiments
35
+ Split::configuration.experiments[metric_descriptor.to_sym]
36
+ end
37
+ end
38
+ end
@@ -1,8 +1,7 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Split
3
4
  class Configuration
4
- attr_accessor :bots
5
- attr_accessor :robot_regex
6
5
  attr_accessor :ignore_ip_addresses
7
6
  attr_accessor :ignore_filter
8
7
  attr_accessor :db_failover
@@ -12,6 +11,7 @@ module Split
12
11
  attr_accessor :enabled
13
12
  attr_accessor :persistence
14
13
  attr_accessor :persistence_cookie_length
14
+ attr_accessor :persistence_cookie_domain
15
15
  attr_accessor :algorithm
16
16
  attr_accessor :store_override
17
17
  attr_accessor :start_manually
@@ -22,13 +22,20 @@ module Split
22
22
  attr_accessor :on_experiment_reset
23
23
  attr_accessor :on_experiment_delete
24
24
  attr_accessor :on_before_experiment_reset
25
+ attr_accessor :on_experiment_winner_choose
25
26
  attr_accessor :on_before_experiment_delete
26
27
  attr_accessor :include_rails_helper
27
28
  attr_accessor :beta_probability_simulations
29
+ attr_accessor :winning_alternative_recalculation_interval
28
30
  attr_accessor :redis
31
+ attr_accessor :dashboard_pagination_default_per_page
32
+ attr_accessor :cache
29
33
 
30
34
  attr_reader :experiments
31
35
 
36
+ attr_writer :bots
37
+ attr_writer :robot_regex
38
+
32
39
  def bots
33
40
  @bots ||= {
34
41
  # Indexers
@@ -48,7 +55,9 @@ module Split
48
55
  'spider' => 'generic web spider',
49
56
  'UnwindFetchor' => 'Gnip crawler',
50
57
  'WordPress' => 'WordPress spider',
58
+ 'YandexAccessibilityBot' => 'Yandex accessibility spider',
51
59
  'YandexBot' => 'Yandex spider',
60
+ 'YandexMobileBot' => 'Yandex mobile spider',
52
61
  'ZIBB' => 'ZIBB spider',
53
62
 
54
63
  # HTTP libraries
@@ -58,12 +67,14 @@ module Split
58
67
  'ColdFusion' => 'ColdFusion http library',
59
68
  'EventMachine HttpClient' => 'Ruby http library',
60
69
  'Go http package' => 'Go http library',
70
+ 'Go-http-client' => 'Go http library',
61
71
  'Java' => 'Generic Java http library',
62
72
  'libwww-perl' => 'Perl client-server library loved by script kids',
63
73
  'lwp-trivial' => 'Another Perl library loved by script kids',
64
74
  'Python-urllib' => 'Python http library',
65
75
  'PycURL' => 'Python http library',
66
76
  'Test Certificate Info' => 'C http library?',
77
+ 'Typhoeus' => 'Ruby http library',
67
78
  'Wget' => 'wget unix CLI http client',
68
79
 
69
80
  # URL expanders / previewers
@@ -71,12 +82,16 @@ module Split
71
82
  'bitlybot' => 'bit.ly bot',
72
83
  'bot@linkfluence.net' => 'Linkfluence bot',
73
84
  'facebookexternalhit' => 'facebook bot',
85
+ 'Facebot' => 'Facebook crawler',
74
86
  'Feedfetcher-Google' => 'Google Feedfetcher',
75
87
  'https://developers.google.com/+/web/snippet' => 'Google+ Snippet Fetcher',
88
+ 'LinkedInBot' => 'LinkedIn bot',
76
89
  'LongURL' => 'URL expander service',
77
90
  'NING' => 'NING - Yet Another Twitter Swarmer',
91
+ 'Pinterestbot' => 'Pinterest Bot',
78
92
  'redditbot' => 'Reddit Bot',
79
93
  'ShortLinkTranslate' => 'Link shortener',
94
+ 'Slackbot' => 'Slackbot link expander',
80
95
  'TweetmemeBot' => 'TweetMeMe Crawler',
81
96
  'Twitterbot' => 'Twitter URL expander',
82
97
  'UnwindFetch' => 'Gnip URL expander',
@@ -84,10 +99,12 @@ module Split
84
99
 
85
100
  # Uptime monitoring
86
101
  'check_http' => 'Nagios monitor',
102
+ 'GoogleStackdriverMonitoring' => 'Google Cloud monitor',
87
103
  'NewRelicPinger' => 'NewRelic monitor',
88
104
  'Panopta' => 'Monitoring service',
89
105
  'Pingdom' => 'Pingdom monitoring',
90
106
  'SiteUptime' => 'Site monitoring services',
107
+ 'UptimeRobot' => 'Monitoring service',
91
108
 
92
109
  # ???
93
110
  'DigitalPersona Fingerprint Software' => 'HP Fingerprint scanner',
@@ -160,7 +177,7 @@ module Split
160
177
  end
161
178
 
162
179
  def normalize_alternatives(alternatives)
163
- given_probability, num_with_probability = alternatives.inject([0,0]) do |a,v|
180
+ given_probability, num_with_probability = alternatives.inject([0, 0]) do |a, v|
164
181
  p, n = a
165
182
  if percent = value_for(v, :percent)
166
183
  [p + percent, n + 1]
@@ -203,26 +220,20 @@ module Split
203
220
  @on_experiment_delete = proc{|experiment|}
204
221
  @on_before_experiment_reset = proc{|experiment|}
205
222
  @on_before_experiment_delete = proc{|experiment|}
223
+ @on_experiment_winner_choose = proc{|experiment|}
206
224
  @db_failover_allow_parameter_override = false
207
225
  @allow_multiple_experiments = false
208
226
  @enabled = true
209
227
  @experiments = {}
210
228
  @persistence = Split::Persistence::SessionAdapter
211
229
  @persistence_cookie_length = 31536000 # One year from now
230
+ @persistence_cookie_domain = nil
212
231
  @algorithm = Split::Algorithms::WeightedSample
213
232
  @include_rails_helper = true
214
233
  @beta_probability_simulations = 10000
234
+ @winning_alternative_recalculation_interval = 60 * 60 * 24 # 1 day
215
235
  @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
236
+ @dashboard_pagination_default_per_page = 10
226
237
  end
227
238
 
228
239
  private
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Split
3
4
  module DashboardHelpers
4
5
  def h(text)
@@ -19,9 +20,9 @@ module Split
19
20
 
20
21
  def round(number, precision = 2)
21
22
  begin
22
- BigDecimal.new(number.to_s)
23
+ BigDecimal(number.to_s)
23
24
  rescue ArgumentError
24
- BigDecimal.new(0)
25
+ BigDecimal(0)
25
26
  end.round(precision).to_f
26
27
  end
27
28
 
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'split/dashboard/paginator'
4
+
5
+ module Split
6
+ module DashboardPaginationHelpers
7
+ def pagination_per
8
+ default_per_page = Split.configuration.dashboard_pagination_default_per_page
9
+ @pagination_per ||= (params[:per] || default_per_page).to_i
10
+ end
11
+
12
+ def page_number
13
+ @page_number ||= (params[:page] || 1).to_i
14
+ end
15
+
16
+ def paginated(collection)
17
+ Split::DashboardPaginator.new(collection, page_number, pagination_per).paginate
18
+ end
19
+
20
+ def pagination(collection)
21
+ html = []
22
+ html << first_page_tag if show_first_page_tag?
23
+ html << ellipsis_tag if show_first_ellipsis_tag?
24
+ html << prev_page_tag if show_prev_page_tag?
25
+ html << current_page_tag
26
+ html << next_page_tag if show_next_page_tag?(collection)
27
+ html << ellipsis_tag if show_last_ellipsis_tag?(collection)
28
+ html << last_page_tag(collection) if show_last_page_tag?(collection)
29
+ html.join
30
+ end
31
+
32
+ private
33
+
34
+ def show_first_page_tag?
35
+ page_number > 2
36
+ end
37
+
38
+ def first_page_tag
39
+ %Q(<a href="#{url.chop}?page=1&per=#{pagination_per}">1</a>)
40
+ end
41
+
42
+ def show_first_ellipsis_tag?
43
+ page_number >= 4
44
+ end
45
+
46
+ def ellipsis_tag
47
+ '<span>...</span>'
48
+ end
49
+
50
+ def show_prev_page_tag?
51
+ page_number > 1
52
+ end
53
+
54
+ def prev_page_tag
55
+ %Q(<a href="#{url.chop}?page=#{page_number - 1}&per=#{pagination_per}">#{page_number - 1}</a>)
56
+ end
57
+
58
+ def current_page_tag
59
+ "<span><b>#{page_number}</b></span>"
60
+ end
61
+
62
+ def show_next_page_tag?(collection)
63
+ (page_number * pagination_per) < collection.count
64
+ end
65
+
66
+ def next_page_tag
67
+ %Q(<a href="#{url.chop}?page=#{page_number + 1}&per=#{pagination_per}">#{page_number + 1}</a>)
68
+ end
69
+
70
+ def show_last_ellipsis_tag?(collection)
71
+ (total_pages(collection) - page_number) >= 3
72
+ end
73
+
74
+ def total_pages(collection)
75
+ collection.count / pagination_per + ((collection.count % pagination_per).zero? ? 0 : 1)
76
+ end
77
+
78
+ def show_last_page_tag?(collection)
79
+ page_number < (total_pages(collection) - 1)
80
+ end
81
+
82
+ def last_page_tag(collection)
83
+ total = total_pages(collection)
84
+ %Q(<a href="#{url.chop}?page=#{total}&per=#{pagination_per}">#{total}</a>)
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Split
4
+ class DashboardPaginator
5
+ def initialize(collection, page_number, per)
6
+ @collection = collection
7
+ @page_number = page_number
8
+ @per = per
9
+ end
10
+
11
+ def paginate
12
+ to = @page_number * @per
13
+ from = to - @per
14
+ @collection[from...to]
15
+ end
16
+ end
17
+ end
@@ -22,3 +22,13 @@ function confirmReopen() {
22
22
  var agree = confirm("This will reopen the experiment. Are you sure?");
23
23
  return agree ? true : false;
24
24
  }
25
+
26
+ function confirmEnableCohorting(){
27
+ var agree = confirm("This will enable the cohorting of the experiment. Are you sure?");
28
+ return agree ? true : false;
29
+ }
30
+
31
+ function confirmDisableCohorting(){
32
+ var agree = confirm("This will disable the cohorting of the experiment. Note: Existing participants will continue to receive their alternative and may continue to convert. Are you sure?");
33
+ return agree ? true : false;
34
+ }
@@ -317,3 +317,17 @@ a.button.green:focus, button.green:focus, input[type="submit"].green:focus {
317
317
  }
318
318
 
319
319
 
320
+ .pagination {
321
+ text-align: center;
322
+ font-size: 15px;
323
+ }
324
+
325
+ .pagination a, .paginaton span {
326
+ display: inline-block;
327
+ padding: 5px;
328
+ }
329
+
330
+ .divider {
331
+ display: inline-block;
332
+ margin-left: 10px;
333
+ }
@@ -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">
@@ -6,7 +6,7 @@
6
6
  <input type="button" id="toggle-active" value="Hide active" />
7
7
  <input type="button" id="clear-filter" value="Clear filters" />
8
8
 
9
- <% @experiments.each do |experiment| %>
9
+ <% paginated(@experiments).each do |experiment| %>
10
10
  <% if experiment.goals.empty? %>
11
11
  <%= erb :_experiment, :locals => {:goal => nil, :experiment => experiment} %>
12
12
  <% else %>
@@ -16,6 +16,10 @@
16
16
  <% end %>
17
17
  <% end %>
18
18
  <% end %>
19
+
20
+ <div class="pagination">
21
+ <%= pagination(@experiments) %>
22
+ </div>
19
23
  <% else %>
20
24
  <p class="intro">No experiments have started yet, you need to define them in your code and introduce them to your users.</p>
21
25
  <p class="intro">Check out the <a href='https://github.com/splitrb/split#readme'>Readme</a> for more help getting started.</p>
@@ -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
+
2
3
  require 'sinatra/base'
3
4
  require 'split'
4
5
  require 'bigdecimal'
5
6
  require 'split/dashboard/helpers'
7
+ require 'split/dashboard/pagination_helpers'
6
8
 
7
9
  module Split
8
10
  class Dashboard < Sinatra::Base
@@ -14,6 +16,7 @@ module Split
14
16
  set :method_override, true
15
17
 
16
18
  helpers Split::DashboardHelpers
19
+ helpers Split::DashboardPaginationHelpers
17
20
 
18
21
  get '/' do
19
22
  # Display experiments without a winner at the top of the dashboard
@@ -31,7 +34,13 @@ module Split
31
34
  end
32
35
 
33
36
  post '/force_alternative' do
34
- Split::User.new(self)[params[:experiment]] = params[:alternative]
37
+ experiment = Split::ExperimentCatalog.find(params[:experiment])
38
+ alternative = Split::Alternative.new(params[:alternative], experiment.name)
39
+
40
+ cookies = JSON.parse(request.cookies['split_override']) rescue {}
41
+ cookies[experiment.name] = alternative.name
42
+ response.set_cookie('split_override', { value: cookies.to_json, path: '/' })
43
+
35
44
  redirect url('/')
36
45
  end
37
46
 
@@ -60,6 +69,17 @@ module Split
60
69
  redirect url('/')
61
70
  end
62
71
 
72
+ post '/update_cohorting' do
73
+ @experiment = Split::ExperimentCatalog.find(params[:experiment])
74
+ case params[:cohorting_action].downcase
75
+ when "enable"
76
+ @experiment.enable_cohorting
77
+ when "disable"
78
+ @experiment.disable_cohorting
79
+ end
80
+ redirect url('/')
81
+ end
82
+
63
83
  delete '/experiment' do
64
84
  @experiment = Split::ExperimentCatalog.find(params[:experiment])
65
85
  @experiment.delete
@@ -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
@@ -28,8 +29,8 @@ module Split
28
29
  end
29
30
  end
30
31
 
31
- def ab_test(*arguments,&block)
32
- split_context_shim.ab_test(*arguments,&block)
32
+ def ab_test(*arguments, &block)
33
+ split_context_shim.ab_test(*arguments, &block)
33
34
  end
34
35
 
35
36
  private
data/lib/split/engine.rb CHANGED
@@ -1,10 +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
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
8
13
  end
9
14
  end
10
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