ab-split 1.0.0
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.
- checksums.yaml +7 -0
- data/.codeclimate.yml +30 -0
- data/.csslintrc +2 -0
- data/.eslintignore +1 -0
- data/.eslintrc +213 -0
- data/.github/FUNDING.yml +1 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +24 -0
- data/.rspec +1 -0
- data/.rubocop.yml +7 -0
- data/.rubocop_todo.yml +679 -0
- data/.travis.yml +60 -0
- data/Appraisals +19 -0
- data/CHANGELOG.md +696 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/CONTRIBUTING.md +62 -0
- data/Gemfile +7 -0
- data/LICENSE +22 -0
- data/README.md +955 -0
- data/Rakefile +9 -0
- data/ab-split.gemspec +44 -0
- data/gemfiles/4.2.gemfile +9 -0
- data/gemfiles/5.0.gemfile +9 -0
- data/gemfiles/5.1.gemfile +9 -0
- data/gemfiles/5.2.gemfile +9 -0
- data/gemfiles/6.0.gemfile +9 -0
- data/lib/split.rb +76 -0
- data/lib/split/algorithms/block_randomization.rb +23 -0
- data/lib/split/algorithms/weighted_sample.rb +18 -0
- data/lib/split/algorithms/whiplash.rb +38 -0
- data/lib/split/alternative.rb +191 -0
- data/lib/split/combined_experiments_helper.rb +37 -0
- data/lib/split/configuration.rb +255 -0
- data/lib/split/dashboard.rb +74 -0
- data/lib/split/dashboard/helpers.rb +45 -0
- data/lib/split/dashboard/pagination_helpers.rb +86 -0
- data/lib/split/dashboard/paginator.rb +16 -0
- data/lib/split/dashboard/public/dashboard-filtering.js +43 -0
- data/lib/split/dashboard/public/dashboard.js +24 -0
- data/lib/split/dashboard/public/jquery-1.11.1.min.js +4 -0
- data/lib/split/dashboard/public/reset.css +48 -0
- data/lib/split/dashboard/public/style.css +328 -0
- data/lib/split/dashboard/views/_controls.erb +18 -0
- data/lib/split/dashboard/views/_experiment.erb +155 -0
- data/lib/split/dashboard/views/_experiment_with_goal_header.erb +8 -0
- data/lib/split/dashboard/views/index.erb +26 -0
- data/lib/split/dashboard/views/layout.erb +27 -0
- data/lib/split/encapsulated_helper.rb +42 -0
- data/lib/split/engine.rb +15 -0
- data/lib/split/exceptions.rb +6 -0
- data/lib/split/experiment.rb +486 -0
- data/lib/split/experiment_catalog.rb +51 -0
- data/lib/split/extensions/string.rb +16 -0
- data/lib/split/goals_collection.rb +45 -0
- data/lib/split/helper.rb +165 -0
- data/lib/split/metric.rb +101 -0
- data/lib/split/persistence.rb +28 -0
- data/lib/split/persistence/cookie_adapter.rb +94 -0
- data/lib/split/persistence/dual_adapter.rb +85 -0
- data/lib/split/persistence/redis_adapter.rb +57 -0
- data/lib/split/persistence/session_adapter.rb +29 -0
- data/lib/split/redis_interface.rb +50 -0
- data/lib/split/trial.rb +117 -0
- data/lib/split/user.rb +69 -0
- data/lib/split/version.rb +7 -0
- data/lib/split/zscore.rb +57 -0
- data/spec/algorithms/block_randomization_spec.rb +32 -0
- data/spec/algorithms/weighted_sample_spec.rb +19 -0
- data/spec/algorithms/whiplash_spec.rb +24 -0
- data/spec/alternative_spec.rb +320 -0
- data/spec/combined_experiments_helper_spec.rb +57 -0
- data/spec/configuration_spec.rb +258 -0
- data/spec/dashboard/pagination_helpers_spec.rb +200 -0
- data/spec/dashboard/paginator_spec.rb +37 -0
- data/spec/dashboard_helpers_spec.rb +42 -0
- data/spec/dashboard_spec.rb +210 -0
- data/spec/encapsulated_helper_spec.rb +52 -0
- data/spec/experiment_catalog_spec.rb +53 -0
- data/spec/experiment_spec.rb +533 -0
- data/spec/goals_collection_spec.rb +80 -0
- data/spec/helper_spec.rb +1111 -0
- data/spec/metric_spec.rb +31 -0
- data/spec/persistence/cookie_adapter_spec.rb +106 -0
- data/spec/persistence/dual_adapter_spec.rb +194 -0
- data/spec/persistence/redis_adapter_spec.rb +90 -0
- data/spec/persistence/session_adapter_spec.rb +32 -0
- data/spec/persistence_spec.rb +34 -0
- data/spec/redis_interface_spec.rb +111 -0
- data/spec/spec_helper.rb +52 -0
- data/spec/split_spec.rb +43 -0
- data/spec/support/cookies_mock.rb +20 -0
- data/spec/trial_spec.rb +299 -0
- data/spec/user_spec.rb +87 -0
- metadata +322 -0
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Split
|
3
|
+
module CombinedExperimentsHelper
|
4
|
+
def ab_combined_test(metric_descriptor, control = nil, *alternatives)
|
5
|
+
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
|
+
|
8
|
+
alternative = nil
|
9
|
+
weighted_alternatives = nil
|
10
|
+
experiment[:combined_experiments].each do |combined_experiment|
|
11
|
+
if alternative.nil?
|
12
|
+
if control
|
13
|
+
alternative = ab_test(combined_experiment, control, alternatives)
|
14
|
+
else
|
15
|
+
normalized_alternatives = Split::Configuration.new.normalize_alternatives(experiment[:alternatives])
|
16
|
+
alternative = ab_test(combined_experiment, normalized_alternatives[0], *normalized_alternatives[1])
|
17
|
+
end
|
18
|
+
else
|
19
|
+
weighted_alternatives ||= experiment[:alternatives].each_with_object({}) do |alt, memo|
|
20
|
+
alt = Alternative.new(alt, experiment[:name]).name
|
21
|
+
memo[alt] = (alt == alternative ? 1 : 0)
|
22
|
+
end
|
23
|
+
|
24
|
+
ab_test(combined_experiment, [weighted_alternatives])
|
25
|
+
end
|
26
|
+
end
|
27
|
+
alternative
|
28
|
+
end
|
29
|
+
|
30
|
+
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]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,255 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Split
|
3
|
+
class Configuration
|
4
|
+
attr_accessor :ignore_ip_addresses
|
5
|
+
attr_accessor :ignore_filter
|
6
|
+
attr_accessor :db_failover
|
7
|
+
attr_accessor :db_failover_on_db_error
|
8
|
+
attr_accessor :db_failover_allow_parameter_override
|
9
|
+
attr_accessor :allow_multiple_experiments
|
10
|
+
attr_accessor :enabled
|
11
|
+
attr_accessor :persistence
|
12
|
+
attr_accessor :persistence_cookie_length
|
13
|
+
attr_accessor :algorithm
|
14
|
+
attr_accessor :store_override
|
15
|
+
attr_accessor :start_manually
|
16
|
+
attr_accessor :reset_manually
|
17
|
+
attr_accessor :on_trial
|
18
|
+
attr_accessor :on_trial_choose
|
19
|
+
attr_accessor :on_trial_complete
|
20
|
+
attr_accessor :on_experiment_reset
|
21
|
+
attr_accessor :on_experiment_delete
|
22
|
+
attr_accessor :on_before_experiment_reset
|
23
|
+
attr_accessor :on_before_experiment_delete
|
24
|
+
attr_accessor :include_rails_helper
|
25
|
+
attr_accessor :beta_probability_simulations
|
26
|
+
attr_accessor :winning_alternative_recalculation_interval
|
27
|
+
attr_accessor :redis
|
28
|
+
attr_accessor :dashboard_pagination_default_per_page
|
29
|
+
|
30
|
+
attr_reader :experiments
|
31
|
+
|
32
|
+
attr_writer :bots
|
33
|
+
attr_writer :robot_regex
|
34
|
+
|
35
|
+
def bots
|
36
|
+
@bots ||= {
|
37
|
+
# 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',
|
58
|
+
|
59
|
+
# 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',
|
75
|
+
|
76
|
+
# 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',
|
95
|
+
|
96
|
+
# 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',
|
104
|
+
|
105
|
+
# ???
|
106
|
+
'DigitalPersona Fingerprint Software' => 'HP Fingerprint scanner',
|
107
|
+
'ShowyouBot' => 'Showyou iOS app spider',
|
108
|
+
'ZyBorg' => 'Zyborg? Hmmm....',
|
109
|
+
'ELB-HealthChecker' => 'ELB Health Check'
|
110
|
+
}
|
111
|
+
end
|
112
|
+
|
113
|
+
def experiments= experiments
|
114
|
+
raise InvalidExperimentsFormatError.new('Experiments must be a Hash') unless experiments.respond_to?(:keys)
|
115
|
+
@experiments = experiments
|
116
|
+
end
|
117
|
+
|
118
|
+
def disabled?
|
119
|
+
!enabled
|
120
|
+
end
|
121
|
+
|
122
|
+
def experiment_for(name)
|
123
|
+
if normalized_experiments
|
124
|
+
# TODO symbols
|
125
|
+
normalized_experiments[name.to_sym]
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def metrics
|
130
|
+
return @metrics if defined?(@metrics)
|
131
|
+
@metrics = {}
|
132
|
+
if self.experiments
|
133
|
+
self.experiments.each do |key, value|
|
134
|
+
metrics = value_for(value, :metric) rescue nil
|
135
|
+
Array(metrics).each do |metric_name|
|
136
|
+
if metric_name
|
137
|
+
@metrics[metric_name.to_sym] ||= []
|
138
|
+
@metrics[metric_name.to_sym] << Split::Experiment.new(key)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
@metrics
|
144
|
+
end
|
145
|
+
|
146
|
+
def normalized_experiments
|
147
|
+
return nil if @experiments.nil?
|
148
|
+
|
149
|
+
experiment_config = {}
|
150
|
+
@experiments.keys.each do |name|
|
151
|
+
experiment_config[name.to_sym] = {}
|
152
|
+
end
|
153
|
+
|
154
|
+
@experiments.each do |experiment_name, settings|
|
155
|
+
alternatives = if (alts = value_for(settings, :alternatives))
|
156
|
+
normalize_alternatives(alts)
|
157
|
+
end
|
158
|
+
|
159
|
+
experiment_data = {
|
160
|
+
alternatives: alternatives,
|
161
|
+
goals: value_for(settings, :goals),
|
162
|
+
metadata: value_for(settings, :metadata),
|
163
|
+
algorithm: value_for(settings, :algorithm),
|
164
|
+
resettable: value_for(settings, :resettable)
|
165
|
+
}
|
166
|
+
|
167
|
+
experiment_data.each do |name, value|
|
168
|
+
experiment_config[experiment_name.to_sym][name] = value if value != nil
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
experiment_config
|
173
|
+
end
|
174
|
+
|
175
|
+
def normalize_alternatives(alternatives)
|
176
|
+
given_probability, num_with_probability = alternatives.inject([0,0]) do |a,v|
|
177
|
+
p, n = a
|
178
|
+
if percent = value_for(v, :percent)
|
179
|
+
[p + percent, n + 1]
|
180
|
+
else
|
181
|
+
a
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
num_without_probability = alternatives.length - num_with_probability
|
186
|
+
unassigned_probability = ((100.0 - given_probability) / num_without_probability / 100.0)
|
187
|
+
|
188
|
+
if num_with_probability.nonzero?
|
189
|
+
alternatives = alternatives.map do |v|
|
190
|
+
if (name = value_for(v, :name)) && (percent = value_for(v, :percent))
|
191
|
+
{ name => percent / 100.0 }
|
192
|
+
elsif name = value_for(v, :name)
|
193
|
+
{ name => unassigned_probability }
|
194
|
+
else
|
195
|
+
{ v => unassigned_probability }
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
[alternatives.shift, alternatives]
|
200
|
+
else
|
201
|
+
alternatives = alternatives.dup
|
202
|
+
[alternatives.shift, alternatives]
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def robot_regex
|
207
|
+
@robot_regex ||= /\b(?:#{escaped_bots.join('|')})\b|\A\W*\z/i
|
208
|
+
end
|
209
|
+
|
210
|
+
def initialize
|
211
|
+
@ignore_ip_addresses = []
|
212
|
+
@ignore_filter = proc{ |request| is_robot? || is_ignored_ip_address? }
|
213
|
+
@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|}
|
219
|
+
@db_failover_allow_parameter_override = false
|
220
|
+
@allow_multiple_experiments = false
|
221
|
+
@enabled = true
|
222
|
+
@experiments = {}
|
223
|
+
@persistence = Split::Persistence::SessionAdapter
|
224
|
+
@persistence_cookie_length = 31536000 # One year from now
|
225
|
+
@algorithm = Split::Algorithms::WeightedSample
|
226
|
+
@include_rails_helper = true
|
227
|
+
@beta_probability_simulations = 10000
|
228
|
+
@winning_alternative_recalculation_interval = 60 * 60 * 24 # 1 day
|
229
|
+
@redis = ENV.fetch(ENV.fetch('REDIS_PROVIDER', 'REDIS_URL'), 'redis://localhost:6379')
|
230
|
+
@dashboard_pagination_default_per_page = 10
|
231
|
+
end
|
232
|
+
|
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
|
+
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]
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
def escaped_bots
|
252
|
+
bots.map { |key, _| Regexp.escape(key) }
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'sinatra/base'
|
3
|
+
require 'split'
|
4
|
+
require 'bigdecimal'
|
5
|
+
require 'split/dashboard/helpers'
|
6
|
+
require 'split/dashboard/pagination_helpers'
|
7
|
+
|
8
|
+
module Split
|
9
|
+
class Dashboard < Sinatra::Base
|
10
|
+
dir = File.dirname(File.expand_path(__FILE__))
|
11
|
+
|
12
|
+
set :views, "#{dir}/dashboard/views"
|
13
|
+
set :public_folder, "#{dir}/dashboard/public"
|
14
|
+
set :static, true
|
15
|
+
set :method_override, true
|
16
|
+
|
17
|
+
helpers Split::DashboardHelpers
|
18
|
+
helpers Split::DashboardPaginationHelpers
|
19
|
+
|
20
|
+
get '/' do
|
21
|
+
# Display experiments without a winner at the top of the dashboard
|
22
|
+
@experiments = Split::ExperimentCatalog.all_active_first
|
23
|
+
|
24
|
+
@metrics = Split::Metric.all
|
25
|
+
|
26
|
+
# Display Rails Environment mode (or Rack version if not using Rails)
|
27
|
+
if Object.const_defined?('Rails')
|
28
|
+
@current_env = Rails.env.titlecase
|
29
|
+
else
|
30
|
+
@current_env = "Rack: #{Rack.version}"
|
31
|
+
end
|
32
|
+
erb :index
|
33
|
+
end
|
34
|
+
|
35
|
+
post '/force_alternative' do
|
36
|
+
experiment = Split::ExperimentCatalog.find(params[:experiment])
|
37
|
+
alternative = Split::Alternative.new(params[:alternative], experiment.name)
|
38
|
+
alternative.increment_participation
|
39
|
+
Split::User.new(self)[experiment.key] = alternative.name
|
40
|
+
redirect url('/')
|
41
|
+
end
|
42
|
+
|
43
|
+
post '/experiment' do
|
44
|
+
@experiment = Split::ExperimentCatalog.find(params[:experiment])
|
45
|
+
@alternative = Split::Alternative.new(params[:alternative], params[:experiment])
|
46
|
+
@experiment.winner = @alternative.name
|
47
|
+
redirect url('/')
|
48
|
+
end
|
49
|
+
|
50
|
+
post '/start' do
|
51
|
+
@experiment = Split::ExperimentCatalog.find(params[:experiment])
|
52
|
+
@experiment.start
|
53
|
+
redirect url('/')
|
54
|
+
end
|
55
|
+
|
56
|
+
post '/reset' do
|
57
|
+
@experiment = Split::ExperimentCatalog.find(params[:experiment])
|
58
|
+
@experiment.reset
|
59
|
+
redirect url('/')
|
60
|
+
end
|
61
|
+
|
62
|
+
post '/reopen' do
|
63
|
+
@experiment = Split::ExperimentCatalog.find(params[:experiment])
|
64
|
+
@experiment.reset_winner
|
65
|
+
redirect url('/')
|
66
|
+
end
|
67
|
+
|
68
|
+
delete '/experiment' do
|
69
|
+
@experiment = Split::ExperimentCatalog.find(params[:experiment])
|
70
|
+
@experiment.delete
|
71
|
+
redirect url('/')
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Split
|
3
|
+
module DashboardHelpers
|
4
|
+
def h(text)
|
5
|
+
Rack::Utils.escape_html(text)
|
6
|
+
end
|
7
|
+
|
8
|
+
def url(*path_parts)
|
9
|
+
[ path_prefix, path_parts ].join("/").squeeze('/')
|
10
|
+
end
|
11
|
+
|
12
|
+
def path_prefix
|
13
|
+
request.env['SCRIPT_NAME']
|
14
|
+
end
|
15
|
+
|
16
|
+
def number_to_percentage(number, precision = 2)
|
17
|
+
round(number * 100)
|
18
|
+
end
|
19
|
+
|
20
|
+
def round(number, precision = 2)
|
21
|
+
begin
|
22
|
+
BigDecimal(number.to_s)
|
23
|
+
rescue ArgumentError
|
24
|
+
BigDecimal(0)
|
25
|
+
end.round(precision).to_f
|
26
|
+
end
|
27
|
+
|
28
|
+
def confidence_level(z_score)
|
29
|
+
return z_score if z_score.is_a? String
|
30
|
+
|
31
|
+
z = round(z_score.to_s.to_f, 3).abs
|
32
|
+
|
33
|
+
if z >= 2.58
|
34
|
+
'99% confidence'
|
35
|
+
elsif z >= 1.96
|
36
|
+
'95% confidence'
|
37
|
+
elsif z >= 1.65
|
38
|
+
'90% confidence'
|
39
|
+
else
|
40
|
+
'Insufficient confidence'
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'split/dashboard/paginator'
|
3
|
+
|
4
|
+
module Split
|
5
|
+
module DashboardPaginationHelpers
|
6
|
+
def pagination_per
|
7
|
+
default_per_page = Split.configuration.dashboard_pagination_default_per_page
|
8
|
+
@pagination_per ||= (params[:per] || default_per_page).to_i
|
9
|
+
end
|
10
|
+
|
11
|
+
def page_number
|
12
|
+
@page_number ||= (params[:page] || 1).to_i
|
13
|
+
end
|
14
|
+
|
15
|
+
def paginated(collection)
|
16
|
+
Split::DashboardPaginator.new(collection, page_number, pagination_per).paginate
|
17
|
+
end
|
18
|
+
|
19
|
+
def pagination(collection)
|
20
|
+
html = []
|
21
|
+
html << first_page_tag if show_first_page_tag?
|
22
|
+
html << ellipsis_tag if show_first_ellipsis_tag?
|
23
|
+
html << prev_page_tag if show_prev_page_tag?
|
24
|
+
html << current_page_tag
|
25
|
+
html << next_page_tag if show_next_page_tag?(collection)
|
26
|
+
html << ellipsis_tag if show_last_ellipsis_tag?(collection)
|
27
|
+
html << last_page_tag(collection) if show_last_page_tag?(collection)
|
28
|
+
html.join
|
29
|
+
end
|
30
|
+
|
31
|
+
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
|
85
|
+
end
|
86
|
+
end
|