split 3.4.0 → 4.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/FUNDING.yml +1 -0
- data/.github/dependabot.yml +7 -0
- data/.github/workflows/ci.yml +71 -0
- data/.rubocop.yml +177 -1
- data/.rubocop_todo.yml +40 -493
- data/CHANGELOG.md +41 -0
- data/Gemfile +1 -0
- data/README.md +26 -6
- data/Rakefile +1 -0
- data/gemfiles/{4.2.gemfile → 6.1.gemfile} +1 -1
- data/gemfiles/7.0.gemfile +9 -0
- data/lib/split/algorithms/block_randomization.rb +1 -0
- data/lib/split/algorithms/weighted_sample.rb +2 -1
- data/lib/split/algorithms/whiplash.rb +3 -2
- data/lib/split/alternative.rb +1 -0
- data/lib/split/cache.rb +28 -0
- data/lib/split/combined_experiments_helper.rb +1 -0
- data/lib/split/configuration.rb +8 -12
- data/lib/split/dashboard/helpers.rb +1 -0
- data/lib/split/dashboard/pagination_helpers.rb +1 -0
- 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.rb +17 -2
- data/lib/split/encapsulated_helper.rb +3 -2
- data/lib/split/engine.rb +5 -4
- data/lib/split/exceptions.rb +1 -0
- data/lib/split/experiment.rb +82 -60
- data/lib/split/experiment_catalog.rb +1 -3
- data/lib/split/extensions/string.rb +1 -0
- data/lib/split/goals_collection.rb +1 -0
- data/lib/split/helper.rb +26 -7
- data/lib/split/metric.rb +2 -1
- data/lib/split/persistence/cookie_adapter.rb +6 -1
- 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 +8 -28
- data/lib/split/trial.rb +20 -10
- data/lib/split/user.rb +15 -3
- 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_spec.rb +45 -5
- data/spec/encapsulated_helper_spec.rb +1 -1
- data/spec/experiment_spec.rb +78 -13
- data/spec/goals_collection_spec.rb +1 -1
- data/spec/helper_spec.rb +68 -32
- data/spec/persistence/cookie_adapter_spec.rb +1 -1
- 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 +34 -3
- data/split.gemspec +7 -7
- metadata +27 -35
- data/.travis.yml +0 -60
data/lib/split/experiment.rb
CHANGED
@@ -1,4 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rubystats'
|
4
|
+
|
2
5
|
module Split
|
3
6
|
class Experiment
|
4
7
|
attr_accessor :name
|
@@ -13,26 +16,19 @@ module Split
|
|
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.fetch(:resettable, true),
|
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
|
|
@@ -62,7 +62,7 @@ module Split
|
|
62
62
|
alts = load_alternatives_from_configuration
|
63
63
|
options[:goals] = Split::GoalsCollection.new(@name).load_from_configuration
|
64
64
|
options[:metadata] = load_metadata_from_configuration
|
65
|
-
options[:resettable] = exp_config
|
65
|
+
options[:resettable] = exp_config[:resettable]
|
66
66
|
options[:algorithm] = exp_config[:algorithm]
|
67
67
|
end
|
68
68
|
end
|
@@ -87,8 +87,8 @@ module Split
|
|
87
87
|
persist_experiment_configuration
|
88
88
|
end
|
89
89
|
|
90
|
-
redis.
|
91
|
-
|
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,11 +135,13 @@ 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
|
|
@@ -151,10 +153,11 @@ module Split
|
|
151
153
|
def winner=(winner_name)
|
152
154
|
redis.hset(:experiment_winner, name, winner_name.to_s)
|
153
155
|
@has_winner = true
|
156
|
+
Split.configuration.on_experiment_winner_choose.call(self)
|
154
157
|
end
|
155
158
|
|
156
159
|
def participant_count
|
157
|
-
alternatives.inject(0){|sum,a| sum + a.participant_count}
|
160
|
+
alternatives.inject(0){|sum, a| sum + a.participant_count}
|
158
161
|
end
|
159
162
|
|
160
163
|
def control
|
@@ -164,6 +167,7 @@ module Split
|
|
164
167
|
def reset_winner
|
165
168
|
redis.hdel(:experiment_winner, name)
|
166
169
|
@has_winner = false
|
170
|
+
Split::Cache.clear_key(@name)
|
167
171
|
end
|
168
172
|
|
169
173
|
def start
|
@@ -171,13 +175,15 @@ module Split
|
|
171
175
|
end
|
172
176
|
|
173
177
|
def start_time
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
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
|
181
187
|
end
|
182
188
|
end
|
183
189
|
end
|
@@ -228,6 +234,7 @@ module Split
|
|
228
234
|
|
229
235
|
def reset
|
230
236
|
Split.configuration.on_before_experiment_reset.call(self)
|
237
|
+
Split::Cache.clear_key(@name)
|
231
238
|
alternatives.each(&:reset)
|
232
239
|
reset_winner
|
233
240
|
Split.configuration.on_experiment_reset.call(self)
|
@@ -241,6 +248,7 @@ module Split
|
|
241
248
|
end
|
242
249
|
reset_winner
|
243
250
|
redis.srem(:experiments, name)
|
251
|
+
remove_experiment_cohorting
|
244
252
|
remove_experiment_configuration
|
245
253
|
Split.configuration.on_experiment_delete.call(self)
|
246
254
|
increment_version
|
@@ -338,7 +346,7 @@ module Split
|
|
338
346
|
|
339
347
|
def find_simulated_winner(simulated_cr_hash)
|
340
348
|
# figure out which alternative had the highest simulated conversion rate
|
341
|
-
winning_pair = ["",0.0]
|
349
|
+
winning_pair = ["", 0.0]
|
342
350
|
simulated_cr_hash.each do |alternative, rate|
|
343
351
|
if rate > winning_pair[1]
|
344
352
|
winning_pair = [alternative, rate]
|
@@ -349,17 +357,13 @@ module Split
|
|
349
357
|
end
|
350
358
|
|
351
359
|
def calc_simulated_conversion_rates(beta_params)
|
352
|
-
# initialize a random variable (from which to simulate conversion rates ~beta-distributed)
|
353
|
-
rand = SimpleRandom.new
|
354
|
-
rand.set_seed
|
355
|
-
|
356
360
|
simulated_cr_hash = {}
|
357
361
|
|
358
362
|
# create a hash which has the conversion rate pulled from each alternative's beta distribution
|
359
363
|
beta_params.each do |alternative, params|
|
360
364
|
alpha = params[0]
|
361
365
|
beta = params[1]
|
362
|
-
simulated_conversion_rate =
|
366
|
+
simulated_conversion_rate = Rubystats::BetaDistribution.new(alpha, beta).rng
|
363
367
|
simulated_cr_hash[alternative] = simulated_conversion_rate
|
364
368
|
end
|
365
369
|
|
@@ -397,6 +401,23 @@ module Split
|
|
397
401
|
js_id.gsub('/', '--')
|
398
402
|
end
|
399
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
|
+
|
400
421
|
protected
|
401
422
|
|
402
423
|
def experiment_config_key
|
@@ -423,15 +444,7 @@ module Split
|
|
423
444
|
end
|
424
445
|
|
425
446
|
def load_alternatives_from_redis
|
426
|
-
alternatives =
|
427
|
-
when 'set' # convert legacy sets to lists
|
428
|
-
alts = redis.smembers(@name)
|
429
|
-
redis.del(@name)
|
430
|
-
alts.reverse.each {|a| redis.lpush(@name, a) }
|
431
|
-
redis.lrange(@name, 0, -1)
|
432
|
-
else
|
433
|
-
redis.lrange(@name, 0, -1)
|
434
|
-
end
|
447
|
+
alternatives = redis.lrange(@name, 0, -1)
|
435
448
|
alternatives.map do |alt|
|
436
449
|
alt = begin
|
437
450
|
JSON.parse(alt)
|
@@ -456,7 +469,12 @@ module Split
|
|
456
469
|
redis_interface.add_to_set(:experiments, name)
|
457
470
|
redis_interface.persist_list(name, @alternatives.map{|alt| {alt.name => alt.weight}.to_json})
|
458
471
|
goals_collection.save
|
459
|
-
|
472
|
+
|
473
|
+
if @metadata
|
474
|
+
redis.set(metadata_key, @metadata.to_json)
|
475
|
+
else
|
476
|
+
delete_metadata
|
477
|
+
end
|
460
478
|
end
|
461
479
|
|
462
480
|
def remove_experiment_configuration
|
@@ -467,16 +485,20 @@ module Split
|
|
467
485
|
end
|
468
486
|
|
469
487
|
def experiment_configuration_has_changed?
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
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
|
476
493
|
end
|
477
494
|
|
478
495
|
def goals_collection
|
479
496
|
Split::GoalsCollection.new(@name, @goals)
|
480
497
|
end
|
498
|
+
|
499
|
+
def remove_experiment_cohorting
|
500
|
+
@cohorting_disabled = false
|
501
|
+
redis.hdel(experiment_config_key, :cohorting)
|
502
|
+
end
|
481
503
|
end
|
482
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
|
data/lib/split/helper.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Split
|
3
4
|
module Helper
|
4
5
|
OVERRIDE_PARAM_NAME = "ab_test"
|
@@ -32,8 +33,8 @@ module Split
|
|
32
33
|
end
|
33
34
|
|
34
35
|
if block_given?
|
35
|
-
metadata =
|
36
|
-
yield(alternative, metadata)
|
36
|
+
metadata = experiment.metadata[alternative] if experiment.metadata
|
37
|
+
yield(alternative, metadata || {})
|
37
38
|
else
|
38
39
|
alternative
|
39
40
|
end
|
@@ -51,9 +52,14 @@ module Split
|
|
51
52
|
return true
|
52
53
|
else
|
53
54
|
alternative_name = ab_user[experiment.key]
|
54
|
-
trial = Trial.new(
|
55
|
-
|
56
|
-
|
55
|
+
trial = Trial.new(
|
56
|
+
:user => ab_user,
|
57
|
+
:experiment => experiment,
|
58
|
+
:alternative => alternative_name,
|
59
|
+
:goals => options[:goals],
|
60
|
+
)
|
61
|
+
|
62
|
+
trial.complete!(self)
|
57
63
|
|
58
64
|
if should_reset
|
59
65
|
reset!(experiment)
|
@@ -70,6 +76,7 @@ module Split
|
|
70
76
|
|
71
77
|
if experiments.any?
|
72
78
|
experiments.each do |experiment|
|
79
|
+
next if override_present?(experiment.key)
|
73
80
|
finish_experiment(experiment, options.merge(:goals => goals))
|
74
81
|
end
|
75
82
|
end
|
@@ -105,15 +112,27 @@ module Split
|
|
105
112
|
Split.configuration.db_failover_on_db_error.call(e)
|
106
113
|
end
|
107
114
|
|
108
|
-
|
109
115
|
def override_present?(experiment_name)
|
110
|
-
|
116
|
+
override_alternative_by_params(experiment_name) || override_alternative_by_cookies(experiment_name)
|
111
117
|
end
|
112
118
|
|
113
119
|
def override_alternative(experiment_name)
|
120
|
+
override_alternative_by_params(experiment_name) || override_alternative_by_cookies(experiment_name)
|
121
|
+
end
|
122
|
+
|
123
|
+
def override_alternative_by_params(experiment_name)
|
114
124
|
defined?(params) && params[OVERRIDE_PARAM_NAME] && params[OVERRIDE_PARAM_NAME][experiment_name]
|
115
125
|
end
|
116
126
|
|
127
|
+
def override_alternative_by_cookies(experiment_name)
|
128
|
+
return unless defined?(request)
|
129
|
+
|
130
|
+
if request.cookies && request.cookies.key?('split_override')
|
131
|
+
experiments = JSON.parse(request.cookies['split_override']) rescue {}
|
132
|
+
experiments[experiment_name]
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
117
136
|
def split_generically_disabled?
|
118
137
|
defined?(params) && params['SPLIT_DISABLE']
|
119
138
|
end
|
data/lib/split/metric.rb
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Split
|
3
4
|
class Metric
|
4
5
|
attr_accessor :name
|
5
6
|
attr_accessor :experiments
|
6
7
|
|
7
8
|
def initialize(attrs = {})
|
8
|
-
attrs.each do |key,value|
|
9
|
+
attrs.each do |key, value|
|
9
10
|
if self.respond_to?("#{key}=")
|
10
11
|
self.send("#{key}=", value)
|
11
12
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require "json"
|
3
4
|
|
4
5
|
module Split
|
@@ -44,7 +45,7 @@ module Split
|
|
44
45
|
end
|
45
46
|
|
46
47
|
def default_options
|
47
|
-
{ expires: @expires, path: '/' }
|
48
|
+
{ expires: @expires, path: '/', domain: cookie_domain_config }.compact
|
48
49
|
end
|
49
50
|
|
50
51
|
def set_cookie_via_rack(key, value)
|
@@ -86,6 +87,10 @@ module Split
|
|
86
87
|
Split.configuration.persistence_cookie_length
|
87
88
|
end
|
88
89
|
|
90
|
+
def cookie_domain_config
|
91
|
+
Split.configuration.persistence_cookie_domain
|
92
|
+
end
|
93
|
+
|
89
94
|
def action_dispatch?
|
90
95
|
defined?(Rails) && @response.is_a?(ActionDispatch::Response)
|
91
96
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Split
|
3
4
|
module Persistence
|
4
5
|
class RedisAdapter
|
@@ -39,6 +40,10 @@ module Split
|
|
39
40
|
Split.redis.hkeys(redis_key)
|
40
41
|
end
|
41
42
|
|
43
|
+
def self.find(user_id)
|
44
|
+
new(nil, user_id)
|
45
|
+
end
|
46
|
+
|
42
47
|
def self.with_config(options={})
|
43
48
|
self.config.merge!(options)
|
44
49
|
self
|
data/lib/split/persistence.rb
CHANGED
@@ -8,8 +8,10 @@ module Split
|
|
8
8
|
require 'split/persistence/session_adapter'
|
9
9
|
|
10
10
|
ADAPTERS = {
|
11
|
-
:
|
12
|
-
:
|
11
|
+
cookie: Split::Persistence::CookieAdapter,
|
12
|
+
session: Split::Persistence::SessionAdapter,
|
13
|
+
redis: Split::Persistence::RedisAdapter,
|
14
|
+
dual_adapter: Split::Persistence::DualAdapter
|
13
15
|
}.freeze
|
14
16
|
|
15
17
|
def self.adapter
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Split
|
3
4
|
# Simplifies the interface to Redis.
|
4
5
|
class RedisInterface
|
@@ -7,40 +8,19 @@ module Split
|
|
7
8
|
end
|
8
9
|
|
9
10
|
def persist_list(list_name, list_values)
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
set_list_index(list_name, index, value)
|
11
|
+
if list_values.length > 0
|
12
|
+
redis.multi do |multi|
|
13
|
+
tmp_list = "#{list_name}_tmp"
|
14
|
+
multi.rpush(tmp_list, list_values)
|
15
|
+
multi.rename(tmp_list, list_name)
|
16
16
|
end
|
17
17
|
end
|
18
|
-
make_list_length(list_name, list_values.length)
|
19
|
-
list_values
|
20
|
-
end
|
21
|
-
|
22
|
-
def add_to_list(list_name, value)
|
23
|
-
redis.rpush(list_name, value)
|
24
|
-
end
|
25
|
-
|
26
|
-
def set_list_index(list_name, index, value)
|
27
|
-
redis.lset(list_name, index, value)
|
28
|
-
end
|
29
|
-
|
30
|
-
def list_length(list_name)
|
31
|
-
redis.llen(list_name)
|
32
|
-
end
|
33
18
|
|
34
|
-
|
35
|
-
redis.rpop(list_name)
|
36
|
-
end
|
37
|
-
|
38
|
-
def make_list_length(list_name, new_length)
|
39
|
-
redis.ltrim(list_name, 0, new_length - 1)
|
19
|
+
list_values
|
40
20
|
end
|
41
21
|
|
42
22
|
def add_to_set(set_name, value)
|
43
|
-
redis.sadd(set_name, value)
|
23
|
+
redis.sadd(set_name, value)
|
44
24
|
end
|
45
25
|
|
46
26
|
private
|
data/lib/split/trial.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Split
|
3
4
|
class Trial
|
5
|
+
attr_accessor :goals
|
4
6
|
attr_accessor :experiment
|
5
7
|
attr_writer :metadata
|
6
8
|
|
@@ -8,11 +10,12 @@ module Split
|
|
8
10
|
self.experiment = attrs.delete(:experiment)
|
9
11
|
self.alternative = attrs.delete(:alternative)
|
10
12
|
self.metadata = attrs.delete(:metadata)
|
13
|
+
self.goals = attrs.delete(:goals) || []
|
11
14
|
|
12
15
|
@user = attrs.delete(:user)
|
13
16
|
@options = attrs
|
14
17
|
|
15
|
-
@
|
18
|
+
@alternative_chosen = false
|
16
19
|
end
|
17
20
|
|
18
21
|
def metadata
|
@@ -33,7 +36,7 @@ module Split
|
|
33
36
|
end
|
34
37
|
end
|
35
38
|
|
36
|
-
def complete!(
|
39
|
+
def complete!(context = nil)
|
37
40
|
if alternative
|
38
41
|
if Array(goals).empty?
|
39
42
|
alternative.increment_completion
|
@@ -51,8 +54,9 @@ module Split
|
|
51
54
|
def choose!(context = nil)
|
52
55
|
@user.cleanup_old_experiments!
|
53
56
|
# Only run the process once
|
54
|
-
return alternative if @
|
57
|
+
return alternative if @alternative_chosen
|
55
58
|
|
59
|
+
new_participant = @user[@experiment.key].nil?
|
56
60
|
if override_is_alternative?
|
57
61
|
self.alternative = @options[:override]
|
58
62
|
if should_store_alternative? && !@user[@experiment.key]
|
@@ -70,19 +74,25 @@ module Split
|
|
70
74
|
else
|
71
75
|
self.alternative = @user[@experiment.key]
|
72
76
|
if alternative.nil?
|
73
|
-
|
77
|
+
if @experiment.cohorting_disabled?
|
78
|
+
self.alternative = @experiment.control
|
79
|
+
else
|
80
|
+
self.alternative = @experiment.next_alternative
|
74
81
|
|
75
|
-
|
76
|
-
|
82
|
+
# Increment the number of participants since we are actually choosing a new alternative
|
83
|
+
self.alternative.increment_participation
|
77
84
|
|
78
|
-
|
85
|
+
run_callback context, Split.configuration.on_trial_choose
|
86
|
+
end
|
79
87
|
end
|
80
88
|
end
|
81
89
|
end
|
82
90
|
|
83
|
-
|
84
|
-
|
85
|
-
|
91
|
+
new_participant_and_cohorting_disabled = new_participant && @experiment.cohorting_disabled?
|
92
|
+
|
93
|
+
@user[@experiment.key] = alternative.name unless @experiment.has_winner? || !should_store_alternative? || new_participant_and_cohorting_disabled
|
94
|
+
@alternative_chosen = true
|
95
|
+
run_callback context, Split.configuration.on_trial unless @options[:disabled] || Split.configuration.disabled? || new_participant_and_cohorting_disabled
|
86
96
|
alternative
|
87
97
|
end
|
88
98
|
|
data/lib/split/user.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'forwardable'
|
3
4
|
|
4
5
|
module Split
|
@@ -7,7 +8,7 @@ module Split
|
|
7
8
|
def_delegators :@user, :keys, :[], :[]=, :delete
|
8
9
|
attr_reader :user
|
9
10
|
|
10
|
-
def initialize(context, adapter=nil)
|
11
|
+
def initialize(context, adapter = nil)
|
11
12
|
@user = adapter || Split::Persistence.adapter.new(context)
|
12
13
|
@cleaned_up = false
|
13
14
|
end
|
@@ -27,7 +28,8 @@ module Split
|
|
27
28
|
def max_experiments_reached?(experiment_key)
|
28
29
|
if Split.configuration.allow_multiple_experiments == 'control'
|
29
30
|
experiments = active_experiments
|
30
|
-
|
31
|
+
experiment_key_without_version = key_without_version(experiment_key)
|
32
|
+
count_control = experiments.count {|k, v| k == experiment_key_without_version || v == 'control'}
|
31
33
|
experiments.size > count_control
|
32
34
|
else
|
33
35
|
!Split.configuration.allow_multiple_experiments &&
|
@@ -36,7 +38,7 @@ module Split
|
|
36
38
|
end
|
37
39
|
|
38
40
|
def cleanup_old_versions!(experiment)
|
39
|
-
keys = user.keys.select { |k| k
|
41
|
+
keys = user.keys.select { |k| key_without_version(k) == experiment.name }
|
40
42
|
keys_without_experiment(keys, experiment.key).each { |key| user.delete(key) }
|
41
43
|
end
|
42
44
|
|
@@ -52,6 +54,16 @@ module Split
|
|
52
54
|
experiment_pairs
|
53
55
|
end
|
54
56
|
|
57
|
+
def self.find(user_id, adapter)
|
58
|
+
adapter = adapter.is_a?(Symbol) ? Split::Persistence::ADAPTERS[adapter] : adapter
|
59
|
+
|
60
|
+
if adapter.respond_to?(:find)
|
61
|
+
User.new(nil, adapter.find(user_id))
|
62
|
+
else
|
63
|
+
nil
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
55
67
|
private
|
56
68
|
|
57
69
|
def keys_without_experiment(keys, experiment_key)
|
data/lib/split/version.rb
CHANGED