split 3.3.2 → 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 (89) hide show
  1. checksums.yaml +4 -4
  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 +121 -0
  10. data/CODE_OF_CONDUCT.md +3 -3
  11. data/CONTRIBUTING.md +1 -1
  12. data/Gemfile +6 -1
  13. data/README.md +51 -21
  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 +5 -4
  22. data/lib/split/configuration.rb +94 -96
  23. data/lib/split/dashboard/helpers.rb +7 -7
  24. data/lib/split/dashboard/pagination_helpers.rb +56 -57
  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/views/layout.erb +1 -1
  32. data/lib/split/dashboard.rb +46 -21
  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 +52 -24
  41. data/lib/split/metric.rb +6 -6
  42. data/lib/split/persistence/cookie_adapter.rb +47 -44
  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 -29
  48. data/lib/split/trial.rb +44 -35
  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 +35 -28
  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 +71 -67
  61. data/spec/dashboard/paginator_spec.rb +10 -9
  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 +531 -424
  69. data/spec/metric_spec.rb +14 -14
  70. data/spec/persistence/cookie_adapter_spec.rb +26 -11
  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 +11 -11
  78. data/spec/support/cookies_mock.rb +1 -2
  79. data/spec/trial_spec.rb +102 -75
  80. data/spec/user_spec.rb +69 -27
  81. data/split.gemspec +26 -23
  82. metadata +68 -42
  83. data/.travis.yml +0 -66
  84. data/Appraisals +0 -19
  85. data/gemfiles/4.2.gemfile +0 -9
  86. data/gemfiles/5.0.gemfile +0 -9
  87. data/gemfiles/5.1.gemfile +0 -9
  88. data/gemfiles/5.2.gemfile +0 -9
  89. data/gemfiles/6.0.gemfile +0 -9
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Split
3
4
  module CombinedExperimentsHelper
4
5
  def ab_combined_test(metric_descriptor, control = nil, *alternatives)
@@ -28,10 +29,10 @@ module Split
28
29
  end
29
30
 
30
31
  def find_combined_experiment(metric_descriptor)
31
- raise(Split::InvalidExperimentsFormatError, 'Invalid descriptor class (String or Symbol required)') unless metric_descriptor.class == String || metric_descriptor.class == Symbol
32
- raise(Split::InvalidExperimentsFormatError, 'Enable configuration') unless Split.configuration.enabled
33
- raise(Split::InvalidExperimentsFormatError, 'Enable `allow_multiple_experiments`') unless Split.configuration.allow_multiple_experiments
34
- experiment = Split::configuration.experiments[metric_descriptor.to_sym]
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]
35
36
  end
36
37
  end
37
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,94 +22,100 @@ 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
28
29
  attr_accessor :winning_alternative_recalculation_interval
29
30
  attr_accessor :redis
31
+ attr_accessor :dashboard_pagination_default_per_page
32
+ attr_accessor :cache
30
33
 
31
34
  attr_reader :experiments
32
35
 
36
+ attr_writer :bots
37
+ attr_writer :robot_regex
38
+
33
39
  def bots
