split 3.4.0 → 4.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/dependabot.yml +7 -0
  4. data/.github/workflows/ci.yml +71 -0
  5. data/.rubocop.yml +177 -1
  6. data/.rubocop_todo.yml +40 -493
  7. data/CHANGELOG.md +41 -0
  8. data/Gemfile +1 -0
  9. data/README.md +26 -6
  10. data/Rakefile +1 -0
  11. data/gemfiles/{4.2.gemfile → 6.1.gemfile} +1 -1
  12. data/gemfiles/7.0.gemfile +9 -0
  13. data/lib/split/algorithms/block_randomization.rb +1 -0
  14. data/lib/split/algorithms/weighted_sample.rb +2 -1
  15. data/lib/split/algorithms/whiplash.rb +3 -2
  16. data/lib/split/alternative.rb +1 -0
  17. data/lib/split/cache.rb +28 -0
  18. data/lib/split/combined_experiments_helper.rb +1 -0
  19. data/lib/split/configuration.rb +8 -12
  20. data/lib/split/dashboard/helpers.rb +1 -0
  21. data/lib/split/dashboard/pagination_helpers.rb +1 -0
  22. data/lib/split/dashboard/paginator.rb +1 -0
  23. data/lib/split/dashboard/public/dashboard.js +10 -0
  24. data/lib/split/dashboard/public/style.css +5 -0
  25. data/lib/split/dashboard/views/_controls.erb +13 -0
  26. data/lib/split/dashboard.rb +17 -2
  27. data/lib/split/encapsulated_helper.rb +3 -2
  28. data/lib/split/engine.rb +5 -4
  29. data/lib/split/exceptions.rb +1 -0
  30. data/lib/split/experiment.rb +82 -60
  31. data/lib/split/experiment_catalog.rb +1 -3
  32. data/lib/split/extensions/string.rb +1 -0
  33. data/lib/split/goals_collection.rb +1 -0
  34. data/lib/split/helper.rb +26 -7
  35. data/lib/split/metric.rb +2 -1
  36. data/lib/split/persistence/cookie_adapter.rb +6 -1
  37. data/lib/split/persistence/redis_adapter.rb +5 -0
  38. data/lib/split/persistence/session_adapter.rb +1 -0
  39. data/lib/split/persistence.rb +4 -2
  40. data/lib/split/redis_interface.rb +8 -28
  41. data/lib/split/trial.rb +20 -10
  42. data/lib/split/user.rb +15 -3
  43. data/lib/split/version.rb +2 -4
  44. data/lib/split/zscore.rb +1 -0
  45. data/lib/split.rb +9 -3
  46. data/spec/alternative_spec.rb +1 -1
  47. data/spec/cache_spec.rb +88 -0
  48. data/spec/configuration_spec.rb +17 -15
  49. data/spec/dashboard_spec.rb +45 -5
  50. data/spec/encapsulated_helper_spec.rb +1 -1
  51. data/spec/experiment_spec.rb +78 -13
  52. data/spec/goals_collection_spec.rb +1 -1
  53. data/spec/helper_spec.rb +68 -32
  54. data/spec/persistence/cookie_adapter_spec.rb +1 -1
  55. data/spec/persistence/redis_adapter_spec.rb +9 -0
  56. data/spec/redis_interface_spec.rb +0 -69
  57. data/spec/spec_helper.rb +5 -6
  58. data/spec/trial_spec.rb +45 -19
  59. data/spec/user_spec.rb +34 -3
  60. data/split.gemspec +7 -7
  61. metadata +27 -35
  62. data/.travis.yml +0 -60
@@ -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
- alternatives = extract_alternatives_from_options(options)
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
- self.alternatives = options[:alternatives]
44
- self.goals = options[:goals]
45
- self.resettable = options[:resettable]
46
- self.algorithm = options[:algorithm]
47
- self.metadata = options[:metadata]
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.fetch(:resettable, true)
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.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
- !redis.exists(name)
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
- experiment_winner = redis.hget(:experiment_winner, name)
139
- if experiment_winner
140
- Split::Alternative.new(experiment_winner, name)
141
- else
142
- nil
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
- t = redis.hget(:experiment_start_times, @name)
175
- if t
176
- # Check if stored time is an integer
177
- if t =~ /^[-+]?[0-9]+$/
178
- Time.at(t.to_i)
179
- else
180
- Time.parse(t)
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 = rand.beta(alpha, beta)
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 = case redis.type(@name)
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
- redis.set(metadata_key, @metadata.to_json) unless @metadata.nil?
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
- existing_alternatives = load_alternatives_from_redis
471
- existing_goals = Split::GoalsCollection.new(@name).load_from_redis
472
- existing_metadata = load_metadata_from_redis
473
- existing_alternatives.map(&:to_s) != @alternatives.map(&:to_s) ||
474
- existing_goals != @goals ||
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
- return unless Split.redis.exists(name)
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
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  class String
3
4
  # Constatntize is often provided by ActiveSupport, but ActiveSupport is not a dependency of Split.
4
5
  unless method_defined?(:constantize)
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Split
3
4
  class GoalsCollection
4
5
 
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 = trial ? trial.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(:user => ab_user, :experiment => experiment,
55
- :alternative => alternative_name)
56
- trial.complete!(options[:goals], self)
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
- override_alternative(experiment_name)
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
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Split
3
4
  module Persistence
4
5
  class SessionAdapter
@@ -8,8 +8,10 @@ module Split
8
8
  require 'split/persistence/session_adapter'
9
9
 
10
10
  ADAPTERS = {
11
- :cookie => Split::Persistence::CookieAdapter,
12
- :session => Split::Persistence::SessionAdapter
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
- max_index = list_length(list_name) - 1
11
- list_values.each_with_index do |value, index|
12
- if index > max_index
13
- add_to_list(list_name, value)
14
- else
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
- def remove_last_item_from_list(list_name)
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) unless redis.sismember(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
- @alternative_choosen = false
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!(goals=[], context = nil)
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 @alternative_choosen
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
- self.alternative = @experiment.next_alternative
77
+ if @experiment.cohorting_disabled?
78
+ self.alternative = @experiment.control
79
+ else
80
+ self.alternative = @experiment.next_alternative
74
81
 
75
- # Increment the number of participants since we are actually choosing a new alternative
76
- self.alternative.increment_participation
82
+ # Increment the number of participants since we are actually choosing a new alternative
83
+ self.alternative.increment_participation
77
84
 
78
- run_callback context, Split.configuration.on_trial_choose
85
+ run_callback context, Split.configuration.on_trial_choose
86
+ end
79
87
  end
80
88
  end
81
89
  end
82
90
 
83
- @user[@experiment.key] = alternative.name if !@experiment.has_winner? && should_store_alternative?
84
- @alternative_choosen = true
85
- run_callback context, Split.configuration.on_trial unless @options[:disabled] || Split.configuration.disabled?
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
- count_control = experiments.count {|k,v| k == experiment_key || v == 'control'}
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.match(Regexp.new(experiment.name)) }
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
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Split
3
- MAJOR = 3
4
- MINOR = 4
5
- PATCH = 0
6
- VERSION = [MAJOR, MINOR, PATCH].join('.')
4
+ VERSION = "4.0.1"
7
5
  end
data/lib/split/zscore.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Split
3
4
  class Zscore
4
5