split 3.3.2 → 4.0.0.pre2
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 +61 -0
- data/.rspec +1 -0
- data/.rubocop.yml +71 -1044
- data/.rubocop_todo.yml +226 -0
- data/Appraisals +1 -1
- data/CHANGELOG.md +62 -0
- data/CODE_OF_CONDUCT.md +3 -3
- data/Gemfile +2 -0
- data/README.md +40 -18
- data/Rakefile +2 -0
- data/gemfiles/6.0.gemfile +1 -1
- 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 +4 -3
- data/lib/split/cache.rb +28 -0
- data/lib/split/combined_experiments_helper.rb +2 -1
- data/lib/split/configuration.rb +13 -14
- data/lib/split/dashboard/helpers.rb +1 -0
- data/lib/split/dashboard/pagination_helpers.rb +3 -3
- data/lib/split/dashboard/paginator.rb +1 -0
- data/lib/split/dashboard/public/dashboard.js +10 -0
- data/lib/split/dashboard/public/style.css +5 -0
- data/lib/split/dashboard/views/_controls.erb +13 -0
- data/lib/split/dashboard/views/layout.erb +1 -1
- data/lib/split/dashboard.rb +19 -1
- data/lib/split/encapsulated_helper.rb +3 -2
- data/lib/split/engine.rb +7 -4
- data/lib/split/exceptions.rb +1 -0
- data/lib/split/experiment.rb +98 -65
- 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 +28 -8
- data/lib/split/metric.rb +2 -1
- data/lib/split/persistence/cookie_adapter.rb +6 -1
- 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 -28
- data/lib/split/trial.rb +21 -11
- 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 +9 -3
- data/spec/alternative_spec.rb +1 -1
- data/spec/cache_spec.rb +88 -0
- data/spec/configuration_spec.rb +17 -15
- data/spec/dashboard/pagination_helpers_spec.rb +3 -1
- 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 +116 -12
- data/spec/goals_collection_spec.rb +1 -1
- data/spec/helper_spec.rb +186 -112
- data/spec/persistence/cookie_adapter_spec.rb +1 -1
- 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/trial_spec.rb +45 -19
- data/spec/user_spec.rb +45 -3
- data/split.gemspec +8 -9
- metadata +28 -36
- data/.travis.yml +0 -66
- data/gemfiles/4.2.gemfile +0 -9
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,14 +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
|
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
|
@@ -82,7 +88,7 @@ module Split
|
|
82
88
|
'LinkedInBot' => 'LinkedIn bot',
|
83
89
|
'LongURL' => 'URL expander service',
|
84
90
|
'NING' => 'NING - Yet Another Twitter Swarmer',
|
85
|
-
'
|
91
|
+
'Pinterestbot' => 'Pinterest Bot',
|
86
92
|
'redditbot' => 'Reddit Bot',
|
87
93
|
'ShortLinkTranslate' => 'Link shortener',
|
88
94
|
'Slackbot' => 'Slackbot link expander',
|
@@ -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]
|
@@ -214,27 +220,20 @@ module Split
|
|
214
220
|
@on_experiment_delete = proc{|experiment|}
|
215
221
|
@on_before_experiment_reset = proc{|experiment|}
|
216
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
235
|
@redis = ENV.fetch(ENV.fetch('REDIS_PROVIDER', 'REDIS_URL'), 'redis://localhost:6379')
|
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
|
236
|
+
@dashboard_pagination_default_per_page = 10
|
238
237
|
end
|
239
238
|
|
240
239
|
private
|
@@ -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
|
@@ -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
|
+
}
|
@@ -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">
|
data/lib/split/dashboard.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'sinatra/base'
|
3
4
|
require 'split'
|
4
5
|
require 'bigdecimal'
|
@@ -33,7 +34,13 @@ module Split
|
|
33
34
|
end
|
34
35
|
|
35
36
|
post '/force_alternative' do
|
36
|
-
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
|
+
|
37
44
|
redirect url('/')
|
38
45
|
end
|
39
46
|
|
@@ -62,6 +69,17 @@ module Split
|
|
62
69
|
redirect url('/')
|
63
70
|
end
|
64
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
|
+
|
65
83
|
delete '/experiment' do
|
66
84
|
@experiment = Split::ExperimentCatalog.find(params[:experiment])
|
67
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,12 +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
|
-
|
8
|
-
|
9
|
-
|
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
|
10
13
|
end
|
11
14
|
end
|
12
15
|
end
|
data/lib/split/exceptions.rb
CHANGED
data/lib/split/experiment.rb
CHANGED
@@ -1,38 +1,34 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rubystats'
|
4
|
+
|
2
5
|
module Split
|
3
6
|
class Experiment
|
4
7
|
attr_accessor :name
|
5
|
-
attr_writer :algorithm
|
6
|
-
attr_accessor :resettable
|
7
8
|
attr_accessor :goals
|
8
|
-
attr_accessor :alternatives
|
9
9
|
attr_accessor :alternative_probabilities
|
10
10
|
attr_accessor :metadata
|
11
11
|
|
12
|
+
attr_reader :alternatives
|
13
|
+
attr_reader :resettable
|
14
|
+
|
12
15
|
DEFAULT_OPTIONS = {
|
13
16
|
:resettable => true
|
14
17
|
}
|
15
18
|
|
19
|
+
def self.find(name)
|
20
|
+
Split.cache(:experiments, name) do
|
21
|
+
return unless Split.redis.exists?(name)
|
22
|
+
Experiment.new(name).tap { |exp| exp.load_from_redis }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
16
26
|
def initialize(name, options = {})
|
17
27
|
options = DEFAULT_OPTIONS.merge(options)
|
18
28
|
|
19
29
|
@name = name.to_s
|
20
30
|
|
21
|
-
|
22
|
-
|
23
|
-
if alternatives.empty? && (exp_config = Split.configuration.experiment_for(name))
|
24
|
-
options = {
|
25
|
-
alternatives: load_alternatives_from_configuration,
|
26
|
-
goals: Split::GoalsCollection.new(@name).load_from_configuration,
|
27
|
-
metadata: load_metadata_from_configuration,
|
28
|
-
resettable: exp_config[:resettable],
|
29
|
-
algorithm: exp_config[:algorithm]
|
30
|
-
}
|
31
|
-
else
|
32
|
-
options[:alternatives] = alternatives
|
33
|
-
end
|
34
|
-
|
35
|
-
set_alternatives_and_options(options)
|
31
|
+
extract_alternatives_from_options(options)
|
36
32
|
end
|
37
33
|
|
38
34
|
def self.finished_key(key)
|
@@ -40,11 +36,15 @@ module Split
|
|
40
36
|
end
|
41
37
|
|
42
38
|
def set_alternatives_and_options(options)
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
self.
|
39
|
+
options_with_defaults = DEFAULT_OPTIONS.merge(
|
40
|
+
options.reject { |k, v| v.nil? }
|
41
|
+
)
|
42
|
+
|
43
|
+
self.alternatives = options_with_defaults[:alternatives]
|
44
|
+
self.goals = options_with_defaults[:goals]
|
45
|
+
self.resettable = options_with_defaults[:resettable]
|
46
|
+
self.algorithm = options_with_defaults[:algorithm]
|
47
|
+
self.metadata = options_with_defaults[:metadata]
|
48
48
|
end
|
49
49
|
|
50
50
|
def extract_alternatives_from_options(options)
|
@@ -52,7 +52,7 @@ module Split
|
|
52
52
|
|
53
53
|
if alts.length == 1
|
54
54
|
if alts[0].is_a? Hash
|
55
|
-
alts = alts[0].map{|k,v| {k => v} }
|
55
|
+
alts = alts[0].map{|k, v| {k => v} }
|
56
56
|
end
|
57
57
|
end
|
58
58
|
|
@@ -81,14 +81,14 @@ module Split
|
|
81
81
|
|
82
82
|
if new_record?
|
83
83
|
start unless Split.configuration.start_manually
|
84
|
+
persist_experiment_configuration
|
84
85
|
elsif experiment_configuration_has_changed?
|
85
86
|
reset unless Split.configuration.reset_manually
|
87
|
+
persist_experiment_configuration
|
86
88
|
end
|
87
89
|
|
88
|
-
|
89
|
-
|
90
|
-
redis.hset(experiment_config_key, :resettable, resettable)
|
91
|
-
redis.hset(experiment_config_key, :algorithm, algorithm.to_s)
|
90
|
+
redis.hmset(experiment_config_key, :resettable, resettable,
|
91
|
+
:algorithm, algorithm.to_s)
|
92
92
|
self
|
93
93
|
end
|
94
94
|
|
@@ -101,7 +101,7 @@ module Split
|
|
101
101
|
end
|
102
102
|
|
103
103
|
def new_record?
|
104
|
-
|
104
|
+
ExperimentCatalog.find(name).nil?
|
105
105
|
end
|
106
106
|
|
107
107
|
def ==(obj)
|
@@ -135,24 +135,29 @@ module Split
|
|
135
135
|
end
|
136
136
|
|
137
137
|
def winner
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
138
|
+
Split.cache(:experiment_winner, name) do
|
139
|
+
experiment_winner = redis.hget(:experiment_winner, name)
|
140
|
+
if experiment_winner
|
141
|
+
Split::Alternative.new(experiment_winner, name)
|
142
|
+
else
|
143
|
+
nil
|
144
|
+
end
|
143
145
|
end
|
144
146
|
end
|
145
147
|
|
146
148
|
def has_winner?
|
147
|
-
|
149
|
+
return @has_winner if defined? @has_winner
|
150
|
+
@has_winner = !winner.nil?
|
148
151
|
end
|
149
152
|
|
150
153
|
def winner=(winner_name)
|
151
154
|
redis.hset(:experiment_winner, name, winner_name.to_s)
|
155
|
+
@has_winner = true
|
156
|
+
Split.configuration.on_experiment_winner_choose.call(self)
|
152
157
|
end
|
153
158
|
|
154
159
|
def participant_count
|
155
|
-
alternatives.inject(0){|sum,a| sum + a.participant_count}
|
160
|
+
alternatives.inject(0){|sum, a| sum + a.participant_count}
|
156
161
|
end
|
157
162
|
|
158
163
|
def control
|
@@ -161,6 +166,8 @@ module Split
|
|
161
166
|
|
162
167
|
def reset_winner
|
163
168
|
redis.hdel(:experiment_winner, name)
|
169
|
+
@has_winner = false
|
170
|
+
Split::Cache.clear_key(@name)
|
164
171
|
end
|
165
172
|
|
166
173
|
def start
|
@@ -168,13 +175,15 @@ module Split
|
|
168
175
|
end
|
169
176
|
|
170
177
|
def start_time
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
+
Split.cache(:experiment_start_times, @name) do
|
179
|
+
t = redis.hget(:experiment_start_times, @name)
|
180
|
+
if t
|
181
|
+
# Check if stored time is an integer
|
182
|
+
if t =~ /^[-+]?[0-9]+$/
|
183
|
+
Time.at(t.to_i)
|
184
|
+
else
|
185
|
+
Time.parse(t)
|
186
|
+
end
|
178
187
|
end
|
179
188
|
end
|
180
189
|
end
|
@@ -225,6 +234,7 @@ module Split
|
|
225
234
|
|
226
235
|
def reset
|
227
236
|
Split.configuration.on_before_experiment_reset.call(self)
|
237
|
+
Split::Cache.clear_key(@name)
|
228
238
|
alternatives.each(&:reset)
|
229
239
|
reset_winner
|
230
240
|
Split.configuration.on_experiment_reset.call(self)
|
@@ -238,6 +248,7 @@ module Split
|
|
238
248
|
end
|
239
249
|
reset_winner
|
240
250
|
redis.srem(:experiments, name)
|
251
|
+
remove_experiment_cohorting
|
241
252
|
remove_experiment_configuration
|
242
253
|
Split.configuration.on_experiment_delete.call(self)
|
243
254
|
increment_version
|
@@ -335,7 +346,7 @@ module Split
|
|
335
346
|
|
336
347
|
def find_simulated_winner(simulated_cr_hash)
|
337
348
|
# figure out which alternative had the highest simulated conversion rate
|
338
|
-
winning_pair = ["",0.0]
|
349
|
+
winning_pair = ["", 0.0]
|
339
350
|
simulated_cr_hash.each do |alternative, rate|
|
340
351
|
if rate > winning_pair[1]
|
341
352
|
winning_pair = [alternative, rate]
|
@@ -346,17 +357,13 @@ module Split
|
|
346
357
|
end
|
347
358
|
|
348
359
|
def calc_simulated_conversion_rates(beta_params)
|
349
|
-
# initialize a random variable (from which to simulate conversion rates ~beta-distributed)
|
350
|
-
rand = SimpleRandom.new
|
351
|
-
rand.set_seed
|
352
|
-
|
353
360
|
simulated_cr_hash = {}
|
354
361
|
|
355
362
|
# create a hash which has the conversion rate pulled from each alternative's beta distribution
|
356
363
|
beta_params.each do |alternative, params|
|
357
364
|
alpha = params[0]
|
358
365
|
beta = params[1]
|
359
|
-
simulated_conversion_rate =
|
366
|
+
simulated_conversion_rate = Rubystats::BetaDistribution.new(alpha, beta).rng
|
360
367
|
simulated_cr_hash[alternative] = simulated_conversion_rate
|
361
368
|
end
|
362
369
|
|
@@ -394,6 +401,23 @@ module Split
|
|
394
401
|
js_id.gsub('/', '--')
|
395
402
|
end
|
396
403
|
|
404
|
+
def cohorting_disabled?
|
405
|
+
@cohorting_disabled ||= begin
|
406
|
+
value = redis.hget(experiment_config_key, :cohorting)
|
407
|
+
value.nil? ? false : value.downcase == "true"
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|
411
|
+
def disable_cohorting
|
412
|
+
@cohorting_disabled = true
|
413
|
+
redis.hset(experiment_config_key, :cohorting, true)
|
414
|
+
end
|
415
|
+
|
416
|
+
def enable_cohorting
|
417
|
+
@cohorting_disabled = false
|
418
|
+
redis.hset(experiment_config_key, :cohorting, false)
|
419
|
+
end
|
420
|
+
|
397
421
|
protected
|
398
422
|
|
399
423
|
def experiment_config_key
|
@@ -420,14 +444,14 @@ module Split
|
|
420
444
|
end
|
421
445
|
|
422
446
|
def load_alternatives_from_redis
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
447
|
+
alternatives = redis.lrange(@name, 0, -1)
|
448
|
+
alternatives.map do |alt|
|
449
|
+
alt = begin
|
450
|
+
JSON.parse(alt)
|
451
|
+
rescue
|
452
|
+
alt
|
453
|
+
end
|
454
|
+
Split::Alternative.new(alt, @name)
|
431
455
|
end
|
432
456
|
end
|
433
457
|
|
@@ -443,9 +467,14 @@ module Split
|
|
443
467
|
|
444
468
|
def persist_experiment_configuration
|
445
469
|
redis_interface.add_to_set(:experiments, name)
|
446
|
-
redis_interface.persist_list(name, @alternatives.map
|
470
|
+
redis_interface.persist_list(name, @alternatives.map{|alt| {alt.name => alt.weight}.to_json})
|
447
471
|
goals_collection.save
|
448
|
-
|
472
|
+
|
473
|
+
if @metadata
|
474
|
+
redis.set(metadata_key, @metadata.to_json)
|
475
|
+
else
|
476
|
+
delete_metadata
|
477
|
+
end
|
449
478
|
end
|
450
479
|
|
451
480
|
def remove_experiment_configuration
|
@@ -456,16 +485,20 @@ module Split
|
|
456
485
|
end
|
457
486
|
|
458
487
|
def experiment_configuration_has_changed?
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
existing_metadata != @metadata
|
488
|
+
existing_experiment = Experiment.find(@name)
|
489
|
+
|
490
|
+
existing_experiment.alternatives.map(&:to_s) != @alternatives.map(&:to_s) ||
|
491
|
+
existing_experiment.goals != @goals ||
|
492
|
+
existing_experiment.metadata != @metadata
|
465
493
|
end
|
466
494
|
|
467
495
|
def goals_collection
|
468
496
|
Split::GoalsCollection.new(@name, @goals)
|
469
497
|
end
|
498
|
+
|
499
|
+
def remove_experiment_cohorting
|
500
|
+
@cohorting_disabled = false
|
501
|
+
redis.hdel(experiment_config_key, :cohorting)
|
502
|
+
end
|
470
503
|
end
|
471
504
|
end
|
@@ -13,8 +13,7 @@ module Split
|
|
13
13
|
end
|
14
14
|
|
15
15
|
def self.find(name)
|
16
|
-
|
17
|
-
Experiment.new(name).tap { |exp| exp.load_from_redis }
|
16
|
+
Experiment.find(name)
|
18
17
|
end
|
19
18
|
|
20
19
|
def self.find_or_initialize(metric_descriptor, control = nil, *alternatives)
|
@@ -46,6 +45,5 @@ module Split
|
|
46
45
|
return experiment_name, goals
|
47
46
|
end
|
48
47
|
private_class_method :normalize_experiment
|
49
|
-
|
50
48
|
end
|
51
49
|
end
|