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
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Split
3
4
  class Alternative
4
5
  attr_accessor :name
@@ -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,13 +67,13 @@ 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
79
  def set_completed_count(count, goal = nil)
@@ -81,7 +82,7 @@ module Split
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
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,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
- 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,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Split
3
4
  class Configuration
4
5
  attr_accessor :ignore_ip_addresses
@@ -10,6 +11,7 @@ module Split
10
11
  attr_accessor :enabled
11
12
  attr_accessor :persistence
12
13
  attr_accessor :persistence_cookie_length
14
+ attr_accessor :persistence_cookie_domain
13
15
  attr_accessor :algorithm
14
16
  attr_accessor :store_override
15
17
  attr_accessor :start_manually
@@ -20,12 +22,14 @@ module Split
20
22
  attr_accessor :on_experiment_reset
21
23
  attr_accessor :on_experiment_delete
22
24
  attr_accessor :on_before_experiment_reset
25
+ attr_accessor :on_experiment_winner_choose
23
26
  attr_accessor :on_before_experiment_delete
24
27
  attr_accessor :include_rails_helper
25
28
  attr_accessor :beta_probability_simulations
26
29
  attr_accessor :winning_alternative_recalculation_interval
27
30
  attr_accessor :redis
28
31
  attr_accessor :dashboard_pagination_default_per_page
32
+ attr_accessor :cache
29
33
 
30
34
  attr_reader :experiments
31
35
 
@@ -35,83 +39,83 @@ module Split
35
39
  def bots
36
40
  @bots ||= {
37
41
  # Indexers
38
- 'AdsBot-Google' => 'Google Adwords',
39
- 'Baidu' => 'Chinese search engine',
40
- 'Baiduspider' => 'Chinese search engine',
41
- 'bingbot' => 'Microsoft bing bot',
42
- 'Butterfly' => 'Topsy Labs',
43
- 'Gigabot' => 'Gigabot spider',
44
- 'Googlebot' => 'Google spider',
45
- 'MJ12bot' => 'Majestic-12 spider',
46
- 'msnbot' => 'Microsoft bot',
47
- 'rogerbot' => 'SeoMoz spider',
48
- 'PaperLiBot' => 'PaperLi is another content curation service',
49
- 'Slurp' => 'Yahoo spider',
50
- 'Sogou' => 'Chinese search engine',
51
- 'spider' => 'generic web spider',
52
- 'UnwindFetchor' => 'Gnip crawler',
53
- 'WordPress' => 'WordPress spider',
54
- 'YandexAccessibilityBot' => 'Yandex accessibility spider',
55
- 'YandexBot' => 'Yandex spider',
56
- 'YandexMobileBot' => 'Yandex mobile spider',
57
- '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",
58
62
 
59
63
  # HTTP libraries
60
- 'Apache-HttpClient' => 'Java http library',
61
- 'AppEngine-Google' => 'Google App Engine',
62
- 'curl' => 'curl unix CLI http client',
63
- 'ColdFusion' => 'ColdFusion http library',
64
- 'EventMachine HttpClient' => 'Ruby http library',
65
- 'Go http package' => 'Go http library',
66
- 'Go-http-client' => 'Go http library',
67
- 'Java' => 'Generic Java http library',
68
- 'libwww-perl' => 'Perl client-server library loved by script kids',
69
- 'lwp-trivial' => 'Another Perl library loved by script kids',
70
- 'Python-urllib' => 'Python http library',
71
- 'PycURL' => 'Python http library',
72
- 'Test Certificate Info' => 'C http library?',
73
- 'Typhoeus' => 'Ruby http library',
74
- '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",
75
79
 
76
80
  # URL expanders / previewers
77
- 'awe.sm' => 'Awe.sm URL expander',
78
- 'bitlybot' => 'bit.ly bot',
79
- 'bot@linkfluence.net' => 'Linkfluence bot',
80
- 'facebookexternalhit' => 'facebook bot',
81
- 'Facebot' => 'Facebook crawler',
82
- 'Feedfetcher-Google' => 'Google Feedfetcher',
83
- 'https://developers.google.com/+/web/snippet' => 'Google+ Snippet Fetcher',
84
- 'LinkedInBot' => 'LinkedIn bot',
85
- 'LongURL' => 'URL expander service',
86
- 'NING' => 'NING - Yet Another Twitter Swarmer',
87
- 'Pinterest' => 'Pinterest Bot',
88
- 'redditbot' => 'Reddit Bot',
89
- 'ShortLinkTranslate' => 'Link shortener',
90
- 'Slackbot' => 'Slackbot link expander',
91
- 'TweetmemeBot' => 'TweetMeMe Crawler',
92
- 'Twitterbot' => 'Twitter URL expander',
93
- 'UnwindFetch' => 'Gnip URL expander',
94
- '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",
95
99
 
96
100
  # Uptime monitoring
97
- 'check_http' => 'Nagios monitor',
98
- 'GoogleStackdriverMonitoring' => 'Google Cloud monitor',
99
- 'NewRelicPinger' => 'NewRelic monitor',
100
- 'Panopta' => 'Monitoring service',
101
- 'Pingdom' => 'Pingdom monitoring',
102
- 'SiteUptime' => 'Site monitoring services',
103
- '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",
104
108
 
105
109
  # ???
106
- 'DigitalPersona Fingerprint Software' => 'HP Fingerprint scanner',
107
- 'ShowyouBot' => 'Showyou iOS app spider',
108
- 'ZyBorg' => 'Zyborg? Hmmm....',
109
- '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"
110
114
  }
