split 3.3.2 → 4.0.5

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 (89) hide show
  1. checksums.yaml +4 -4
  2. data/.eslintrc +1 -1
  3. data/.github/FUNDING.yml +1 -0
  4. data/.github/ISSUE_TEMPLATE/bug_report.md +24 -0
  5. data/.github/dependabot.yml +7 -0
  6. data/.github/workflows/ci.yml +63 -0
  7. data/.rspec +1 -0
  8. data/.rubocop.yml +67 -1043
  9. data/CHANGELOG.md +121 -0
  10. data/CODE_OF_CONDUCT.md +3 -3
  11. data/CONTRIBUTING.md +1 -1
  12. data/Gemfile +6 -1
  13. data/README.md +51 -21
  14. data/Rakefile +6 -5
  15. data/lib/split/algorithms/block_randomization.rb +7 -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 +25 -25
  20. data/lib/split/cache.rb +27 -0
  21. data/lib/split/combined_experiments_helper.rb +5 -4
  22. data/lib/split/configuration.rb +94 -96
  23. data/lib/split/dashboard/helpers.rb +7 -7
  24. data/lib/split/dashboard/pagination_helpers.rb +56 -57
  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/views/layout.erb +1 -1
  32. data/lib/split/dashboard.rb +46 -21
  33. data/lib/split/encapsulated_helper.rb +15 -8
  34. data/lib/split/engine.rb +7 -4
  35. data/lib/split/exceptions.rb +1 -0
  36. data/lib/split/experiment.rb +160 -122
  37. data/lib/split/experiment_catalog.rb +7 -8
  38. data/lib/split/extensions/string.rb +2 -1
  39. data/lib/split/goals_collection.rb +10 -10
  40. data/lib/split/helper.rb +52 -24
  41. data/lib/split/metric.rb +6 -6
  42. data/lib/split/persistence/cookie_adapter.rb +47 -44
  43. data/lib/split/persistence/dual_adapter.rb +53 -12
  44. data/lib/split/persistence/redis_adapter.rb +8 -4
  45. data/lib/split/persistence/session_adapter.rb +1 -2
  46. data/lib/split/persistence.rb +8 -6
  47. data/lib/split/redis_interface.rb +16 -29
  48. data/lib/split/trial.rb +44 -35
  49. data/lib/split/user.rb +30 -15
  50. data/lib/split/version.rb +2 -4
  51. data/lib/split/zscore.rb +2 -3
  52. data/lib/split.rb +35 -28
  53. data/spec/algorithms/block_randomization_spec.rb +6 -5
  54. data/spec/algorithms/weighted_sample_spec.rb +6 -5
  55. data/spec/algorithms/whiplash_spec.rb +4 -5
  56. data/spec/alternative_spec.rb +35 -36
  57. data/spec/cache_spec.rb +84 -0
  58. data/spec/combined_experiments_helper_spec.rb +18 -17
  59. data/spec/configuration_spec.rb +41 -45
  60. data/spec/dashboard/pagination_helpers_spec.rb +71 -67
  61. data/spec/dashboard/paginator_spec.rb +10 -9
  62. data/spec/dashboard_helpers_spec.rb +19 -18
  63. data/spec/dashboard_spec.rb +153 -48
  64. data/spec/encapsulated_helper_spec.rb +47 -23
  65. data/spec/experiment_catalog_spec.rb +14 -13
  66. data/spec/experiment_spec.rb +224 -111
  67. data/spec/goals_collection_spec.rb +18 -16
  68. data/spec/helper_spec.rb +531 -424
  69. data/spec/metric_spec.rb +14 -14
  70. data/spec/persistence/cookie_adapter_spec.rb +26 -11
  71. data/spec/persistence/dual_adapter_spec.rb +158 -66
  72. data/spec/persistence/redis_adapter_spec.rb +35 -27
  73. data/spec/persistence/session_adapter_spec.rb +2 -3
  74. data/spec/persistence_spec.rb +1 -2
  75. data/spec/redis_interface_spec.rb +25 -82
  76. data/spec/spec_helper.rb +38 -24
  77. data/spec/split_spec.rb +11 -11
  78. data/spec/support/cookies_mock.rb +1 -2
  79. data/spec/trial_spec.rb +102 -75
  80. data/spec/user_spec.rb +69 -27
  81. data/split.gemspec +26 -23
  82. metadata +68 -42
  83. data/.travis.yml +0 -66
  84. data/Appraisals +0 -19
  85. data/gemfiles/4.2.gemfile +0 -9
  86. data/gemfiles/5.0.gemfile +0 -9
  87. data/gemfiles/5.1.gemfile +0 -9
  88. data/gemfiles/5.2.gemfile +0 -9
  89. data/gemfiles/6.0.gemfile +0 -9
