split 3.4.1 → 4.0.4

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 (85) 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 +76 -0
  5. data/.rubocop.yml +177 -4
  6. data/CHANGELOG.md +87 -0
  7. data/CONTRIBUTING.md +1 -1
  8. data/Gemfile +2 -1
  9. data/README.md +37 -9
  10. data/Rakefile +5 -5
  11. data/gemfiles/5.2.gemfile +1 -3
  12. data/gemfiles/6.0.gemfile +1 -3
  13. data/gemfiles/{5.0.gemfile → 6.1.gemfile} +2 -4
  14. data/gemfiles/{5.1.gemfile → 7.0.gemfile} +2 -4
  15. data/lib/split/algorithms/block_randomization.rb +6 -6
  16. data/lib/split/algorithms/weighted_sample.rb +2 -1
  17. data/lib/split/algorithms/whiplash.rb +17 -18
  18. data/lib/split/algorithms.rb +14 -0
  19. data/lib/split/alternative.rb +22 -22
  20. data/lib/split/cache.rb +27 -0
  21. data/lib/split/combined_experiments_helper.rb +5 -4
  22. data/lib/split/configuration.rb +89 -94
  23. data/lib/split/dashboard/helpers.rb +7 -7
  24. data/lib/split/dashboard/pagination_helpers.rb +54 -54
  25. data/lib/split/dashboard/paginator.rb +1 -0
  26. data/lib/split/dashboard/public/dashboard.js +10 -0
  27. data/lib/split/dashboard/public/style.css +10 -2
  28. data/lib/split/dashboard/views/_controls.erb +13 -0
  29. data/lib/split/dashboard/views/_experiment.erb +2 -1
  30. data/lib/split/dashboard/views/index.erb +19 -4
  31. data/lib/split/dashboard.rb +42 -21
  32. data/lib/split/encapsulated_helper.rb +15 -8
  33. data/lib/split/engine.rb +1 -0
  34. data/lib/split/exceptions.rb +1 -0
  35. data/lib/split/experiment.rb +151 -124
  36. data/lib/split/experiment_catalog.rb +7 -8
  37. data/lib/split/extensions/string.rb +2 -1
  38. data/lib/split/goals_collection.rb +9 -10
  39. data/lib/split/helper.rb +50 -23
  40. data/lib/split/metric.rb +6 -6
  41. data/lib/split/persistence/cookie_adapter.rb +46 -44
  42. data/lib/split/persistence/dual_adapter.rb +7 -8
  43. data/lib/split/persistence/redis_adapter.rb +8 -4
  44. data/lib/split/persistence/session_adapter.rb +1 -2
  45. data/lib/split/persistence.rb +8 -6
  46. data/lib/split/redis_interface.rb +15 -29
  47. data/lib/split/trial.rb +43 -34
  48. data/lib/split/user.rb +25 -14
  49. data/lib/split/version.rb +2 -4
  50. data/lib/split/zscore.rb +2 -3
  51. data/lib/split.rb +34 -27
  52. data/spec/algorithms/block_randomization_spec.rb +6 -5
  53. data/spec/algorithms/weighted_sample_spec.rb +6 -5
  54. data/spec/algorithms/whiplash_spec.rb +4 -5
  55. data/spec/alternative_spec.rb +35 -36
  56. data/spec/cache_spec.rb +84 -0
  57. data/spec/combined_experiments_helper_spec.rb +18 -17
  58. data/spec/configuration_spec.rb +41 -45
  59. data/spec/dashboard/pagination_helpers_spec.rb +69 -67
  60. data/spec/dashboard/paginator_spec.rb +10 -9
  61. data/spec/dashboard_helpers_spec.rb +19 -18
  62. data/spec/dashboard_spec.rb +122 -38
  63. data/spec/encapsulated_helper_spec.rb +46 -22
  64. data/spec/experiment_catalog_spec.rb +14 -13
  65. data/spec/experiment_spec.rb +198 -118
  66. data/spec/goals_collection_spec.rb +18 -16
  67. data/spec/helper_spec.rb +454 -385
  68. data/spec/metric_spec.rb +14 -14
  69. data/spec/persistence/cookie_adapter_spec.rb +26 -11
  70. data/spec/persistence/dual_adapter_spec.rb +71 -71
  71. data/spec/persistence/redis_adapter_spec.rb +35 -27
  72. data/spec/persistence/session_adapter_spec.rb +2 -3
  73. data/spec/persistence_spec.rb +1 -2
  74. data/spec/redis_interface_spec.rb +25 -82
  75. data/spec/spec_helper.rb +35 -24
  76. data/spec/split_spec.rb +11 -11
  77. data/spec/support/cookies_mock.rb +1 -2
  78. data/spec/trial_spec.rb +102 -75
  79. data/spec/user_spec.rb +60 -29
  80. data/split.gemspec +22 -21
  81. metadata +43 -40
  82. data/.rubocop_todo.yml +0 -679
  83. data/.travis.yml +0 -60
  84. data/Appraisals +0 -19
  85. data/gemfiles/4.2.gemfile +0 -9
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"
@@ -11,9 +12,9 @@ module Split
11
12
  alternative = if Split.configuration.enabled && !exclude_visitor?
