split 3.0.0 → 4.0.1
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 +71 -0
- data/.rspec +1 -0
- data/.rubocop.yml +71 -1044
- data/.rubocop_todo.yml +226 -0
- data/Appraisals +12 -1
- data/CHANGELOG.md +157 -0
- data/CODE_OF_CONDUCT.md +3 -3
- data/CONTRIBUTING.md +54 -5
- data/Gemfile +2 -0
- data/LICENSE +1 -1
- data/README.md +232 -121
- data/Rakefile +2 -0
- data/gemfiles/5.0.gemfile +1 -2
- data/gemfiles/{4.2.gemfile → 5.1.gemfile} +2 -2
- data/gemfiles/5.2.gemfile +9 -0
- data/gemfiles/6.0.gemfile +9 -0
- data/gemfiles/6.1.gemfile +9 -0
- data/gemfiles/7.0.gemfile +9 -0
- data/lib/split/algorithms/block_randomization.rb +2 -0
- data/lib/split/algorithms/weighted_sample.rb +2 -1
- data/lib/split/algorithms/whiplash.rb +3 -2
- data/lib/split/alternative.rb +7 -3
- data/lib/split/cache.rb +28 -0
- data/lib/split/combined_experiments_helper.rb +38 -0
- data/lib/split/configuration.rb +24 -13
- data/lib/split/dashboard/helpers.rb +3 -2
- data/lib/split/dashboard/pagination_helpers.rb +87 -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 +14 -0
- data/lib/split/dashboard/views/_controls.erb +13 -0
- data/lib/split/dashboard/views/index.erb +5 -1
- data/lib/split/dashboard/views/layout.erb +1 -1
- data/lib/split/dashboard.rb +21 -1
- data/lib/split/encapsulated_helper.rb +3 -2
- data/lib/split/engine.rb +7 -2
- data/lib/split/exceptions.rb +1 -0
- data/lib/split/experiment.rb +103 -69
- data/lib/split/experiment_catalog.rb +1 -3
- data/lib/split/extensions/string.rb +1 -0
- data/lib/split/goals_collection.rb +2 -0
- data/lib/split/helper.rb +42 -9
- data/lib/split/metric.rb +2 -1
- data/lib/split/persistence/cookie_adapter.rb +58 -15
- data/lib/split/persistence/dual_adapter.rb +54 -12
- data/lib/split/persistence/redis_adapter.rb +5 -0
- data/lib/split/persistence/session_adapter.rb +1 -0
- data/lib/split/persistence.rb +4 -2
- data/lib/split/redis_interface.rb +9 -30
- data/lib/split/trial.rb +25 -17
- data/lib/split/user.rb +20 -4
- data/lib/split/version.rb +2 -4
- data/lib/split/zscore.rb +1 -0
- data/lib/split.rb +17 -3
- data/spec/alternative_spec.rb +13 -1
- data/spec/cache_spec.rb +88 -0
- data/spec/combined_experiments_helper_spec.rb +57 -0
- data/spec/configuration_spec.rb +17 -15
- data/spec/dashboard/pagination_helpers_spec.rb +200 -0
- data/spec/dashboard/paginator_spec.rb +37 -0
- data/spec/dashboard_helpers_spec.rb +2 -2
- data/spec/dashboard_spec.rb +78 -17
- data/spec/encapsulated_helper_spec.rb +2 -2
- data/spec/experiment_spec.rb +117 -13
- data/spec/goals_collection_spec.rb +1 -1
- data/spec/helper_spec.rb +211 -112
- data/spec/persistence/cookie_adapter_spec.rb +90 -23
- data/spec/persistence/dual_adapter_spec.rb +160 -68
- data/spec/persistence/redis_adapter_spec.rb +9 -0
- data/spec/redis_interface_spec.rb +0 -69
- data/spec/spec_helper.rb +5 -6
- data/spec/split_spec.rb +7 -7
- data/spec/trial_spec.rb +65 -19
- data/spec/user_spec.rb +45 -3
- data/split.gemspec +20 -10
- metadata +61 -35
- data/.travis.yml +0 -16
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module Split
|
|
3
4
|
module Algorithms
|
|
4
5
|
module WeightedSample
|
|
@@ -8,7 +9,7 @@ module Split
|
|
|
8
9
|
total = weights.inject(:+)
|
|
9
10
|
point = rand * total
|
|
10
11
|
|
|
11
|
-
experiment.alternatives.zip(weights).each do |n,w|
|
|
12
|
+
experiment.alternatives.zip(weights).each do |n, w|
|
|
12
13
|
return n if w >= point
|
|
13
14
|
point -= w
|
|
14
15
|
end
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
# A multi-armed bandit implementation inspired by
|
|
3
4
|
# @aaronsw and victorykit/whiplash
|
|
4
|
-
require '
|
|
5
|
+
require 'rubystats'
|
|
5
6
|
|
|
6
7
|
module Split
|
|
7
8
|
module Algorithms
|
|
@@ -16,7 +17,7 @@ module Split
|
|
|
16
17
|
def arm_guess(participants, completions)
|
|
17
18
|
a = [participants, 0].max
|
|
18
19
|
b = [participants-completions, 0].max
|
|
19
|
-
|
|
20
|
+
Rubystats::BetaDistribution.new(a+fairness_constant, b+fairness_constant).rng
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
def best_guess(alternatives)
|
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
|
|
@@ -75,7 +76,7 @@ module Split
|
|
|
75
76
|
return 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
|
|
@@ -119,7 +120,10 @@ module Split
|
|
|
119
120
|
n_a = alternative.participant_count
|
|
120
121
|
n_c = control.participant_count
|
|
121
122
|
|
|
122
|
-
|
|
123
|
+
# can't calculate zscore for P(x) > 1
|
|
124
|
+
return 'N/A' if p_a > 1 || p_c > 1
|
|
125
|
+
|
|
126
|
+
Split::Zscore.calculate(p_a, n_a, p_c, n_c)
|
|
123
127
|
end
|
|
124
128
|
|
|
125
129
|
def extra_info
|
data/lib/split/cache.rb
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Split
|
|
4
|
+
class Cache
|
|
5
|
+
|
|
6
|
+
def self.clear
|
|
7
|
+
@cache = nil
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.fetch(namespace, key)
|
|
11
|
+
return yield unless Split.configuration.cache
|
|
12
|
+
|
|
13
|
+
@cache ||= {}
|
|
14
|
+
@cache[namespace] ||= {}
|
|
15
|
+
|
|
16
|
+
value = @cache[namespace][key]
|
|
17
|
+
return value if value
|
|
18
|
+
|
|
19
|
+
@cache[namespace][key] = yield
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.clear_key(key)
|
|
23
|
+
@cache&.keys&.each do |namespace|
|
|
24
|
+
@cache[namespace]&.delete(key)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Split
|
|
4
|
+
module CombinedExperimentsHelper
|
|
5
|
+
def ab_combined_test(metric_descriptor, control = nil, *alternatives)
|
|
6
|
+
return nil unless experiment = find_combined_experiment(metric_descriptor)
|
|
7
|
+
raise(Split::InvalidExperimentsFormatError, "Unable to find experiment #{metric_descriptor} in configuration") if experiment[:combined_experiments].nil?
|
|
8
|
+
|
|
9
|
+
alternative = nil
|
|
10
|
+
weighted_alternatives = nil
|
|
11
|
+
experiment[:combined_experiments].each do |combined_experiment|
|
|
12
|
+
if alternative.nil?
|
|
13
|
+
if control
|
|
14
|
+
alternative = ab_test(combined_experiment, control, alternatives)
|
|
15
|
+
else
|
|
16
|
+
normalized_alternatives = Split::Configuration.new.normalize_alternatives(experiment[:alternatives])
|
|
17
|
+
alternative = ab_test(combined_experiment, normalized_alternatives[0], *normalized_alternatives[1])
|
|
18
|
+
end
|
|
19
|
+
else
|
|
20
|
+
weighted_alternatives ||= experiment[:alternatives].each_with_object({}) do |alt, memo|
|
|
21
|
+
alt = Alternative.new(alt, experiment[:name]).name
|
|
22
|
+
memo[alt] = (alt == alternative ? 1 : 0)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
ab_test(combined_experiment, [weighted_alternatives])
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
alternative
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def find_combined_experiment(metric_descriptor)
|
|
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]
|
|
36
|
+
end
|
|
37
|
+
end
|
|
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,13 +22,20 @@ 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
|
|
29
|
+
attr_accessor :winning_alternative_recalculation_interval
|
|
28
30
|
attr_accessor :redis
|
|
31
|
+
attr_accessor :dashboard_pagination_default_per_page
|
|
32
|
+
attr_accessor :cache
|
|
29
33
|
|
|
30
34
|
attr_reader :experiments
|
|
31
35
|
|
|
36
|
+
attr_writer :bots
|
|
37
|
+
attr_writer :robot_regex
|
|
38
|
+
|
|
32
39
|
def bots
|
|
33
40
|
@bots ||= {
|
|
34
41
|
# Indexers
|
|
@@ -48,7 +55,9 @@ module Split
|
|
|
48
55
|
'spider' => 'generic web spider',
|
|
49
56
|
'UnwindFetchor' => 'Gnip crawler',
|
|
50
57
|
'WordPress' => 'WordPress spider',
|
|
58
|
+
'YandexAccessibilityBot' => 'Yandex accessibility spider',
|
|
51
59
|
'YandexBot' => 'Yandex spider',
|
|
60
|
+
'YandexMobileBot' => 'Yandex mobile spider',
|
|
52
61
|
'ZIBB' => 'ZIBB spider',
|
|
53
62
|
|
|
54
63
|
# HTTP libraries
|
|
@@ -58,12 +67,14 @@ module Split
|
|
|
58
67
|
'ColdFusion' => 'ColdFusion http library',
|
|
59
68
|
'EventMachine HttpClient' => 'Ruby http library',
|
|
60
69
|
'Go http package' => 'Go http library',
|
|
70
|
+
'Go-http-client' => 'Go http library',
|
|
61
71
|
'Java' => 'Generic Java http library',
|
|
62
72
|
'libwww-perl' => 'Perl client-server library loved by script kids',
|
|
63
73
|
'lwp-trivial' => 'Another Perl library loved by script kids',
|
|
64
74
|
'Python-urllib' => 'Python http library',
|
|
65
75
|
'PycURL' => 'Python http library',
|
|
66
76
|
'Test Certificate Info' => 'C http library?',
|
|
77
|
+
'Typhoeus' => 'Ruby http library',
|
|
67
78
|
'Wget' => 'wget unix CLI http client',
|
|
68
79
|
|
|
69
80
|
# URL expanders / previewers
|
|
@@ -71,12 +82,16 @@ module Split
|
|
|
71
82
|
'bitlybot' => 'bit.ly bot',
|
|
72
83
|
'bot@linkfluence.net' => 'Linkfluence bot',
|
|
73
84
|
'facebookexternalhit' => 'facebook bot',
|
|
85
|
+
'Facebot' => 'Facebook crawler',
|
|
74
86
|
'Feedfetcher-Google' => 'Google Feedfetcher',
|
|
75
87
|
'https://developers.google.com/+/web/snippet' => 'Google+ Snippet Fetcher',
|
|
88
|
+
'LinkedInBot' => 'LinkedIn bot',
|
|
76
89
|
'LongURL' => 'URL expander service',
|
|
77
90
|
'NING' => 'NING - Yet Another Twitter Swarmer',
|
|
91
|
+
'Pinterestbot' => 'Pinterest Bot',
|
|
78
92
|
'redditbot' => 'Reddit Bot',
|
|
79
93
|
'ShortLinkTranslate' => 'Link shortener',
|
|
94
|
+
'Slackbot' => 'Slackbot link expander',
|
|
80
95
|
'TweetmemeBot' => 'TweetMeMe Crawler',
|
|
81
96
|
'Twitterbot' => 'Twitter URL expander',
|
|
82
97
|
'UnwindFetch' => 'Gnip URL expander',
|
|
@@ -84,10 +99,12 @@ module Split
|
|
|
84
99
|
|
|
85
100
|
# Uptime monitoring
|
|
86
101
|
'check_http' => 'Nagios monitor',
|
|
102
|
+
'GoogleStackdriverMonitoring' => 'Google Cloud monitor',
|
|
87
103
|
'NewRelicPinger' => 'NewRelic monitor',
|
|
88
104
|
'Panopta' => 'Monitoring service',
|
|
89
105
|
'Pingdom' => 'Pingdom monitoring',
|
|
90
106
|
'SiteUptime' => 'Site monitoring services',
|
|
107
|
+
'UptimeRobot' => 'Monitoring service',
|
|
91
108
|
|
|
92
109
|
# ???
|
|
93
110
|
'DigitalPersona Fingerprint Software' => 'HP Fingerprint scanner',
|
|
@@ -160,7 +177,7 @@ module Split
|
|
|
160
177
|
end
|
|
161
178
|
|
|
162
179
|
def normalize_alternatives(alternatives)
|
|
163
|
-
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|
|
|
164
181
|
p, n = a
|
|
165
182
|
if percent = value_for(v, :percent)
|
|
166
183
|
[p + percent, n + 1]
|
|
@@ -203,26 +220,20 @@ module Split
|
|
|
203
220
|
@on_experiment_delete = proc{|experiment|}
|
|
204
221
|
@on_before_experiment_reset = proc{|experiment|}
|
|
205
222
|
@on_before_experiment_delete = proc{|experiment|}
|
|
223
|
+
@on_experiment_winner_choose = proc{|experiment|}
|
|
206
224
|
@db_failover_allow_parameter_override = false
|
|
207
225
|
@allow_multiple_experiments = false
|
|
208
226
|
@enabled = true
|
|
209
227
|
@experiments = {}
|
|
210
228
|
@persistence = Split::Persistence::SessionAdapter
|
|
211
229
|
@persistence_cookie_length = 31536000 # One year from now
|
|
230
|
+
@persistence_cookie_domain = nil
|
|
212
231
|
@algorithm = Split::Algorithms::WeightedSample
|
|
213
232
|
@include_rails_helper = true
|
|
214
233
|
@beta_probability_simulations = 10000
|
|
234
|
+
@winning_alternative_recalculation_interval = 60 * 60 * 24 # 1 day
|
|
215
235
|
@redis = ENV.fetch(ENV.fetch('REDIS_PROVIDER', 'REDIS_URL'), 'redis://localhost:6379')
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
def redis_url=(value)
|
|
219
|
-
warn '[DEPRECATED] `redis_url=` is deprecated in favor of `redis=`'
|
|
220
|
-
self.redis = value
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
def redis_url
|
|
224
|
-
warn '[DEPRECATED] `redis_url` is deprecated in favor of `redis`'
|
|
225
|
-
self.redis
|
|
236
|
+
@dashboard_pagination_default_per_page = 10
|
|
226
237
|
end
|
|
227
238
|
|
|
228
239
|
private
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module Split
|
|
3
4
|
module DashboardHelpers
|
|
4
5
|
def h(text)
|
|
@@ -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
|
|
|
@@ -0,0 +1,87 @@
|
|
|
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
|
+
|
|
34
|
+
def show_first_page_tag?
|
|
35
|
+
page_number > 2
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def first_page_tag
|
|
39
|
+
%Q(<a href="#{url.chop}?page=1&per=#{pagination_per}">1</a>)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def show_first_ellipsis_tag?
|
|
43
|
+
page_number >= 4
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def ellipsis_tag
|
|
47
|
+
'<span>...</span>'
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def show_prev_page_tag?
|
|
51
|
+
page_number > 1
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def prev_page_tag
|
|
55
|
+
%Q(<a href="#{url.chop}?page=#{page_number - 1}&per=#{pagination_per}">#{page_number - 1}</a>)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def current_page_tag
|
|
59
|
+
"<span><b>#{page_number}</b></span>"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def show_next_page_tag?(collection)
|
|
63
|
+
(page_number * pagination_per) < collection.count
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def next_page_tag
|
|
67
|
+
%Q(<a href="#{url.chop}?page=#{page_number + 1}&per=#{pagination_per}">#{page_number + 1}</a>)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def show_last_ellipsis_tag?(collection)
|
|
71
|
+
(total_pages(collection) - page_number) >= 3
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def total_pages(collection)
|
|
75
|
+
collection.count / pagination_per + ((collection.count % pagination_per).zero? ? 0 : 1)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def show_last_page_tag?(collection)
|
|
79
|
+
page_number < (total_pages(collection) - 1)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def last_page_tag(collection)
|
|
83
|
+
total = total_pages(collection)
|
|
84
|
+
%Q(<a href="#{url.chop}?page=#{total}&per=#{pagination_per}">#{total}</a>)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
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
|
+
}
|
|
@@ -317,3 +317,17 @@ a.button.green:focus, button.green:focus, input[type="submit"].green:focus {
|
|
|
317
317
|
}
|
|
318
318
|
|
|
319
319
|
|
|
320
|
+
.pagination {
|
|
321
|
+
text-align: center;
|
|
322
|
+
font-size: 15px;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.pagination a, .paginaton span {
|
|
326
|
+
display: inline-block;
|
|
327
|
+
padding: 5px;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.divider {
|
|
331
|
+
display: inline-block;
|
|
332
|
+
margin-left: 10px;
|
|
333
|
+
}
|
|
@@ -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">
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<input type="button" id="toggle-active" value="Hide active" />
|
|
7
7
|
<input type="button" id="clear-filter" value="Clear filters" />
|
|
8
8
|
|
|
9
|
-
<% @experiments.each do |experiment| %>
|
|
9
|
+
<% paginated(@experiments).each do |experiment| %>
|
|
10
10
|
<% if experiment.goals.empty? %>
|
|
11
11
|
<%= erb :_experiment, :locals => {:goal => nil, :experiment => experiment} %>
|
|
12
12
|
<% else %>
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
<% end %>
|
|
17
17
|
<% end %>
|
|
18
18
|
<% end %>
|
|
19
|
+
|
|
20
|
+
<div class="pagination">
|
|
21
|
+
<%= pagination(@experiments) %>
|
|
22
|
+
</div>
|
|
19
23
|
<% else %>
|
|
20
24
|
<p class="intro">No experiments have started yet, you need to define them in your code and introduce them to your users.</p>
|
|
21
25
|
<p class="intro">Check out the <a href='https://github.com/splitrb/split#readme'>Readme</a> for more help getting started.</p>
|
data/lib/split/dashboard.rb
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
require 'sinatra/base'
|
|
3
4
|
require 'split'
|
|
4
5
|
require 'bigdecimal'
|
|
5
6
|
require 'split/dashboard/helpers'
|
|
7
|
+
require 'split/dashboard/pagination_helpers'
|
|
6
8
|
|
|
7
9
|
module Split
|
|
8
10
|
class Dashboard < Sinatra::Base
|
|
@@ -14,6 +16,7 @@ module Split
|
|
|
14
16
|
set :method_override, true
|
|
15
17
|
|
|
16
18
|
helpers Split::DashboardHelpers
|
|
19
|
+
helpers Split::DashboardPaginationHelpers
|
|
17
20
|
|
|
18
21
|
get '/' do
|
|
19
22
|
# Display experiments without a winner at the top of the dashboard
|
|
@@ -31,7 +34,13 @@ module Split
|
|
|
31
34
|
end
|
|
32
35
|
|
|
33
36
|
post '/force_alternative' do
|
|
34
|
-
Split::
|
|
37
|
+
experiment = Split::ExperimentCatalog.find(params[:experiment])
|
|
38
|
+
alternative = Split::Alternative.new(params[:alternative], experiment.name)
|
|
39
|
+
|
|
40
|
+
cookies = JSON.parse(request.cookies['split_override']) rescue {}
|
|
41
|
+
cookies[experiment.name] = alternative.name
|
|
42
|
+
response.set_cookie('split_override', { value: cookies.to_json, path: '/' })
|
|
43
|
+
|
|
35
44
|
redirect url('/')
|
|
36
45
|
end
|
|
37
46
|
|
|
@@ -60,6 +69,17 @@ module Split
|
|
|
60
69
|
redirect url('/')
|
|
61
70
|
end
|
|
62
71
|
|
|
72
|
+
post '/update_cohorting' do
|
|
73
|
+
@experiment = Split::ExperimentCatalog.find(params[:experiment])
|
|
74
|
+
case params[:cohorting_action].downcase
|
|
75
|
+
when "enable"
|
|
76
|
+
@experiment.enable_cohorting
|
|
77
|
+
when "disable"
|
|
78
|
+
@experiment.disable_cohorting
|
|
79
|
+
end
|
|
80
|
+
redirect url('/')
|
|
81
|
+
end
|
|
82
|
+
|
|
63
83
|
delete '/experiment' do
|
|
64
84
|
@experiment = Split::ExperimentCatalog.find(params[:experiment])
|
|
65
85
|
@experiment.delete
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
require "split/helper"
|
|
3
4
|
|
|
4
5
|
# Split's helper exposes all kinds of methods we don't want to
|
|
@@ -28,8 +29,8 @@ module Split
|
|
|
28
29
|
end
|
|
29
30
|
end
|
|
30
31
|
|
|
31
|
-
def ab_test(*arguments
|
|
32
|
-
split_context_shim.ab_test(*arguments
|
|
32
|
+
def ab_test(*arguments, &block)
|
|
33
|
+
split_context_shim.ab_test(*arguments, &block)
|
|
33
34
|
end
|
|
34
35
|
|
|
35
36
|
private
|
data/lib/split/engine.rb
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module Split
|
|
3
4
|
class Engine < ::Rails::Engine
|
|
4
5
|
initializer "split" do |app|
|
|
5
6
|
if Split.configuration.include_rails_helper
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
ActiveSupport.on_load(:action_controller) do
|
|
8
|
+
::ActionController::Base.send :include, Split::Helper
|
|
9
|
+
::ActionController::Base.helper Split::Helper
|
|
10
|
+
::ActionController::Base.send :include, Split::CombinedExperimentsHelper
|
|
11
|
+
::ActionController::Base.helper Split::CombinedExperimentsHelper
|
|
12
|
+
end
|
|
8
13
|
end
|
|
9
14
|
end
|
|
10
15
|
end
|