split 3.3.2 → 4.0.0.pre2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|