@@ -1,33 +1,33 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Split
3
4
  class ExperimentCatalog
4
5
  # Return all experiments
5
6
  def self.all
6
7
  # Call compact to prevent nil experiments from being returned -- seems to happen during gem upgrades
7
- Split.redis.smembers(:experiments).map {|e| find(e)}.compact
8
+ Split.redis.smembers(:experiments).map { |e| find(e) }.compact
8
9
  end
9
10
 
10
11
  # Return experiments without a winner (considered "active") first
11
12
  def self.all_active_first
12
- all.partition{|e| not e.winner}.map{|es| es.sort_by(&:name)}.flatten
13
+ all.partition { |e| not e.winner }.map { |es| es.sort_by(&:name) }.flatten
13
14
  end
14
15
 
15
16
  def self.find(name)
16
- return unless Split.redis.exists(name)
17
- Experiment.new(name).tap { |exp| exp.load_from_redis }
17
+ Experiment.find(name)
18
18
  end
19
19
 
20
20
  def self.find_or_initialize(metric_descriptor, control = nil, *alternatives)
21
21
  # Check if array is passed to ab_test
22
22
  # e.g. ab_test('name', ['Alt 1', 'Alt 2', 'Alt 3'])
23
- if control.is_a? Array and alternatives.length.zero?
23
+ if control.is_a?(Array) && alternatives.length.zero?
24
24
  control, alternatives = control.first, control[1..-1]
25
25
  end
26
26
 
27
27
  experiment_name_with_version, goals = normalize_experiment(metric_descriptor)
28
- experiment_name = experiment_name_with_version.to_s.split(':')[0]
28
+ experiment_name = experiment_name_with_version.to_s.split(":")[0]
29
29
  Split::Experiment.new(experiment_name,
30
- :alternatives => [control].compact + alternatives, :goals => goals)
30
+ alternatives: [control].compact + alternatives, goals: goals)
31
31
  end
32
32
 
33
33
  def self.find_or_create(metric_descriptor, control = nil, *alternatives)
@@ -46,6 +46,5 @@ module Split
46
46
  return experiment_name, goals
47
47
  end
48
48
  private_class_method :normalize_experiment
49
-
50
49
  end
51
50
  end
@@ -1,9 +1,10 @@
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)
5
6
  def constantize
6
- names = self.split('::')
7
+ names = self.split("::")
7
8
  names.shift if names.empty? || names.first.empty?
8
9
 
9
10
  constant = Object
@@ -1,7 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Split
2
4
  class GoalsCollection
3
-
4
- def initialize(experiment_name, goals=nil)
5
+ def initialize(experiment_name, goals = nil)
5
6
  @experiment_name = experiment_name
6
7
  @goals = goals
7
8
  end
@@ -13,10 +14,10 @@ module Split
13
14
  def load_from_configuration
14
15
  goals = Split.configuration.experiment_for(@experiment_name)[:goals]
15
16
 
16
- if goals.nil?
17
- goals = []
18
- else
17
+ if goals
19
18
  goals.flatten
19
+ else
20
+ []
20
21
  end
21
22
  end
22
23
 
@@ -27,7 +28,7 @@ module Split
27
28
 
28
29
  def validate!
29
30
  unless @goals.nil? || @goals.kind_of?(Array)
30
- raise ArgumentError, 'Goals must be an array'
31
+ raise ArgumentError, "Goals must be an array"
31
32
  end
32
33
  end
33
34
 
@@ -36,9 +37,8 @@ module Split
36
37
  end
37
38
 
38
39
  private
39
-
40
- def goals_key
41
- "#{@experiment_name}:goals"
42
- end
40
+ def goals_key
41
+ "#{@experiment_name}:goals"
42
+ end
43
43
  end
44
44
  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"
@@ -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,16 +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 })
48
+ return false if active_experiments[experiment.name].nil?
47
49
  return true if experiment.has_winner?
48
50
  should_reset = experiment.resettable? && options[:reset]
49
51
  if ab_user[experiment.finished_key] && !should_reset
50
- return true
52
+ true
51
53
  else