12
13
  experiment.save
13
14
  raise(Split::InvalidExperimentsFormatError) unless (Split.configuration.experiments || {}).fetch(experiment.name.to_sym, {})[:combined_experiments].nil?
14
- trial = Trial.new(:user => ab_user, :experiment => experiment,
15
- :override => override_alternative(experiment.name), :exclude => exclude_visitor?,
16
- :disabled => split_generically_disabled?)
15
+ trial = Trial.new(user: ab_user, experiment: experiment,
16
+ override: override_alternative(experiment.name), exclude: exclude_visitor?,
17
+ disabled: split_generically_disabled?)
17
18
  alt = trial.choose!(self)
18
19
  alt ? alt.name : nil
19
20
  else
@@ -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
@@ -43,17 +44,22 @@ module Split
43
44
  ab_user.delete(experiment.key)
44
45
  end
45
46
 
46
- def finish_experiment(experiment, options = {:reset => true})
47
+ def finish_experiment(experiment, options = { reset: true })
47
48
  return false if active_experiments[experiment.name].nil?
48
49
  return true if experiment.has_winner?
49
50
  should_reset = experiment.resettable? && options[:reset]
50
51
  if ab_user[experiment.finished_key] && !should_reset
51
- return true
52
+ 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)
@@ -63,14 +69,15 @@ module Split
63
69
  end
64
70
  end
65
71
 
66
- def ab_finished(metric_descriptor, options = {:reset => true})
72
+ def ab_finished(metric_descriptor, options = { reset: true })
67
73
  return if exclude_visitor? || Split.configuration.disabled?
68
74
  metric_descriptor, goals = normalize_metric(metric_descriptor)
69
75
  experiments = Metric.possible_experiments(metric_descriptor)
70
76
 
71
77
  if experiments.any?
72
78
  experiments.each do |experiment|
73
- finish_experiment(experiment, options.merge(:goals => goals))
79
+ next if override_present?(experiment.key)
80
+ finish_experiment(experiment, options.merge(goals: goals))
74
81
  end
75
82
  end
76
83
  rescue => e
@@ -79,7 +86,7 @@ module Split
79
86
  end
80
87
 
81
88
  def ab_record_extra_info(metric_descriptor, key, value = 1)
82
- return if exclude_visitor? || Split.configuration.disabled?
89
+ return if exclude_visitor? || Split.configuration.disabled? || value.nil?
83
90
  metric_descriptor, _ = normalize_metric(metric_descriptor)
84
91
  experiments = Metric.possible_experiments(metric_descriptor)
85
92
 
@@ -88,7 +95,7 @@ module Split
88
95
  alternative_name = ab_user[experiment.key]
89
96
 