34
40
  @bots ||= {
35
41
  # Indexers
36
- 'AdsBot-Google' => 'Google Adwords',
37
- 'Baidu' => 'Chinese search engine',
38
- 'Baiduspider' => 'Chinese search engine',
39
- 'bingbot' => 'Microsoft bing bot',
40
- 'Butterfly' => 'Topsy Labs',
41
- 'Gigabot' => 'Gigabot spider',
42
- 'Googlebot' => 'Google spider',
43
- 'MJ12bot' => 'Majestic-12 spider',
44
- 'msnbot' => 'Microsoft bot',
45
- 'rogerbot' => 'SeoMoz spider',
46
- 'PaperLiBot' => 'PaperLi is another content curation service',
47
- 'Slurp' => 'Yahoo spider',
48
- 'Sogou' => 'Chinese search engine',
49
- 'spider' => 'generic web spider',
50
- 'UnwindFetchor' => 'Gnip crawler',
51
- 'WordPress' => 'WordPress spider',
52
- 'YandexAccessibilityBot' => 'Yandex accessibility spider',
53
- 'YandexBot' => 'Yandex spider',
54
- 'YandexMobileBot' => 'Yandex mobile spider',
55
- 'ZIBB' => 'ZIBB spider',
42
+ "AdsBot-Google" => "Google Adwords",
43
+ "Baidu" => "Chinese search engine",
44
+ "Baiduspider" => "Chinese search engine",
45
+ "bingbot" => "Microsoft bing bot",
46
+ "Butterfly" => "Topsy Labs",
47
+ "Gigabot" => "Gigabot spider",
48
+ "Googlebot" => "Google spider",
49
+ "MJ12bot" => "Majestic-12 spider",
50
+ "msnbot" => "Microsoft bot",
51
+ "rogerbot" => "SeoMoz spider",
52
+ "PaperLiBot" => "PaperLi is another content curation service",
53
+ "Slurp" => "Yahoo spider",
54
+ "Sogou" => "Chinese search engine",
55
+ "spider" => "generic web spider",
56
+ "UnwindFetchor" => "Gnip crawler",
57
+ "WordPress" => "WordPress spider",
58
+ "YandexAccessibilityBot" => "Yandex accessibility spider",
59
+ "YandexBot" => "Yandex spider",
60
+ "YandexMobileBot" => "Yandex mobile spider",
61
+ "ZIBB" => "ZIBB spider",
56
62
 
57
63
  # HTTP libraries
58
- 'Apache-HttpClient' => 'Java http library',
59
- 'AppEngine-Google' => 'Google App Engine',
60
- 'curl' => 'curl unix CLI http client',
61
- 'ColdFusion' => 'ColdFusion http library',
62
- 'EventMachine HttpClient' => 'Ruby http library',
63
- 'Go http package' => 'Go http library',
64
- 'Go-http-client' => 'Go http library',
65
- 'Java' => 'Generic Java http library',
66
- 'libwww-perl' => 'Perl client-server library loved by script kids',
67
- 'lwp-trivial' => 'Another Perl library loved by script kids',
68
- 'Python-urllib' => 'Python http library',
69
- 'PycURL' => 'Python http library',
70
- 'Test Certificate Info' => 'C http library?',
71
- 'Typhoeus' => 'Ruby http library',
72
- 'Wget' => 'wget unix CLI http client',
64
+ "Apache-HttpClient" => "Java http library",
65
+ "AppEngine-Google" => "Google App Engine",
66
+ "curl" => "curl unix CLI http client",
67
+ "ColdFusion" => "ColdFusion http library",
68
+ "EventMachine HttpClient" => "Ruby http library",
69
+ "Go http package" => "Go http library",
70
+ "Go-http-client" => "Go http library",
71
+ "Java" => "Generic Java http library",
72
+ "libwww-perl" => "Perl client-server library loved by script kids",
73
+ "lwp-trivial" => "Another Perl library loved by script kids",
74
+ "Python-urllib" => "Python http library",
75
+ "PycURL" => "Python http library",
76
+ "Test Certificate Info" => "C http library?",
77
+ "Typhoeus" => "Ruby http library",
78
+ "Wget" => "wget unix CLI http client",
73
79
 
74
80
  # URL expanders / previewers
75
- 'awe.sm' => 'Awe.sm URL expander',
76
- 'bitlybot' => 'bit.ly bot',
77
- 'bot@linkfluence.net' => 'Linkfluence bot',
78
- 'facebookexternalhit' => 'facebook bot',
79
- 'Facebot' => 'Facebook crawler',
80
- 'Feedfetcher-Google' => 'Google Feedfetcher',
81
- 'https://developers.google.com/+/web/snippet' => 'Google+ Snippet Fetcher',
82
- 'LinkedInBot' => 'LinkedIn bot',
83
- 'LongURL' => 'URL expander service',
84
- 'NING' => 'NING - Yet Another Twitter Swarmer',
85
- 'Pinterest' => 'Pinterest Bot',
86
- 'redditbot' => 'Reddit Bot',
87
- 'ShortLinkTranslate' => 'Link shortener',
88
- 'Slackbot' => 'Slackbot link expander',
89
- 'TweetmemeBot' => 'TweetMeMe Crawler',
90
- 'Twitterbot' => 'Twitter URL expander',
91
- 'UnwindFetch' => 'Gnip URL expander',
92
- 'vkShare' => 'VKontake Sharer',
81
+ "awe.sm" => "Awe.sm URL expander",
82
+ "bitlybot" => "bit.ly bot",
83
+ "bot@linkfluence.net" => "Linkfluence bot",
84
+ "facebookexternalhit" => "facebook bot",
85
+ "Facebot" => "Facebook crawler",
86
+ "Feedfetcher-Google" => "Google Feedfetcher",
87
+ "https://developers.google.com/+/web/snippet" => "Google+ Snippet Fetcher",
88
+ "LinkedInBot" => "LinkedIn bot",
89
+ "LongURL" => "URL expander service",
90
+ "NING" => "NING - Yet Another Twitter Swarmer",
91
+ "Pinterestbot" => "Pinterest Bot",
92
+ "redditbot" => "Reddit Bot",
93
+ "ShortLinkTranslate" => "Link shortener",
94
+ "Slackbot" => "Slackbot link expander",
95
+ "TweetmemeBot" => "TweetMeMe Crawler",
96
+ "Twitterbot" => "Twitter URL expander",
97
+ "UnwindFetch" => "Gnip URL expander",
98
+ "vkShare" => "VKontake Sharer",
93
99
 
94
100
  # Uptime monitoring
95
- 'check_http' => 'Nagios monitor',
96
- 'GoogleStackdriverMonitoring' => 'Google Cloud monitor',
97
- 'NewRelicPinger' => 'NewRelic monitor',
98
- 'Panopta' => 'Monitoring service',
99
- 'Pingdom' => 'Pingdom monitoring',
100
- 'SiteUptime' => 'Site monitoring services',
101
- 'UptimeRobot' => 'Monitoring service',
101
+ "check_http" => "Nagios monitor",
102
+ "GoogleStackdriverMonitoring" => "Google Cloud monitor",
103
+ "NewRelicPinger" => "NewRelic monitor",
104
+ "Panopta" => "Monitoring service",
105
+ "Pingdom" => "Pingdom monitoring",
106
+ "SiteUptime" => "Site monitoring services",
107
+ "UptimeRobot" => "Monitoring service",
102
108
 
103
109
  # ???
104
- 'DigitalPersona Fingerprint Software' => 'HP Fingerprint scanner',
105
- 'ShowyouBot' => 'Showyou iOS app spider',
106
- 'ZyBorg' => 'Zyborg? Hmmm....',
107
- 'ELB-HealthChecker' => 'ELB Health Check'
110
+ "DigitalPersona Fingerprint Software" => "HP Fingerprint scanner",
111
+ "ShowyouBot" => "Showyou iOS app spider",
112
+ "ZyBorg" => "Zyborg? Hmmm....",
113
+ "ELB-HealthChecker" => "ELB Health Check"
108
114
  }
