split 3.4.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 +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