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.
- checksums.yaml +4 -4
- data/.eslintrc +1 -1
- data/.github/FUNDING.yml +1 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +24 -0
- data/.github/dependabot.yml +7 -0
- data/.github/workflows/ci.yml +63 -0
- data/.rspec +1 -0
- data/.rubocop.yml +67 -1043
- data/CHANGELOG.md +121 -0
- data/CODE_OF_CONDUCT.md +3 -3
- data/CONTRIBUTING.md +1 -1
- data/Gemfile +6 -1
- data/README.md +51 -21
- data/Rakefile +6 -5
- data/lib/split/algorithms/block_randomization.rb +7 -6
- data/lib/split/algorithms/weighted_sample.rb +2 -1
- data/lib/split/algorithms/whiplash.rb +17 -18
- data/lib/split/algorithms.rb +14 -0
- data/lib/split/alternative.rb +25 -25
- data/lib/split/cache.rb +27 -0
- data/lib/split/combined_experiments_helper.rb +5 -4
- data/lib/split/configuration.rb +94 -96
- data/lib/split/dashboard/helpers.rb +7 -7
- data/lib/split/dashboard/pagination_helpers.rb +56 -57
- data/lib/split/dashboard/paginator.rb +1 -0
- data/lib/split/dashboard/public/dashboard.js +10 -0
- data/lib/split/dashboard/public/style.css +10 -2
- data/lib/split/dashboard/views/_controls.erb +13 -0
- data/lib/split/dashboard/views/_experiment.erb +2 -1
- data/lib/split/dashboard/views/index.erb +19 -4
- data/lib/split/dashboard/views/layout.erb +1 -1
- data/lib/split/dashboard.rb +46 -21
- data/lib/split/encapsulated_helper.rb +15 -8
- data/lib/split/engine.rb +7 -4
- data/lib/split/exceptions.rb +1 -0
- data/lib/split/experiment.rb +160 -122
- data/lib/split/experiment_catalog.rb +7 -8
- data/lib/split/extensions/string.rb +2 -1
- data/lib/split/goals_collection.rb +10 -10
- data/lib/split/helper.rb +52 -24
- data/lib/split/metric.rb +6 -6
- data/lib/split/persistence/cookie_adapter.rb +47 -44
- data/lib/split/persistence/dual_adapter.rb +53 -12
- data/lib/split/persistence/redis_adapter.rb +8 -4
- data/lib/split/persistence/session_adapter.rb +1 -2
- data/lib/split/persistence.rb +8 -6
- data/lib/split/redis_interface.rb +16 -29
- data/lib/split/trial.rb +44 -35
- data/lib/split/user.rb +30 -15
- data/lib/split/version.rb +2 -4
- data/lib/split/zscore.rb +2 -3
- data/lib/split.rb +35 -28
- data/spec/algorithms/block_randomization_spec.rb +6 -5
- data/spec/algorithms/weighted_sample_spec.rb +6 -5
- data/spec/algorithms/whiplash_spec.rb +4 -5
- data/spec/alternative_spec.rb +35 -36
- data/spec/cache_spec.rb +84 -0
- data/spec/combined_experiments_helper_spec.rb +18 -17
- data/spec/configuration_spec.rb +41 -45
- data/spec/dashboard/pagination_helpers_spec.rb +71 -67
- data/spec/dashboard/paginator_spec.rb +10 -9
- data/spec/dashboard_helpers_spec.rb +19 -18
- data/spec/dashboard_spec.rb +153 -48
- data/spec/encapsulated_helper_spec.rb +47 -23
- data/spec/experiment_catalog_spec.rb +14 -13
- data/spec/experiment_spec.rb +224 -111
- data/spec/goals_collection_spec.rb +18 -16
- data/spec/helper_spec.rb +531 -424
- data/spec/metric_spec.rb +14 -14
- data/spec/persistence/cookie_adapter_spec.rb +26 -11
- data/spec/persistence/dual_adapter_spec.rb +158 -66
- data/spec/persistence/redis_adapter_spec.rb +35 -27
- data/spec/persistence/session_adapter_spec.rb +2 -3
- data/spec/persistence_spec.rb +1 -2
- data/spec/redis_interface_spec.rb +25 -82
- data/spec/spec_helper.rb +38 -24
- data/spec/split_spec.rb +11 -11
- data/spec/support/cookies_mock.rb +1 -2
- data/spec/trial_spec.rb +102 -75
- data/spec/user_spec.rb +69 -27
- data/split.gemspec +26 -23
- metadata +68 -42
- data/.travis.yml +0 -66
- data/Appraisals +0 -19
- data/gemfiles/4.2.gemfile +0 -9
- data/gemfiles/5.0.gemfile +0 -9
- data/gemfiles/5.1.gemfile +0 -9
- data/gemfiles/5.2.gemfile +0 -9
- 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,
|
|
32
|
-
raise(Split::InvalidExperimentsFormatError,
|
|
33
|
-
raise(Split::InvalidExperimentsFormatError,
|
|
34
|
-
|
|
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
|
data/lib/split/configuration.rb
CHANGED
|
@@ -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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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=
|
|
112
|
-
raise InvalidExperimentsFormatError.new(
|
|
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
|
-
|
|
155
|
-
|
|
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(
|
|
228
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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[
|
|
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
|
-
|
|
35
|
+
"99% confidence"
|
|
35
36
|
elsif z >= 1.96
|
|
36
|
-
|
|
37
|
+
"95% confidence"
|
|
37
38
|
elsif z >= 1.65
|
|
38
|
-
|
|
39
|
+
"90% confidence"
|
|
39
40
|
else
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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>
|