90
97
  if alternative_name
91
- alternative = experiment.alternatives.find{|alt| alt.name == alternative_name}
98
+ alternative = experiment.alternatives.find { |alt| alt.name == alternative_name }
92
99
  alternative.record_extra_info(key, value) if alternative
93
100
  end
94
101
  end
@@ -98,24 +105,36 @@ module Split
98
105
  Split.configuration.db_failover_on_db_error.call(e)
99
106
  end
100
107
 
101
- def ab_active_experiments()
108
+ def ab_active_experiments
102
109
  ab_user.active_experiments
103
110
  rescue => e
104
111
  raise unless Split.configuration.db_failover
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)
114
- defined?(params) && params[OVERRIDE_PARAM_NAME] && params[OVERRIDE_PARAM_NAME][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)
124
+ params_present? && params[OVERRIDE_PARAM_NAME] && params[OVERRIDE_PARAM_NAME][experiment_name]
125
+ end
126
+
127
+ def override_alternative_by_cookies(experiment_name)
128
+ return unless request_present?
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
115
134
  end
116
135
 
117
136
  def split_generically_disabled?
118
- defined?(params) && params['SPLIT_DISABLE']
137
+ params_present? && params["SPLIT_DISABLE"]
119
138
  end
120
139
 
121
140
  def ab_user
@@ -123,26 +142,34 @@ module Split
123
142
  end
124
143
 
125
144
  def exclude_visitor?
126
- defined?(request) && (instance_exec(request, &Split.configuration.ignore_filter) || is_ignored_ip_address? || is_robot? || is_preview?)
145
+ request_present? && (instance_exec(request, &Split.configuration.ignore_filter) || is_ignored_ip_address? || is_robot? || is_preview?)
127
146
  end
128
147
 
129
148
  def is_robot?
130
- defined?(request) && request.user_agent =~ Split.configuration.robot_regex
149
+ request_present? && request.user_agent =~ Split.configuration.robot_regex
131
150
  end
132
151
 
133
152
  def is_preview?
134
- defined?(request) && defined?(request.headers) && request.headers['x-purpose'] == 'preview'
153
+ request_present? && defined?(request.headers) && request.headers["x-purpose"] == "preview"
135
154
  end
136
155
 
137
156
  def is_ignored_ip_address?
138
157
  return false if Split.configuration.ignore_ip_addresses.empty?
139
158
 
140
159
  Split.configuration.ignore_ip_addresses.each do |ip|
141
- return true if defined?(request) && (request.ip == ip || (ip.class == Regexp && request.ip =~ ip))
160
+ return true if request_present? && (request.ip == ip || (ip.class == Regexp && request.ip =~ ip))
142
161
  end
143
162
  false
144
163
  end
145
164
 
165
+ def params_present?
166
+ defined?(params) && params
167
+ end
168
+
169
+ def request_present?
170
+ defined?(request) && request
171
+ end
172
+
146
173
  def active_experiments
147
174
  ab_user.active_experiments
148
175
  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
@@ -15,13 +16,13 @@ module Split
15
16
  def self.load_from_redis(name)
16
17
  metric = Split.redis.hget(:metrics, name)
17
18
  if metric
18
- experiment_names = metric.split(',')
19
+ experiment_names = metric.split(",")
19
20
 
20
21
  experiments = experiment_names.collect do |experiment_name|
21
22
  Split::ExperimentCatalog.find(experiment_name)
22
23
  end
23
24
 
24
- Split::Metric.new(:name => name, :experiments => experiments)
25
+ Split::Metric.new(name: name, experiments: experiments)
25
26
  else
26
27
  nil
27
28
  end
@@ -30,7 +31,7 @@ module Split
30
31
  def self.load_from_configuration(name)
31
32
  metrics = Split.configuration.metrics
32
33
  if metrics && metrics[name]
33
- Split::Metric.new(:experiments => metrics[name], :name => name)
34
+ Split::Metric.new(experiments: metrics[name], name: name)
34
35
  else
