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
@@ -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
@@ -37,11 +38,11 @@ module Split
37
38
  end
38
39
 
39
40
  def participant_count
40
- Split.redis.hget(key, 'participant_count').to_i
41
+ Split.redis.hget(key, "participant_count").to_i
41
42
  end
42
43
 
43
44
  def participant_count=(count)
44
- Split.redis.hset(key, 'participant_count', count.to_i)
45
+ Split.redis.hset(key, "participant_count", count.to_i)
45
46
  end
46
47
 
47
48
  def completed_count(goal = nil)
@@ -66,22 +67,22 @@ module Split
66
67
  def set_field(goal)
67
68
  field = "completed_count"
68
69
  field += ":" + goal unless goal.nil?
69
- return field
70
+ field
70
71
  end
71
72
 
72
73
  def set_prob_field(goal)
73
74
  field = "p_winner"
74
75
  field += ":" + goal unless goal.nil?
75
- return field
76
+ 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
82
83
 
83
84
  def increment_participation
84
- Split.redis.hincrby key, 'participant_count', 1
85
+ Split.redis.hincrby key, "participant_count", 1
85
86
  end
86
87
 
87
88
  def increment_completion(goal = nil)
@@ -111,7 +112,7 @@ module Split
111
112
  control = experiment.control
112
113
  alternative = self
113
114
 
114
- return 'N/A' if control.name == alternative.name
115
+ return "N/A" if control.name == alternative.name
115
116
 
116
117
  p_a = alternative.conversion_rate(goal)
117
118
  p_c = control.conversion_rate(goal)
@@ -120,13 +121,13 @@ module Split
120
121
  n_c = control.participant_count
121
122
 
122
123
  # can't calculate zscore for P(x) > 1
123
- return 'N/A' if p_a > 1 || p_c > 1
124
+ return "N/A" if p_a > 1 || p_c > 1
124
125
 
125
- z_score = Split::Zscore.calculate(p_a, n_a, p_c, n_c)
126
+ Split::Zscore.calculate(p_a, n_a, p_c, n_c)
126
127
  end
127
128
 
128
129
  def extra_info
129
- data = Split.redis.hget(key, 'recorded_info')
130
+ data = Split.redis.hget(key, "recorded_info")
130
131
  if data && data.length > 1
131
132
  begin
132
133
  JSON.parse(data)
@@ -148,24 +149,24 @@ module Split
148
149
  @recorded_info[k] = value
149
150
  end
150
151
 
151
- Split.redis.hset key, 'recorded_info', (@recorded_info || {}).to_json
152
+ Split.redis.hset key, "recorded_info", (@recorded_info || {}).to_json
152
153
  end
153
154
 
154
155
  def save
155
- Split.redis.hsetnx key, 'participant_count', 0
156
- Split.redis.hsetnx key, 'completed_count', 0
157
- Split.redis.hsetnx key, 'p_winner', p_winner
158
- Split.redis.hsetnx key, 'recorded_info', (@recorded_info || {}).to_json
156
+ Split.redis.hsetnx key, "participant_count", 0
157
+ Split.redis.hsetnx key, "completed_count", 0
158
+ Split.redis.hsetnx key, "p_winner", p_winner
159
+ Split.redis.hsetnx key, "recorded_info", (@recorded_info || {}).to_json
159
160
  end
160
161
 
161
162
  def validate!
162
163
  unless String === @name || hash_with_correct_values?(@name)
163
- raise ArgumentError, 'Alternative must be a string'
164
+ raise ArgumentError, "Alternative must be a string"
164
165
  end
165
166
  end
166
167
 
167
168
  def reset
168
- Split.redis.hmset key, 'participant_count', 0, 'completed_count', 0, 'recorded_info', nil
169
+ Split.redis.hmset key, "participant_count", 0, "completed_count", 0, "recorded_info", ""
169
170
  unless goals.empty?