109
115
  end
110
116
 
111
- def experiments= experiments
112
- raise InvalidExperimentsFormatError.new('Experiments must be a Hash') unless experiments.respond_to?(:keys)
117
+ def experiments=(experiments)
118
+ raise InvalidExperimentsFormatError.new("Experiments must be a Hash") unless experiments.respond_to?(:keys)
113
119
  @experiments = experiments
114
120
  end
115
121
 
@@ -151,8 +157,8 @@ module Split
151
157
 
152
158
  @experiments.each do |experiment_name, settings|
153
159
  alternatives = if (alts = value_for(settings, :alternatives))
154
- normalize_alternatives(alts)
155
- end
160
+ normalize_alternatives(alts)
161
+ end
156
162
 
157
163
  experiment_data = {
158
164
  alternatives: alternatives,
@@ -171,7 +177,7 @@ module Split
171
177
  end
172
178
 
173
179
  def normalize_alternatives(alternatives)
174
- 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|
175
181
  p, n = a
176
182
  if percent = value_for(v, :percent)
177
183
  [p + percent, n + 1]
@@ -207,46 +213,38 @@ module Split
207
213
 
208
214
  def initialize
209
215
  @ignore_ip_addresses = []
210
- @ignore_filter = proc{ |request| is_robot? || is_ignored_ip_address? }
216
+ @ignore_filter = proc { |request| is_robot? || is_ignored_ip_address? }
211
217
  @db_failover = false
212
- @db_failover_on_db_error = proc{|error|} # e.g. use Rails logger here
213
- @on_experiment_reset = proc{|experiment|}
214
- @on_experiment_delete = proc{|experiment|}
215
- @on_before_experiment_reset = proc{|experiment|}
216
- @on_before_experiment_delete = proc{|experiment|}
218
+ @db_failover_on_db_error = proc { |error| } # e.g. use Rails logger here
219
+ @on_experiment_reset = proc { |experiment| }
220
+ @on_experiment_delete = proc { |experiment| }
221
+ @on_before_experiment_reset = proc { |experiment| }
222
+ @on_before_experiment_delete = proc { |experiment| }
223
+ @on_experiment_winner_choose = proc { |experiment| }
217
224
  @db_failover_allow_parameter_override = false
218
225
  @allow_multiple_experiments = false
219
226
  @enabled = true
220
227
  @experiments = {}
221
228
  @persistence = Split::Persistence::SessionAdapter
222
229
  @persistence_cookie_length = 31536000 # One year from now
230
+ @persistence_cookie_domain = nil
223
231
  @algorithm = Split::Algorithms::WeightedSample
224
232
  @include_rails_helper = true
225
233
  @beta_probability_simulations = 10000
226
234
  @winning_alternative_recalculation_interval = 60 * 60 * 24 # 1 day
227
- @redis = ENV.fetch(ENV.fetch('REDIS_PROVIDER', 'REDIS_URL'), 'redis://localhost:6379')
228
- end
229
-
230
- def redis_url=(value)
231
- warn '[DEPRECATED] `redis_url=` is deprecated in favor of `redis=`'
232
- self.redis = value
233
- end
234
-
235
- def redis_url
236
- warn '[DEPRECATED] `redis_url` is deprecated in favor of `redis`'
237
- self.redis
235
+ @redis = ENV.fetch(ENV.fetch("REDIS_PROVIDER", "REDIS_URL"), "redis://localhost:6379")
236
+ @dashboard_pagination_default_per_page = 10
238
237
  end
239
238
 
240
239
  private
241
-
242
- def value_for(hash, key)
243
- if hash.kind_of?(Hash)
244
- hash.has_key?(key.to_s) ? hash[key.to_s] : hash[key.to_sym]
240
+ def value_for(hash, key)
241
+ if hash.kind_of?(Hash)
242
+ hash.has_key?(key.to_s) ? hash[key.to_s] : hash[key.to_sym]
243
+ end
245
244
  end
246
- end
247
245
 
248
- def escaped_bots
249
- bots.map { |key, _| Regexp.escape(key) }
250
- end
246
+ def escaped_bots
247
+ bots.map { |key, _| Regexp.escape(key) }
248
+ end
251
249
  end
252
250
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Split
3
4
  module DashboardHelpers
4
5
  def h(text)
@@ -6,11 +7,11 @@ module Split
6
7
  end
7
8
 
8
9
  def url(*path_parts)
9
- [ path_prefix, path_parts ].join("/").squeeze('/')
10
+ [ path_prefix, path_parts ].join("/").squeeze("/")
10
11
  end
11
12
 
12
13
  def path_prefix
13
- request.env['SCRIPT_NAME']
14
+ request.env["SCRIPT_NAME"]
14
15
  end
15
16
 
16
17
  def number_to_percentage(number, precision = 2)
@@ -31,15 +32,14 @@ module Split
31
32
  z = round(z_score.to_s.to_f, 3).abs
32
33
 
33
34
  if z >= 2.58
34
- '99% confidence'
35
+ "99% confidence"
35
36
  elsif z >= 1.96
36
- '95% confidence'
37
+ "95% confidence"
37
38
  elsif z >= 1.65
38
- '90% confidence'
39
+ "90% confidence"
39
40
  else
40
- 'Insufficient confidence'
41
+ "Insufficient confidence"
41
42
  end
42
-
43
43
  end
44
44
  end
45
45
  end
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
- require 'split/dashboard/paginator'
2
+
3
+ require "split/dashboard/paginator"
3
4
 
4
5
  module Split
5
6
  module DashboardPaginationHelpers
6
- DEFAULT_PER = 10
7
-
8
7
  def pagination_per
9
- @pagination_per ||= (params[:per] || DEFAULT_PER).to_i
8
+ default_per_page = Split.configuration.dashboard_pagination_default_per_page
9
+ @pagination_per ||= (params[:per] || default_per_page).to_i
10
10
  end
11
11
 
12
12
  def page_number
@@ -30,58 +30,57 @@ module Split
30
30
  end
31
31
 
32
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
33
+ def show_first_page_tag?
34
+ page_number > 2
35
+ end
36
+
37
+ def first_page_tag
38
+ %Q(<a href="#{url.chop}?page=1&per=#{pagination_per}">1</a>)
39
+ end
40
+
41
+ def show_first_ellipsis_tag?
42
+ page_number >= 4
43
+ end
44
+
45
+ def ellipsis_tag
46
+ "<span>...</span>"
47
+ end
48
+
49
+ def show_prev_page_tag?
50
+ page_number > 1
51
+ end
52
+
53
+ def prev_page_tag
54
+ %Q(<a href="#{url.chop}?page=#{page_number - 1}&per=#{pagination_per}">#{page_number - 1}</a>)
55
+ end
56
+
57
+ def current_page_tag
58
+ "<span><b>#{page_number}</b></span>"
59
+ end
60
+
61
+ def show_next_page_tag?(collection)
62
+ (page_number * pagination_per) < collection.count
63
+ end
64
+
65
+ def next_page_tag
66
+ %Q(<a href="#{url.chop}?page=#{page_number + 1}&per=#{pagination_per}">#{page_number + 1}</a>)
67
+ end
68
+
69
+ def show_last_ellipsis_tag?(collection)
70
+ (total_pages(collection) - page_number) >= 3
71
+ end
72
+
73
+ def total_pages(collection)
74
+ collection.count / pagination_per + ((collection.count % pagination_per).zero? ? 0 : 1)
75
+ end
76
+
77
+ def show_last_page_tag?(collection)
78
+ page_number < (total_pages(collection) - 1)
79
+ end
80
+
81
+ def last_page_tag(collection)
82
+ total = total_pages(collection)
83
+ %Q(<a href="#{url.chop}?page=#{total}&per=#{pagination_per}">#{total}</a>)
84
+ end
86
85
  end
87
86
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Split
3
4
  class DashboardPaginator
4
5
  def initialize(collection, page_number, per)
@@ -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
+ }
@@ -258,7 +258,7 @@ body {
258
258
  color: #408C48;
259
259
  }
260
260
 
261
- a.button, button, input[type="submit"] {
261
+ .experiment a.button, .experiment button, .experiment input[type="submit"] {
262
262
  padding: 4px 10px;
263
263
  overflow: hidden;
264
264
  background: #d8dae0;
@@ -312,10 +312,13 @@ a.button.green:focus, button.green:focus, input[type="submit"].green:focus {
312
312
  background:#768E7A;
313
313
  }
314
314
 
315
- #filter, #clear-filter {
315
+ .dashboard-controls input, .dashboard-controls select {
316
316
  padding: 10px;
317
317
  }
318
318
 
319
+ .dashboard-controls-bottom {
320
+ margin-top: 10px;
321
+ }
319
322
 
320
323
  .pagination {
321
324
  text-align: center;
@@ -326,3 +329,8 @@ a.button.green:focus, button.green:focus, input[type="submit"].green:focus {
326
329
  display: inline-block;
327
330
  padding: 5px;
328
331
  }
332
+
333
+ .divider {
334
+ display: inline-block;
335
+ margin-left: 10px;
336
+ }
@@ -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>
@@ -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>