35
36
  nil
36
37
  end
@@ -76,7 +77,7 @@ module Split
76
77
  end
77
78
 
78
79
  def save
79
- Split.redis.hset(:metrics, name, experiments.map(&:name).join(','))
80
+ Split.redis.hset(:metrics, name, experiments.map(&:name).join(","))
80
81
  end
81
82
 
82
83
  def complete!
@@ -96,6 +97,5 @@ module Split
96
97
  return metric_name, goals
97
98
  end
98
99
  private_class_method :normalize_metric
99
-
100
100
  end
101
101
  end
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "json"
3
4
 
4
5
  module Split
5
6
  module Persistence
6
7
  class CookieAdapter
7
-
8
8
  def initialize(context)
9
9
  @context = context
10
10
  @request, @response = context.request, context.response
@@ -29,50 +29,49 @@ module Split
29
29
  end
30
30
 
31
31
  private
32
+ def set_cookie(value = {})
33
+ cookie_key = :split.to_s
34
+ cookie_value = default_options.merge(value: JSON.generate(value))
35
+ if action_dispatch?
36
+ # The "send" is necessary when we call ab_test from the controller
37
+ # and thus @context is a rails controller, because then "cookies" is
38
+ # a private method.
39
+ @context.send(:cookies)[cookie_key] = cookie_value
40
+ else
41
+ set_cookie_via_rack(cookie_key, cookie_value)
42
+ end
43
+ end
32
44
 
33
- def set_cookie(value = {})
34
- cookie_key = :split.to_s
35
- cookie_value = default_options.merge(value: JSON.generate(value))
36
- if action_dispatch?
37
- # The "send" is necessary when we call ab_test from the controller
38
- # and thus @context is a rails controller, because then "cookies" is
39
- # a private method.
40
- @context.send(:cookies)[cookie_key] = cookie_value
41
- else
42
- set_cookie_via_rack(cookie_key, cookie_value)
45
+ def default_options
46
+ { expires: @expires, path: "/", domain: cookie_domain_config }.compact
43
47
  end
44
- end
45
48
 
46
- def default_options
47
- { expires: @expires, path: '/' }
48
- end
49
+ def set_cookie_via_rack(key, value)
50
+ delete_cookie_header!(@response.header, key, value)
51
+ Rack::Utils.set_cookie_header!(@response.header, key, value)
52
+ end
49
53
 
50
- def set_cookie_via_rack(key, value)
51
- delete_cookie_header!(@response.header, key, value)
52
- Rack::Utils.set_cookie_header!(@response.header, key, value)
53
- end
54
+ # Use Rack::Utils#make_delete_cookie_header after Rack 2.0.0
55
+ def delete_cookie_header!(header, key, value)
56
+ cookie_header = header["Set-Cookie"]
57
+ case cookie_header
58
+ when nil, ""
59
+ cookies = []
60
+ when String
61
+ cookies = cookie_header.split("\n")
62
+ when Array
63
+ cookies = cookie_header
64
+ end
54
65
 