170
171
  goals.each do |g|
171
172
  field = "completed_count:#{g}"
@@ -179,13 +180,12 @@ module Split
179
180
  end
180
181
 
181
182
  private
183
+ def hash_with_correct_values?(name)
184
+ Hash === name && String === name.keys.first && Float(name.values.first) rescue false
185
+ end
182
186
 
183
- def hash_with_correct_values?(name)
184
- Hash === name && String === name.keys.first && Float(name.values.first) rescue false
185
- end
186
-
187
- def key
188
- "#{experiment_name}:#{name}"
189
- end
187
+ def key
188
+ "#{experiment_name}:#{name}"
189
+ end
190
190
  end
191
191
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Split
4
+ class Cache
5
+ def self.clear
6
+ @cache = nil
7
+ end
8
+
9
+ def self.fetch(namespace, key)
10
+ return yield unless Split.configuration.cache
11
+
12
+ @cache ||= {}
13
+ @cache[namespace] ||= {}
14
+
15
+ value = @cache[namespace][key]
16
+ return value if value
17
+
18
+ @cache[namespace][key] = yield
19
+ end
20
+
21
+ def self.clear_key(key)
22
+ @cache&.keys&.each do |namespace|
23
+ @cache[namespace]&.delete(key)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,9 +1,10 @@
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)
5
6
  return nil unless experiment = find_combined_experiment(metric_descriptor)
6
- raise(Split::InvalidExperimentsFormatError, 'Unable to find experiment #{metric_descriptor} in configuration') if experiment[:combined_experiments].nil?
7
+ raise(Split::InvalidExperimentsFormatError, "Unable to find experiment #{metric_descriptor} in configuration") if experiment[:combined_experiments].nil?
7
8
 
8
9
  alternative = nil
9
10
  weighted_alternatives = nil
@@ -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,89 +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
- 'Java' => 'Generic Java http library',
65
- 'libwww-perl' => 'Perl client-server library loved by script kids',
66
- 'lwp-trivial' => 'Another Perl library loved by script kids',
67
- 'Python-urllib' => 'Python http library',
68
- 'PycURL' => 'Python http library',
69
- 'Test Certificate Info' => 'C http library?',
70
- '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",
71
79
 
72
80
  # URL expanders / previewers
73
- 'awe.sm' => 'Awe.sm URL expander',
74
- 'bitlybot' => 'bit.ly bot',
75
- 'bot@linkfluence.net' => 'Linkfluence bot',
76
- 'facebookexternalhit' => 'facebook bot',
77
- 'Feedfetcher-Google' => 'Google Feedfetcher',
78
- 'https://developers.google.com/+/web/snippet' => 'Google+ Snippet Fetcher',
79
- 'LinkedInBot' => 'LinkedIn bot',
80
- 'LongURL' => 'URL expander service',
81
- 'NING' => 'NING - Yet Another Twitter Swarmer',
82
- 'Pinterest' => 'Pinterest Bot',
83
- 'redditbot' => 'Reddit Bot',
84
- 'ShortLinkTranslate' => 'Link shortener',
85
- 'Slackbot' => 'Slackbot link expander',
86
- 'TweetmemeBot' => 'TweetMeMe Crawler',
87
- 'Twitterbot' => 'Twitter URL expander',
88
- 'UnwindFetch' => 'Gnip URL expander',
89
- '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",
90
99
 
91
100
  # Uptime monitoring
92
- 'check_http' => 'Nagios monitor',
93
- 'NewRelicPinger' => 'NewRelic monitor',
94
- 'Panopta' => 'Monitoring service',
95
- 'Pingdom' => 'Pingdom monitoring',
96
- 'SiteUptime' => 'Site monitoring services',
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",
97
108
 
98
109
  # ???
99
- 'DigitalPersona Fingerprint Software' => 'HP Fingerprint scanner',
100
- 'ShowyouBot' => 'Showyou iOS app spider',
101
- 'ZyBorg' => 'Zyborg? Hmmm....',
102
- '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"
103
114
  }