111
115
  end
112
116
 
113
- def experiments= experiments
114
- 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)
115
119
  @experiments = experiments
116
120
  end
117
121
 
@@ -153,8 +157,8 @@ module Split
153
157
 
154
158
  @experiments.each do |experiment_name, settings|
155
159
  alternatives = if (alts = value_for(settings, :alternatives))
156
- normalize_alternatives(alts)
157
- end
160
+ normalize_alternatives(alts)
161
+ end
158
162
 
159
163
  experiment_data = {
160
164
  alternatives: alternatives,
@@ -173,7 +177,7 @@ module Split
173
177
  end
174
178
 
175
179
  def normalize_alternatives(alternatives)
176
- 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|
177
181
  p, n = a
178
182
  if percent = value_for(v, :percent)
179
183
  [p + percent, n + 1]
@@ -209,47 +213,38 @@ module Split
209
213
 
210
214
  def initialize
211
215
  @ignore_ip_addresses = []
212
- @ignore_filter = proc{ |request| is_robot? || is_ignored_ip_address? }
216
+ @ignore_filter = proc { |request| is_robot? || is_ignored_ip_address? }
213
217
  @db_failover = false
214
- @db_failover_on_db_error = proc{|error|} # e.g. use Rails logger here
215
- @on_experiment_reset = proc{|experiment|}
216
- @on_experiment_delete = proc{|experiment|}
217
- @on_before_experiment_reset = proc{|experiment|}
218
- @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| }
219
224
  @db_failover_allow_parameter_override = false
220
225
  @allow_multiple_experiments = false
221
226
  @enabled = true
222
227
  @experiments = {}
223
228
  @persistence = Split::Persistence::SessionAdapter
224
229
  @persistence_cookie_length = 31536000 # One year from now
230
+ @persistence_cookie_domain = nil
225
231
  @algorithm = Split::Algorithms::WeightedSample
226
232
  @include_rails_helper = true
227
233
  @beta_probability_simulations = 10000
228
234
  @winning_alternative_recalculation_interval = 60 * 60 * 24 # 1 day
229
- @redis = ENV.fetch(ENV.fetch('REDIS_PROVIDER', 'REDIS_URL'), 'redis://localhost:6379')
235
+ @redis = ENV.fetch(ENV.fetch("REDIS_PROVIDER", "REDIS_URL"), "redis://localhost:6379")
230
236
  @dashboard_pagination_default_per_page = 10
231
237
  end
232
238
 
233
- def redis_url=(value)
234
- warn '[DEPRECATED] `redis_url=` is deprecated in favor of `redis=`'
235
- self.redis = value
236
- end
237
-
238
- def redis_url
239
- warn '[DEPRECATED] `redis_url` is deprecated in favor of `redis`'
240
- self.redis
241
- end
242
-
243
239
  private
244
-
245
- def value_for(hash, key)
246
- if hash.kind_of?(Hash)
247
- 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
248
244
  end
249
- end
250
245
 
251
- def escaped_bots
252
- bots.map { |key, _| Regexp.escape(key) }
253
- end
246
+ def escaped_bots
247
+ bots.map { |key, _| Regexp.escape(key) }
248
+ end
254
249
  end
255
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,5 +1,6 @@
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
@@ -29,58 +30,57 @@ module Split
29
30
  end
30
31
 
31
32
  private
32
-
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
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
85
  end
86
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
+ }