split 3.2.0 → 4.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- 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 +174 -0
- data/CODE_OF_CONDUCT.md +3 -3
- data/CONTRIBUTING.md +1 -1
- data/Gemfile +6 -1
- data/README.md +79 -33
- 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 +6 -5
- data/lib/split/configuration.rb +94 -91
- data/lib/split/dashboard/helpers.rb +9 -9
- data/lib/split/dashboard/pagination_helpers.rb +86 -0
- data/lib/split/dashboard/paginator.rb +17 -0
- data/lib/split/dashboard/public/dashboard.js +10 -0
- data/lib/split/dashboard/public/style.css +19 -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 +24 -5
- data/lib/split/dashboard/views/layout.erb +1 -1
- data/lib/split/dashboard.rb +47 -20
- 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 +56 -24
- data/lib/split/metric.rb +6 -6
- data/lib/split/persistence/cookie_adapter.rb +52 -15
- 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 -31
- data/lib/split/trial.rb +48 -41
- 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 +39 -25
- 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 +202 -0
- data/spec/dashboard/paginator_spec.rb +38 -0
- 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 +539 -419
- data/spec/metric_spec.rb +14 -14
- data/spec/persistence/cookie_adapter_spec.rb +105 -27
- 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 +18 -18
- data/spec/support/cookies_mock.rb +1 -2
- data/spec/trial_spec.rb +117 -70
- data/spec/user_spec.rb +69 -27
- data/split.gemspec +26 -22
- metadata +85 -37
- data/.travis.yml +0 -41
- data/Appraisals +0 -13
- data/gemfiles/4.2.gemfile +0 -9
- data/gemfiles/5.0.gemfile +0 -10
- data/gemfiles/5.1.gemfile +0 -10
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
|
|
@@ -15,7 +16,7 @@ module Split
|
|
|
15
16
|
@name = name
|
|
16
17
|
@weight = 1
|
|
17
18
|
end
|
|
18
|
-
p_winner = 0.0
|
|
19
|
+
@p_winner = 0.0
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
def to_s
|
|
@@ -37,11 +38,11 @@ module Split
|
|
|
37
38
|
end
|
|
38
39
|
|
|
39
40
|
def participant_count
|
|
40
|
-
Split.redis.hget(key,
|
|
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,22 +67,22 @@ 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
|
-
def set_completed_count
|
|
79
|
+
def set_completed_count(count, goal = nil)
|
|
79
80
|
field = set_field(goal)
|
|
80
81
|
Split.redis.hset(key, field, count.to_i)
|
|
81
82
|
end
|
|
82
83
|
|
|
83
84
|
def increment_participation
|
|
84
|
-
Split.redis.hincrby key,
|
|
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,9 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module Split
|
|
3
4
|
module CombinedExperimentsHelper
|
|
4
5
|
def ab_combined_test(metric_descriptor, control = nil, *alternatives)
|
|
5
6
|
return nil unless experiment = find_combined_experiment(metric_descriptor)
|
|
6
|
-
raise(Split::InvalidExperimentsFormatError,
|
|
7
|
+
raise(Split::InvalidExperimentsFormatError, "Unable to find experiment #{metric_descriptor} in configuration") if experiment[:combined_experiments].nil?
|
|
7
8
|
|
|
8
9
|
alternative = nil
|
|
9
10
|
weighted_alternatives = nil
|
|
@@ -28,10 +29,10 @@ module Split
|
|
|
28
29
|
end
|
|
29
30
|
|
|
30
31
|
def find_combined_experiment(metric_descriptor)
|
|
31
|
-
raise(Split::InvalidExperimentsFormatError,
|
|
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,89 +22,100 @@ module Split
|
|
|
22
22
|
attr_accessor :on_experiment_reset
|
|
23
23
|
attr_accessor :on_experiment_delete
|
|
24
24
|
attr_accessor :on_before_experiment_reset
|
|
25
|
+
attr_accessor :on_experiment_winner_choose
|
|
25
26
|
attr_accessor :on_before_experiment_delete
|
|
26
27
|
attr_accessor :include_rails_helper
|
|
27
28
|
attr_accessor :beta_probability_simulations
|
|
28
29
|
attr_accessor :winning_alternative_recalculation_interval
|
|
29
30
|
attr_accessor :redis
|
|
31
|
+
attr_accessor :dashboard_pagination_default_per_page
|
|
32
|
+
attr_accessor :cache
|
|
30
33
|
|
|
31
34
|
attr_reader :experiments
|
|
32
35
|
|
|
36
|
+
attr_writer :bots
|
|
37
|
+
attr_writer :robot_regex
|
|
38
|
+
|
|
33
39
|
def bots
|
|
34
40
|
@bots ||= {
|
|
35
41
|
# Indexers
|
|
36
|
-
|
|
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
|
-
|
|
64
|
+
"Apache-HttpClient" => "Java http library",
|
|
65
|
+
"AppEngine-Google" => "Google App Engine",
|
|
66
|
+
"curl" => "curl unix CLI http client",
|
|
67
|
+
"ColdFusion" => "ColdFusion http library",
|
|
68
|
+
"EventMachine HttpClient" => "Ruby http library",
|
|
69
|
+
"Go http package" => "Go http library",
|
|
70
|
+
"Go-http-client" => "Go http library",
|
|
71
|
+
"Java" => "Generic Java http library",
|
|
72
|
+
"libwww-perl" => "Perl client-server library loved by script kids",
|
|
73
|
+
"lwp-trivial" => "Another Perl library loved by script kids",
|
|
74
|
+
"Python-urllib" => "Python http library",
|
|
75
|
+
"PycURL" => "Python http library",
|
|
76
|
+
"Test Certificate Info" => "C http library?",
|
|
77
|
+
"Typhoeus" => "Ruby http library",
|
|
78
|
+
"Wget" => "wget unix CLI http client",
|
|
71
79
|
|
|
72
80
|
# URL expanders / previewers
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
81
|
+
"awe.sm" => "Awe.sm URL expander",
|
|
82
|
+
"bitlybot" => "bit.ly bot",
|
|
83
|
+
"bot@linkfluence.net" => "Linkfluence bot",
|
|
84
|
+
"facebookexternalhit" => "facebook bot",
|
|
85
|
+
"Facebot" => "Facebook crawler",
|
|
86
|
+
"Feedfetcher-Google" => "Google Feedfetcher",
|
|
87
|
+
"https://developers.google.com/+/web/snippet" => "Google+ Snippet Fetcher",
|
|
88
|
+
"LinkedInBot" => "LinkedIn bot",
|
|
89
|
+
"LongURL" => "URL expander service",
|
|
90
|
+
"NING" => "NING - Yet Another Twitter Swarmer",
|
|
91
|
+
"Pinterestbot" => "Pinterest Bot",
|
|
92
|
+
"redditbot" => "Reddit Bot",
|
|
93
|
+
"ShortLinkTranslate" => "Link shortener",
|
|
94
|
+
"Slackbot" => "Slackbot link expander",
|
|
95
|
+
"TweetmemeBot" => "TweetMeMe Crawler",
|
|
96
|
+
"Twitterbot" => "Twitter URL expander",
|
|
97
|
+
"UnwindFetch" => "Gnip URL expander",
|
|
98
|
+
"vkShare" => "VKontake Sharer",
|
|
90
99
|
|
|
91
100
|
# Uptime monitoring
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
101
|
+
"check_http" => "Nagios monitor",
|
|
102
|
+
"GoogleStackdriverMonitoring" => "Google Cloud monitor",
|
|
103
|
+
"NewRelicPinger" => "NewRelic monitor",
|
|
104
|
+
"Panopta" => "Monitoring service",
|
|
105
|
+
"Pingdom" => "Pingdom monitoring",
|
|
106
|
+
"SiteUptime" => "Site monitoring services",
|
|
107
|
+
"UptimeRobot" => "Monitoring service",
|
|
97
108
|
|
|
98
109
|
# ???
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
110
|
+
"DigitalPersona Fingerprint Software" => "HP Fingerprint scanner",
|
|
111
|
+
"ShowyouBot" => "Showyou iOS app spider",
|
|
112
|
+
"ZyBorg" => "Zyborg? Hmmm....",
|
|
113
|
+
"ELB-HealthChecker" => "ELB Health Check"
|
|
103
114
|
}
|
|
104
115
|
end
|
|
105
116
|
|
|
106
|
-
def experiments=
|
|
107
|
-
raise InvalidExperimentsFormatError.new(
|
|
117
|
+
def experiments=(experiments)
|
|
118
|
+
raise InvalidExperimentsFormatError.new("Experiments must be a Hash") unless experiments.respond_to?(:keys)
|
|
108
119
|
@experiments = experiments
|
|
109
120
|
end
|
|
110
121
|
|
|
@@ -146,8 +157,8 @@ module Split
|
|
|
146
157
|
|
|
147
158
|
@experiments.each do |experiment_name, settings|
|
|
148
159
|
alternatives = if (alts = value_for(settings, :alternatives))
|
|
149
|
-
|
|
150
|
-
|
|
160
|
+
normalize_alternatives(alts)
|
|
161
|
+
end
|
|
151
162
|
|
|
152
163
|
experiment_data = {
|
|
153
164
|
alternatives: alternatives,
|
|
@@ -166,7 +177,7 @@ module Split
|
|
|
166
177
|
end
|
|
167
178
|
|
|
168
179
|
def normalize_alternatives(alternatives)
|
|
169
|
-
given_probability, num_with_probability = alternatives.inject([0,0]) do |a,v|
|
|
180
|
+
given_probability, num_with_probability = alternatives.inject([0, 0]) do |a, v|
|
|
170
181
|
p, n = a
|
|
171
182
|
if percent = value_for(v, :percent)
|
|
172
183
|
[p + percent, n + 1]
|
|
@@ -202,46 +213,38 @@ module Split
|
|
|
202
213
|
|
|
203
214
|
def initialize
|
|
204
215
|
@ignore_ip_addresses = []
|
|
205
|
-
@ignore_filter = proc{ |request| is_robot? || is_ignored_ip_address? }
|
|
216
|
+
@ignore_filter = proc { |request| is_robot? || is_ignored_ip_address? }
|
|
206
217
|
@db_failover = false
|
|
207
|
-
@db_failover_on_db_error = proc{|error|} # e.g. use Rails logger here
|
|
208
|
-
@on_experiment_reset = proc{|experiment|}
|
|
209
|
-
@on_experiment_delete = proc{|experiment|}
|
|
210
|
-
@on_before_experiment_reset = proc{|experiment|}
|
|
211
|
-
@on_before_experiment_delete = proc{|experiment|}
|
|
218
|
+
@db_failover_on_db_error = proc { |error| } # e.g. use Rails logger here
|
|
219
|
+
@on_experiment_reset = proc { |experiment| }
|
|
220
|
+
@on_experiment_delete = proc { |experiment| }
|
|
221
|
+
@on_before_experiment_reset = proc { |experiment| }
|
|
222
|
+
@on_before_experiment_delete = proc { |experiment| }
|
|
223
|
+
@on_experiment_winner_choose = proc { |experiment| }
|
|
212
224
|
@db_failover_allow_parameter_override = false
|
|
213
225
|
@allow_multiple_experiments = false
|
|
214
226
|
@enabled = true
|
|
215
227
|
@experiments = {}
|
|
216
228
|
@persistence = Split::Persistence::SessionAdapter
|
|
217
229
|
@persistence_cookie_length = 31536000 # One year from now
|
|
230
|
+
@persistence_cookie_domain = nil
|
|
218
231
|
@algorithm = Split::Algorithms::WeightedSample
|
|
219
232
|
@include_rails_helper = true
|
|
220
233
|
@beta_probability_simulations = 10000
|
|
221
234
|
@winning_alternative_recalculation_interval = 60 * 60 * 24 # 1 day
|
|
222
|
-
@redis = ENV.fetch(ENV.fetch(
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
def redis_url=(value)
|
|
226
|
-
warn '[DEPRECATED] `redis_url=` is deprecated in favor of `redis=`'
|
|
227
|
-
self.redis = value
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
def redis_url
|
|
231
|
-
warn '[DEPRECATED] `redis_url` is deprecated in favor of `redis`'
|
|
232
|
-
self.redis
|
|
235
|
+
@redis = ENV.fetch(ENV.fetch("REDIS_PROVIDER", "REDIS_URL"), "redis://localhost:6379")
|
|
236
|
+
@dashboard_pagination_default_per_page = 10
|
|
233
237
|
end
|
|
234
238
|
|
|
235
239
|
private
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
+
def value_for(hash, key)
|
|
241
|
+
if hash.kind_of?(Hash)
|
|
242
|
+
hash.has_key?(key.to_s) ? hash[key.to_s] : hash[key.to_sym]
|
|
243
|
+
end
|
|
240
244
|
end
|
|
241
|
-
end
|
|
242
245
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
+
def escaped_bots
|
|
247
|
+
bots.map { |key, _| Regexp.escape(key) }
|
|
248
|
+
end
|
|
246
249
|
end
|
|
247
250
|
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module Split
|
|
3
4
|
module DashboardHelpers
|
|
4
5
|
def h(text)
|
|
@@ -6,11 +7,11 @@ module Split
|
|
|
6
7
|
end
|
|
7
8
|
|
|
8
9
|
def url(*path_parts)
|
|
9
|
-
[ path_prefix, path_parts ].join("/").squeeze(
|
|
10
|
+
[ path_prefix, path_parts ].join("/").squeeze("/")
|
|
10
11
|
end
|
|
11
12
|
|
|
12
13
|
def path_prefix
|
|
13
|
-
request.env[
|
|
14
|
+
request.env["SCRIPT_NAME"]
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
def number_to_percentage(number, precision = 2)
|
|
@@ -19,9 +20,9 @@ module Split
|
|
|
19
20
|
|
|
20
21
|
def round(number, precision = 2)
|
|
21
22
|
begin
|
|
22
|
-
BigDecimal
|
|
23
|
+
BigDecimal(number.to_s)
|
|
23
24
|
rescue ArgumentError
|
|
24
|
-
BigDecimal
|
|
25
|
+
BigDecimal(0)
|
|
25
26
|
end.round(precision).to_f
|
|
26
27
|
end
|
|
27
28
|
|
|
@@ -31,15 +32,14 @@ module Split
|
|
|
31
32
|
z = round(z_score.to_s.to_f, 3).abs
|
|
32
33
|
|
|
33
34
|
if z >= 2.58
|
|
34
|
-
|
|
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
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "split/dashboard/paginator"
|
|
4
|
+
|
|
5
|
+
module Split
|
|
6
|
+
module DashboardPaginationHelpers
|
|
7
|
+
def pagination_per
|
|
8
|
+
default_per_page = Split.configuration.dashboard_pagination_default_per_page
|
|
9
|
+
@pagination_per ||= (params[:per] || default_per_page).to_i
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def page_number
|
|
13
|
+
@page_number ||= (params[:page] || 1).to_i
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def paginated(collection)
|
|
17
|
+
Split::DashboardPaginator.new(collection, page_number, pagination_per).paginate
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def pagination(collection)
|
|
21
|
+
html = []
|
|
22
|
+
html << first_page_tag if show_first_page_tag?
|
|
23
|
+
html << ellipsis_tag if show_first_ellipsis_tag?
|
|
24
|
+
html << prev_page_tag if show_prev_page_tag?
|
|
25
|
+
html << current_page_tag
|
|
26
|
+
html << next_page_tag if show_next_page_tag?(collection)
|
|
27
|
+
html << ellipsis_tag if show_last_ellipsis_tag?(collection)
|
|
28
|
+
html << last_page_tag(collection) if show_last_page_tag?(collection)
|
|
29
|
+
html.join
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
def show_first_page_tag?
|
|
34
|
+
page_number > 2
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def first_page_tag
|
|
38
|
+
%Q(<a href="#{url.chop}?page=1&per=#{pagination_per}">1</a>)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def show_first_ellipsis_tag?
|
|
42
|
+
page_number >= 4
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def ellipsis_tag
|
|
46
|
+
"<span>...</span>"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def show_prev_page_tag?
|
|
50
|
+
page_number > 1
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def prev_page_tag
|
|
54
|
+
%Q(<a href="#{url.chop}?page=#{page_number - 1}&per=#{pagination_per}">#{page_number - 1}</a>)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def current_page_tag
|
|
58
|
+
"<span><b>#{page_number}</b></span>"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def show_next_page_tag?(collection)
|
|
62
|
+
(page_number * pagination_per) < collection.count
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def next_page_tag
|
|
66
|
+
%Q(<a href="#{url.chop}?page=#{page_number + 1}&per=#{pagination_per}">#{page_number + 1}</a>)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def show_last_ellipsis_tag?(collection)
|
|
70
|
+
(total_pages(collection) - page_number) >= 3
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def total_pages(collection)
|
|
74
|
+
collection.count / pagination_per + ((collection.count % pagination_per).zero? ? 0 : 1)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def show_last_page_tag?(collection)
|
|
78
|
+
page_number < (total_pages(collection) - 1)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def last_page_tag(collection)
|
|
82
|
+
total = total_pages(collection)
|
|
83
|
+
%Q(<a href="#{url.chop}?page=#{total}&per=#{pagination_per}">#{total}</a>)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Split
|
|
4
|
+
class DashboardPaginator
|
|
5
|
+
def initialize(collection, page_number, per)
|
|
6
|
+
@collection = collection
|
|
7
|
+
@page_number = page_number
|
|
8
|
+
@per = per
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def paginate
|
|
12
|
+
to = @page_number * @per
|
|
13
|
+
from = to - @per
|
|
14
|
+
@collection[from...to]
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -22,3 +22,13 @@ function confirmReopen() {
|
|
|
22
22
|
var agree = confirm("This will reopen the experiment. Are you sure?");
|
|
23
23
|
return agree ? true : false;
|
|
24
24
|
}
|
|
25
|
+
|
|
26
|
+
function confirmEnableCohorting(){
|
|
27
|
+
var agree = confirm("This will enable the cohorting of the experiment. Are you sure?");
|
|
28
|
+
return agree ? true : false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function confirmDisableCohorting(){
|
|
32
|
+
var agree = confirm("This will disable the cohorting of the experiment. Note: Existing participants will continue to receive their alternative and may continue to convert. Are you sure?");
|
|
33
|
+
return agree ? true : false;
|
|
34
|
+
}
|
|
@@ -258,7 +258,7 @@ body {
|
|
|
258
258
|
color: #408C48;
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
-
a.button, button, input[type="submit"] {
|
|
261
|
+
.experiment a.button, .experiment button, .experiment input[type="submit"] {
|
|
262
262
|
padding: 4px 10px;
|
|
263
263
|
overflow: hidden;
|
|
264
264
|
background: #d8dae0;
|
|
@@ -312,8 +312,25 @@ a.button.green:focus, button.green:focus, input[type="submit"].green:focus {
|
|
|
312
312
|
background:#768E7A;
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
-
|
|
315
|
+
.dashboard-controls input, .dashboard-controls select {
|
|
316
316
|
padding: 10px;
|
|
317
317
|
}
|
|
318
318
|
|
|
319
|
+
.dashboard-controls-bottom {
|
|
320
|
+
margin-top: 10px;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.pagination {
|
|
324
|
+
text-align: center;
|
|
325
|
+
font-size: 15px;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.pagination a, .paginaton span {
|
|
329
|
+
display: inline-block;
|
|
330
|
+
padding: 5px;
|
|
331
|
+
}
|
|
319
332
|
|
|
333
|
+
.divider {
|
|
334
|
+
display: inline-block;
|
|
335
|
+
margin-left: 10px;
|
|
336
|
+
}
|