104
115
  end
105
116
 
106
- def experiments= experiments
107
- 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)
108
119
  @experiments = experiments
109
120
  end
110
121
 
@@ -146,8 +157,8 @@ module Split
146
157
 
147
158
  @experiments.each do |experiment_name, settings|
148
159
  alternatives = if (alts = value_for(settings, :alternatives))
149
- normalize_alternatives(alts)
150
- end
160
+ normalize_alternatives(alts)
161
+ end
151
162
 
152
163
  experiment_data = {
153
164
  alternatives: alternatives,
@@ -166,7 +177,7 @@ module Split
166
177
  end
167
178
 
168
179
  def normalize_alternatives(alternatives)
169
- 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|
170
181
  p, n = a
171
182
  if percent = value_for(v, :percent)
172
183
  [p + percent, n + 1]
@@ -202,46 +213,38 @@ module Split
202
213
 
203
214
  def initialize
204
215
  @ignore_ip_addresses = []
205
- @ignore_filter = proc{ |request| is_robot? || is_ignored_ip_address? }
216
+ @ignore_filter = proc { |request| is_robot? || is_ignored_ip_address? }
206
217
  @db_failover = false
207
- @db_failover_on_db_error = proc{|error|} # e.g. use Rails logger here
208
- @on_experiment_reset = proc{|experiment|}
209
- @on_experiment_delete = proc{|experiment|}
210
- @on_before_experiment_reset = proc{|experiment|}
211
- @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| }
212
224
  @db_failover_allow_parameter_override = false
213
225
  @allow_multiple_experiments = false
214
226
  @enabled = true
215
227
  @experiments = {}
216
228
  @persistence = Split::Persistence::SessionAdapter
217
229
  @persistence_cookie_length = 31536000 # One year from now
230
+ @persistence_cookie_domain = nil
218
231
  @algorithm = Split::Algorithms::WeightedSample
219
232
  @include_rails_helper = true
220
233
  @beta_probability_simulations = 10000
221
234
  @winning_alternative_recalculation_interval = 60 * 60 * 24 # 1 day
222
- @redis = ENV.fetch(ENV.fetch('REDIS_PROVIDER', 'REDIS_URL'), 'redis://localhost:6379')
223
- end
224
-
225
- def redis_url=(value)
226
- warn '[DEPRECATED] `redis_url=` is deprecated in favor of `redis=`'
227
- self.redis = value
228
- end
229
-
230
- def redis_url
231
- warn '[DEPRECATED] `redis_url` is deprecated in favor of `redis`'
232
- self.redis
235
+ @redis = ENV.fetch(ENV.fetch("REDIS_PROVIDER", "REDIS_URL"), "redis://localhost:6379")
236
+ @dashboard_pagination_default_per_page = 10
233
237
  end
234
238
 
235
239
  private
236
-
237
- def value_for(hash, key)
238
- if hash.kind_of?(Hash)
239
- 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
240
244
  end
241
- end
242
245
 
243
- def escaped_bots
244
- bots.map { |key, _| Regexp.escape(key) }
245
- end
246
+ def escaped_bots
247
+ bots.map { |key, _| Regexp.escape(key) }
248
+ end
246
249
  end
247
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)
@@ -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
 
@@ -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
@@ -0,0 +1,86 @@
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
+ 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
85
+ end
86
+ 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
+ }
@@ -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,8 +312,25 @@ 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
+ }
322
+
323
+ .pagination {
324
+ text-align: center;
325
+ font-size: 15px;
326
+ }
327
+
328
+ .pagination a, .paginaton span {
329
+ display: inline-block;
330
+ padding: 5px;
331
+ }
319
332
 
333
+ .divider {
334
+ display: inline-block;
335
+ margin-left: 10px;
336
+ }