55
- # Use Rack::Utils#make_delete_cookie_header after Rack 2.0.0
56
- def delete_cookie_header!(header, key, value)
57
- cookie_header = header['Set-Cookie']
58
- case cookie_header
59
- when nil, ''
60
- cookies = []
61
- when String
62
- cookies = cookie_header.split("\n")
63
- when Array
64
- cookies = cookie_header
66
+ cookies.reject! { |cookie| cookie =~ /\A#{Rack::Utils.escape(key)}=/ }
67
+ header["Set-Cookie"] = cookies.join("\n")
65
68
  end
66
69
 
67
- cookies.reject! { |cookie| cookie =~ /\A#{Rack::Utils.escape(key)}=/ }
68
- header['Set-Cookie'] = cookies.join("\n")
69
- end
70
-
71
- def hash
72
- @hash ||= begin
73
- if cookies = @cookies[:split.to_s]
70
+ def hash
71
+ @hash ||= if cookies = @cookies[:split.to_s]
74
72
  begin
75
- JSON.parse(cookies)
73
+ parsed = JSON.parse(cookies)
74
+ parsed.is_a?(Hash) ? parsed : {}
76
75
  rescue JSON::ParserError
77
76
  {}
78
77
  end
@@ -80,15 +79,18 @@ module Split
80
79
  {}
81
80
  end
82
81
  end
83
- end
84
82
 
85
- def cookie_length_config
86
- Split.configuration.persistence_cookie_length
87
- end
83
+ def cookie_length_config
84
+ Split.configuration.persistence_cookie_length
85
+ end
88
86
 
89
- def action_dispatch?
90
- defined?(Rails) && @response.is_a?(ActionDispatch::Response)
91
- end
87
+ def cookie_domain_config
88
+ Split.configuration.persistence_cookie_domain
89
+ end
90
+
91
+ def action_dispatch?
92
+ defined?(Rails) && @response.is_a?(ActionDispatch::Response)
93
+ end
92
94
  end
93
95
  end
94
96
  end
@@ -3,7 +3,7 @@
3
3
  module Split
4
4
  module Persistence
5
5
  class DualAdapter
6
- def self.with_config(options={})
6
+ def self.with_config(options = {})
7
7
  self.config.merge!(options)
8
8
  self
9
9
  end
@@ -72,14 +72,13 @@ module Split
72
72
  end
73
73
 
74
74
  private
75
+ def decrement_participation?(old_value, value)
76
+ !old_value.nil? && !value.nil? && old_value != value
77
+ end
75
78
 
76
- def decrement_participation?(old_value, value)
77
- !old_value.nil? && !value.nil? && old_value != value
78
- end
79
-
80
- def decrement_participation(key, value)
81
- Split.redis.hincrby("#{key}:#{value}", 'participant_count', -1)
82
- end
79
+ def decrement_participation(key, value)
80
+ Split.redis.hincrby("#{key}:#{value}", "participant_count", -1)
81
+ end
83
82
  end
84
83
  end
85
84
  end
@@ -1,8 +1,9 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Split
3
4
  module Persistence
4
5
  class RedisAdapter
5
- DEFAULT_CONFIG = {:namespace => 'persistence'}.freeze
6
+ DEFAULT_CONFIG = { namespace: "persistence" }.freeze
6
7
 
7
8
  attr_reader :redis_key
8
9
 
@@ -26,7 +27,7 @@ module Split
26
27
  end
27
28
 
28
29
  def []=(field, value)
29
- Split.redis.hset(redis_key, field, value)
30
+ Split.redis.hset(redis_key, field, value.to_s)
30
31
  expire_seconds = self.class.config[:expire_seconds]
31
32
  Split.redis.expire(redis_key, expire_seconds) if expire_seconds
32
33
  end
@@ -39,7 +40,11 @@ module Split
39
40
  Split.redis.hkeys(redis_key)
40
41
  end
41
42
 
42
- def self.with_config(options={})
43
+ def self.find(user_id)
44
+ new(nil, user_id)
45
+ end
46
+
47
+ def self.with_config(options = {})
43
48
  self.config.merge!(options)
44
49
  self
45
50
  end
@@ -51,7 +56,6 @@ module Split
51
56
  def self.reset_config!
52
57
  @config = DEFAULT_CONFIG.dup
53
58
  end
54
-
55
59
  end
56
60
  end
57
61
  end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Split
3
4
  module Persistence
4
5
  class SessionAdapter
5
-
6
6
  def initialize(context)
7
7
  @session = context.session
8
8
  @session[:split] ||= {}
@@ -23,7 +23,6 @@ module Split
23
23
  def keys
24
24
  @session[:split].keys
25
25
  end
26
-
27
26
  end
28
27
  end
29
28
  end
@@ -2,14 +2,16 @@
2
2
 
3
3
  module Split
4
4
  module Persistence
5
- require 'split/persistence/cookie_adapter'
6
- require 'split/persistence/dual_adapter'
7
- require 'split/persistence/redis_adapter'
8
- require 'split/persistence/session_adapter'
5
+ require "split/persistence/cookie_adapter"
6
+ require "split/persistence/dual_adapter"
7
+ require "split/persistence/redis_adapter"
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,44 +8,29 @@ 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
+ tmp_list += redis_namespace_used? ? "{#{Split.redis.namespace}:#{list_name}}" : "{#{list_name}}"
15
+ multi.rpush(tmp_list, list_values)
16
+ multi.rename(tmp_list, list_name)
16
17
  end
17
18
  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
19
 
30
- def list_length(list_name)
31
- redis.llen(list_name)
32
- end
33
-
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)
20
+ list_values
40
21
  end
41
22
 
42
23
  def add_to_set(set_name, value)
43
- redis.sadd(set_name, value) unless redis.sismember(set_name, value)
24
+ return redis.sadd?(set_name, value) if redis.respond_to?(:sadd?)
25
+
26
+ redis.sadd(set_name, value)
44
27
  end
45
28
 
46
29
  private
30
+ attr_accessor :redis
47
31
 
48
- attr_accessor :redis
32
+ def redis_namespace_used?
33
+ Redis.const_defined?("Namespace") && Split.redis.is_a?(Redis::Namespace)
34
+ end
49
35
  end
50
36
  end
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
@@ -21,24 +24,24 @@ module Split
21
24
 
22
25
  def alternative
23
26
  @alternative ||= if @experiment.has_winner?
24
- @experiment.winner
25
- end
27
+ @experiment.winner
28
+ end
26
29
  end
27
30
 
28
31
  def alternative=(alternative)
29
32
  @alternative = if alternative.kind_of?(Split::Alternative)
30
33
  alternative
31
34
  else
32
- @experiment.alternatives.find{|a| a.name == alternative }
35
+ @experiment.alternatives.find { |a| a.name == alternative }
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
40
43
  else
41
- Array(goals).each {|g| alternative.increment_completion(g) }
44
+ Array(goals).each { |g| alternative.increment_completion(g) }
42
45
  end
43
46
 
44
47
  run_callback context, Split.configuration.on_trial_complete
@@ -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,48 +74,53 @@ 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
 
89
99
  private
100
+ def run_callback(context, callback_name)
101
+ context.send(callback_name, self) if callback_name && context.respond_to?(callback_name, true)
102
+ end
90
103
 
91
- def run_callback(context, callback_name)
92
- context.send(callback_name, self) if callback_name && context.respond_to?(callback_name, true)
93
- end
94
-
95
- def override_is_alternative?
96
- @experiment.alternatives.map(&:name).include?(@options[:override])
97
- end
104
+ def override_is_alternative?
105
+ @experiment.alternatives.map(&:name).include?(@options[:override])
106
+ end
98
107
 
99
- def should_store_alternative?
100
- if @options[:override] || @options[:disabled]
101
- Split.configuration.store_override
102
- else
103
- !exclude_user?
108
+ def should_store_alternative?
109
+ if @options[:override] || @options[:disabled]
110
+ Split.configuration.store_override
111
+ else
112
+ !exclude_user?
113
+ end
104
114
  end
105
- end
106
115
 
107
- def cleanup_old_versions
108
- if @experiment.version > 0
109
- @user.cleanup_old_versions!(@experiment)
116
+ def cleanup_old_versions
117
+ if @experiment.version > 0
118
+ @user.cleanup_old_versions!(@experiment)
119
+ end
110
120
  end
111
- end
112
121
 
113
- def exclude_user?
114
- @options[:exclude] || @experiment.start_time.nil? || @user.max_experiments_reached?(@experiment.key)
115
- end
122
+ def exclude_user?
123
+ @options[:exclude] || @experiment.start_time.nil? || @user.max_experiments_reached?(@experiment.key)
124
+ end
116
125
  end
117
126
  end