52
54
  alternative_name = ab_user[experiment.key]
53
- trial = Trial.new(:user => ab_user, :experiment => experiment,
54
- :alternative => alternative_name)
55
- 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)
56
63
 
57
64
  if should_reset
58
65
  reset!(experiment)
@@ -62,14 +69,15 @@ module Split
62
69
  end
63
70
  end
64
71
 
65
- def ab_finished(metric_descriptor, options = {:reset => true})
72
+ def ab_finished(metric_descriptor, options = { reset: true })
66
73
  return if exclude_visitor? || Split.configuration.disabled?
67
74
  metric_descriptor, goals = normalize_metric(metric_descriptor)
68
75
  experiments = Metric.possible_experiments(metric_descriptor)
69
76
 
70
77
  if experiments.any?
71
78
  experiments.each do |experiment|
72
- finish_experiment(experiment, options.merge(:goals => goals))
79
+ next if override_present?(experiment.key)
80
+ finish_experiment(experiment, options.merge(goals: goals))
73
81
  end
74
82
  end
75
83
  rescue => e
@@ -78,8 +86,8 @@ module Split
78
86
  end
79
87
 
80
88
  def ab_record_extra_info(metric_descriptor, key, value = 1)
81
- return if exclude_visitor? || Split.configuration.disabled?
82
- metric_descriptor, goals = normalize_metric(metric_descriptor)
89
+ return if exclude_visitor? || Split.configuration.disabled? || value.nil?
90
+ metric_descriptor, _ = normalize_metric(metric_descriptor)
83
91
  experiments = Metric.possible_experiments(metric_descriptor)
84
92
 
85
93
  if experiments.any?
@@ -87,7 +95,7 @@ module Split
87
95
  alternative_name = ab_user[experiment.key]
88
96
 
89
97
  if alternative_name
90
- alternative = experiment.alternatives.find{|alt| alt.name == alternative_name}
98
+ alternative = experiment.alternatives.find { |alt| alt.name == alternative_name }
91
99
  alternative.record_extra_info(key, value) if alternative
92
100
  end
93
101
  end
@@ -97,24 +105,36 @@ module Split
97
105
  Split.configuration.db_failover_on_db_error.call(e)
98
106
  end
99
107
 
100
- def ab_active_experiments()
108
+ def ab_active_experiments
101
109
  ab_user.active_experiments
102
110
  rescue => e
103
111
  raise unless Split.configuration.db_failover
104
112
  Split.configuration.db_failover_on_db_error.call(e)
105
113
  end
106
114
 
107
-
108
115
  def override_present?(experiment_name)
109
- override_alternative(experiment_name)
116
+ override_alternative_by_params(experiment_name) || override_alternative_by_cookies(experiment_name)
110
117
  end
111
118
 
112
119
  def override_alternative(experiment_name)
113
- 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
114
134
  end
115
135
 
116
136
  def split_generically_disabled?
117
- defined?(params) && params['SPLIT_DISABLE']
137
+ params_present? && params["SPLIT_DISABLE"]
118
138
  end
119
139
 
120
140
  def ab_user
@@ -122,26 +142,34 @@ module Split
122
142
  end
123
143
 
124
144
  def exclude_visitor?
125
- 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?)
126
146
  end
127
147
 
128
148
  def is_robot?
129
- defined?(request) && request.user_agent =~ Split.configuration.robot_regex
149
+ request_present? && request.user_agent =~ Split.configuration.robot_regex
130
150
  end
131
151
 
132
152
  def is_preview?
133
- defined?(request) && defined?(request.headers) && request.headers['x-purpose'] == 'preview'
153
+ request_present? && defined?(request.headers) && request.headers["x-purpose"] == "preview"
134
154
  end
135
155
 
136
156
  def is_ignored_ip_address?
137
157
  return false if Split.configuration.ignore_ip_addresses.empty?
138
158
 
139
159
  Split.configuration.ignore_ip_addresses.each do |ip|
140
- 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))
141
161
  end
142
162
  false
143
163
  end
144
164
 
165
+ def params_present?
166
+ defined?(params) && params
167
+ end
168
+
169
+ def request_present?
170
+ defined?(request) && request
171
+ end
172
+
145
173
  def active_experiments
146
174
  ab_user.active_experiments
