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.
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