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.
- checksums.yaml +4 -4
- data/.github/FUNDING.yml +1 -0
- data/.github/dependabot.yml +7 -0
- data/.github/workflows/ci.yml +76 -0
- data/.rubocop.yml +177 -4
- data/CHANGELOG.md +87 -0
- data/CONTRIBUTING.md +1 -1
- data/Gemfile +2 -1
- data/README.md +37 -9
- data/Rakefile +5 -5
- data/gemfiles/5.2.gemfile +1 -3
- data/gemfiles/6.0.gemfile +1 -3
- data/gemfiles/{5.0.gemfile → 6.1.gemfile} +2 -4
- data/gemfiles/{5.1.gemfile → 7.0.gemfile} +2 -4
- data/lib/split/algorithms/block_randomization.rb +6 -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 +22 -22
- data/lib/split/cache.rb +27 -0
- data/lib/split/combined_experiments_helper.rb +5 -4
- data/lib/split/configuration.rb +89 -94
- data/lib/split/dashboard/helpers.rb +7 -7
- data/lib/split/dashboard/pagination_helpers.rb +54 -54
- 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.rb +42 -21
- data/lib/split/encapsulated_helper.rb +15 -8
- data/lib/split/engine.rb +1 -0
- data/lib/split/exceptions.rb +1 -0
- data/lib/split/experiment.rb +151 -124
- data/lib/split/experiment_catalog.rb +7 -8
- data/lib/split/extensions/string.rb +2 -1
- data/lib/split/goals_collection.rb +9 -10
- data/lib/split/helper.rb +50 -23
- data/lib/split/metric.rb +6 -6
- data/lib/split/persistence/cookie_adapter.rb +46 -44
- data/lib/split/persistence/dual_adapter.rb +7 -8
- 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 +15 -29
- data/lib/split/trial.rb +43 -34
- data/lib/split/user.rb +25 -14
- data/lib/split/version.rb +2 -4
- data/lib/split/zscore.rb +2 -3
- data/lib/split.rb +34 -27
- 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 +69 -67
- data/spec/dashboard/paginator_spec.rb +10 -9
- data/spec/dashboard_helpers_spec.rb +19 -18
- data/spec/dashboard_spec.rb +122 -38
- data/spec/encapsulated_helper_spec.rb +46 -22
- data/spec/experiment_catalog_spec.rb +14 -13
- data/spec/experiment_spec.rb +198 -118
- data/spec/goals_collection_spec.rb +18 -16
- data/spec/helper_spec.rb +454 -385
- data/spec/metric_spec.rb +14 -14
- data/spec/persistence/cookie_adapter_spec.rb +26 -11
- data/spec/persistence/dual_adapter_spec.rb +71 -71
- 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 +35 -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 +60 -29
- data/split.gemspec +22 -21
- metadata +43 -40
- data/.rubocop_todo.yml +0 -679
- data/.travis.yml +0 -60
- data/Appraisals +0 -19
- data/gemfiles/4.2.gemfile +0 -9
data/lib/split/alternative.rb
CHANGED
|
@@ -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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
156
|
-
Split.redis.hsetnx key,
|
|
157
|
-
Split.redis.hsetnx key,
|
|
158
|
-
Split.redis.hsetnx key,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
data/lib/split/cache.rb
ADDED
|
@@ -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,
|
|
32
|
-
raise(Split::InvalidExperimentsFormatError,
|
|
33
|
-
raise(Split::InvalidExperimentsFormatError,
|
|
34
|
-
Split
|
|
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,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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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=
|
|
114
|
-
raise InvalidExperimentsFormatError.new(
|
|
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
|
-
|
|
157
|
-
|
|
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(
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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[
|
|
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,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
|
|
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
|
-
|
|
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
|
-
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
|
|
@@ -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
|
+
}
|