147
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,50 @@ 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
+ headers = @response.respond_to?(:header) ? @response.header : @response.headers
51
+ delete_cookie_header!(headers, key, value)
52
+ Rack::Utils.set_cookie_header!(headers, key, value)
53
+ end
49
54
 
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
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
65
+ end
54
66
 
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
67
+ cookies.reject! { |cookie| cookie =~ /\A#{Rack::Utils.escape(key)}=/ }
68
+ header["Set-Cookie"] = cookies.join("\n")
65
69
  end
66
70
 
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]
71
+ def hash
72
+ @hash ||= if cookies = @cookies[:split.to_s]
74
73
  begin
75
- JSON.parse(cookies)
74
+ parsed = JSON.parse(cookies)
75
+ parsed.is_a?(Hash) ? parsed : {}
76
76
  rescue JSON::ParserError
77
77
  {}
78
78
  end
@@ -80,15 +80,18 @@ module Split
80
80
  {}
81
81
  end
82
82
  end
83
- end
84
83
 
85
- def cookie_length_config
86
- Split.configuration.persistence_cookie_length
87
- end
84
+ def cookie_length_config
85
+ Split.configuration.persistence_cookie_length
86
+ end
88
87
 
89
- def action_dispatch?
90
- defined?(Rails) && @response.is_a?(ActionDispatch::Response)
91
- end
88
+ def cookie_domain_config
89
+ Split.configuration.persistence_cookie_domain
90
+ end
91
+
92
+ def action_dispatch?
93
+ defined?(Rails) && @response.is_a?(ActionDispatch::Response)
94
+ end
92
95
  end
93
96
  end
94
97
  end
@@ -1,12 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'forwardable'
4
-
5
3
  module Split
6
4
  module Persistence
7
5
  class DualAdapter
8
- extend Forwardable
9
- def_delegators :@adapter, :keys, :[], :[]=, :delete
6
+ def self.with_config(options = {})
7
+ self.config.merge!(options)
8
+ self
9
+ end
10
+
11
+ def self.config
12
+ @config ||= {}
13
+ end
10
14
 
11
15
  def initialize(context)
12
16
  if logged_in = self.class.config[:logged_in]
@@ -22,22 +26,59 @@ module Split
22
26
  raise "Please configure :logged_out_adapter"
23
27
  end
24
28
 
25
- if logged_in.call(context)
26
- @adapter = logged_in_adapter.new(context)
29
+ @fallback_to_logged_out_adapter =
30
+ self.class.config[:fallback_to_logged_out_adapter] || false
31
+ @logged_in = logged_in.call(context)
32
+ @logged_in_adapter = logged_in_adapter.new(context)
33
+ @logged_out_adapter = logged_out_adapter.new(context)
34
+ @active_adapter = @logged_in ? @logged_in_adapter : @logged_out_adapter
35
+ end
36
+
37
+ def keys
38
+ if @fallback_to_logged_out_adapter
39
+ (@logged_in_adapter.keys + @logged_out_adapter.keys).uniq
27
40
  else
28
- @adapter = logged_out_adapter.new(context)
41
+ @active_adapter.keys
29
42
  end
30
43
  end
31
44
 
32
- def self.with_config(options={})
33
- self.config.merge!(options)
34
- self
45
+ def [](key)
46
+ if @fallback_to_logged_out_adapter
47
+ @logged_in && @logged_in_adapter[key] || @logged_out_adapter[key]
48
+ else
49
+ @active_adapter[key]
50
+ end
35
51
  end
36
52
 
37
- def self.config
38
- @config ||= {}
53
+ def []=(key, value)
54
+ if @fallback_to_logged_out_adapter
55
+ @logged_in_adapter[key] = value if @logged_in
56
+ old_value = @logged_out_adapter[key]
57
+ @logged_out_adapter[key] = value
58
+
59
+ decrement_participation(key, old_value) if decrement_participation?(old_value, value)
60
+ else
61
+ @active_adapter[key] = value
62
+ end
39
63
  end
40
64
 
65
+ def delete(key)
66
+ if @fallback_to_logged_out_adapter
67
+ @logged_in_adapter.delete(key)
68
+ @logged_out_adapter.delete(key)
69
+ else
70
+ @active_adapter.delete(key)
71
+ end
72
+ end
73
+
74
+ private
75
+ def decrement_participation?(old_value, value)
76
+ !old_value.nil? && !value.nil? && old_value != value
77
+ end
78
+
79
+ def decrement_participation(key, value)
80
+ Split.redis.hincrby("#{key}:#{value}", "participant_count", -1)
81
+ end
41
82
  end
42
83
  end
43
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