split 3.2.0 → 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.
- checksums.yaml +5 -5
- data/.eslintrc +1 -1
- data/.github/FUNDING.yml +1 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +24 -0
- data/.github/dependabot.yml +7 -0
- data/.github/workflows/ci.yml +63 -0
- data/.rspec +1 -0
- data/.rubocop.yml +67 -1043
- data/CHANGELOG.md +174 -0
- data/CODE_OF_CONDUCT.md +3 -3
- data/CONTRIBUTING.md +1 -1
- data/Gemfile +6 -1
- data/README.md +79 -33
- data/Rakefile +6 -5
- data/lib/split/algorithms/block_randomization.rb +7 -6
- data/lib/split/algorithms/weighted_sample.rb +2 -1
- data/lib/split/algorithms/whiplash.rb +17 -18
- data/lib/split/algorithms.rb +14 -0
- data/lib/split/alternative.rb +25 -25
- data/lib/split/cache.rb +27 -0
- data/lib/split/combined_experiments_helper.rb +6 -5
- data/lib/split/configuration.rb +94 -91
- data/lib/split/dashboard/helpers.rb +9 -9
- data/lib/split/dashboard/pagination_helpers.rb +86 -0
- data/lib/split/dashboard/paginator.rb +17 -0
- data/lib/split/dashboard/public/dashboard.js +10 -0
- data/lib/split/dashboard/public/style.css +19 -2
- data/lib/split/dashboard/views/_controls.erb +13 -0
- data/lib/split/dashboard/views/_experiment.erb +2 -1
- data/lib/split/dashboard/views/index.erb +24 -5
- data/lib/split/dashboard/views/layout.erb +1 -1
- data/lib/split/dashboard.rb +47 -20
- data/lib/split/encapsulated_helper.rb +15 -8
- data/lib/split/engine.rb +7 -4
- data/lib/split/exceptions.rb +1 -0
- data/lib/split/experiment.rb +160 -122
- data/lib/split/experiment_catalog.rb +7 -8
- data/lib/split/extensions/string.rb +2 -1
- data/lib/split/goals_collection.rb +10 -10
- data/lib/split/helper.rb +56 -24
- data/lib/split/metric.rb +6 -6
- data/lib/split/persistence/cookie_adapter.rb +52 -15
- data/lib/split/persistence/dual_adapter.rb +53 -12
- data/lib/split/persistence/redis_adapter.rb +8 -4
- data/lib/split/persistence/session_adapter.rb +1 -2
- data/lib/split/persistence.rb +8 -6
- data/lib/split/redis_interface.rb +16 -31
- data/lib/split/trial.rb +48 -41
- data/lib/split/user.rb +30 -15
- data/lib/split/version.rb +2 -4
- data/lib/split/zscore.rb +2 -3
- data/lib/split.rb +39 -25
- data/spec/algorithms/block_randomization_spec.rb +6 -5
- data/spec/algorithms/weighted_sample_spec.rb +6 -5
- data/spec/algorithms/whiplash_spec.rb +4 -5
- data/spec/alternative_spec.rb +35 -36
- data/spec/cache_spec.rb +84 -0
- data/spec/combined_experiments_helper_spec.rb +18 -17
- data/spec/configuration_spec.rb +41 -45
- data/spec/dashboard/pagination_helpers_spec.rb +202 -0
- data/spec/dashboard/paginator_spec.rb +38 -0
- data/spec/dashboard_helpers_spec.rb +19 -18
- data/spec/dashboard_spec.rb +153 -48
- data/spec/encapsulated_helper_spec.rb +47 -23
- data/spec/experiment_catalog_spec.rb +14 -13
- data/spec/experiment_spec.rb +224 -111
- data/spec/goals_collection_spec.rb +18 -16
- data/spec/helper_spec.rb +539 -419
- data/spec/metric_spec.rb +14 -14
- data/spec/persistence/cookie_adapter_spec.rb +105 -27
- data/spec/persistence/dual_adapter_spec.rb +158 -66
- data/spec/persistence/redis_adapter_spec.rb +35 -27
- data/spec/persistence/session_adapter_spec.rb +2 -3
- data/spec/persistence_spec.rb +1 -2
- data/spec/redis_interface_spec.rb +25 -82
- data/spec/spec_helper.rb +38 -24
- data/spec/split_spec.rb +18 -18
- data/spec/support/cookies_mock.rb +1 -2
- data/spec/trial_spec.rb +117 -70
- data/spec/user_spec.rb +69 -27
- data/split.gemspec +26 -22
- metadata +85 -37
- data/.travis.yml +0 -41
- data/Appraisals +0 -13
- data/gemfiles/4.2.gemfile +0 -9
- data/gemfiles/5.0.gemfile +0 -10
- data/gemfiles/5.1.gemfile +0 -10
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"
|
|
@@ -8,12 +9,12 @@ module Split
|
|
|
8
9
|
def ab_test(metric_descriptor, control = nil, *alternatives)
|
|
9
10
|
begin
|
|
10
11
|
experiment = ExperimentCatalog.find_or_initialize(metric_descriptor, control, *alternatives)
|
|
11
|
-
alternative = if Split.configuration.enabled
|
|
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(:
|
|
15
|
-
:
|
|
16
|
-
:
|
|
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 =
|
|
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 = {:
|
|
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
|
-
|
|
52
|
+
true
|
|
51
53
|
else
|
|
52
54
|
alternative_name = ab_user[experiment.key]
|
|
53
|
-
trial = Trial.new(
|
|
54
|
-
|
|
55
|
-
|
|
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 = {:
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
137
|
+
params_present? && params["SPLIT_DISABLE"]
|
|
118
138
|
end
|
|
119
139
|
|
|
120
140
|
def ab_user
|
|
@@ -122,22 +142,34 @@ module Split
|
|
|
122
142
|
end
|
|
123
143
|
|
|
124
144
|
def exclude_visitor?
|
|
125
|
-
|
|
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
|
-
|
|
149
|
+
request_present? && request.user_agent =~ Split.configuration.robot_regex
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def is_preview?
|
|
153
|
+
request_present? && defined?(request.headers) && request.headers["x-purpose"] == "preview"
|
|
130
154
|
end
|
|
131
155
|
|
|
132
156
|
def is_ignored_ip_address?
|
|
133
157
|
return false if Split.configuration.ignore_ip_addresses.empty?
|
|
134
158
|
|
|
135
159
|
Split.configuration.ignore_ip_addresses.each do |ip|
|
|
136
|
-
return true if
|
|
160
|
+
return true if request_present? && (request.ip == ip || (ip.class == Regexp && request.ip =~ ip))
|
|
137
161
|
end
|
|
138
162
|
false
|
|
139
163
|
end
|
|
140
164
|
|
|
165
|
+
def params_present?
|
|
166
|
+
defined?(params) && params
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def request_present?
|
|
170
|
+
defined?(request) && request
|
|
171
|
+
end
|
|
172
|
+
|
|
141
173
|
def active_experiments
|
|
142
174
|
ab_user.active_experiments
|
|
143
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(:
|
|
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(:
|
|
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,11 +1,12 @@
|
|
|
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
|
+
@context = context
|
|
9
10
|
@request, @response = context.request, context.response
|
|
10
11
|
@cookies = @request.cookies
|
|
11
12
|
@expires = Time.now + cookie_length_config
|
|
@@ -28,20 +29,50 @@ module Split
|
|
|
28
29
|
end
|
|
29
30
|
|
|
30
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
|
|
31
44
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
45
|
+
def default_options
|
|
46
|
+
{ expires: @expires, path: "/", domain: cookie_domain_config }.compact
|
|
47
|
+
end
|
|
35
48
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
39
54
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
66
|
+
|
|
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 ||= if cookies = @cookies[:split.to_s]
|
|
43
73
|
begin
|
|
44
|
-
JSON.parse(cookies)
|
|
74
|
+
parsed = JSON.parse(cookies)
|
|
75
|
+
parsed.is_a?(Hash) ? parsed : {}
|
|
45
76
|
rescue JSON::ParserError
|
|
46
77
|
{}
|
|
47
78
|
end
|
|
@@ -49,12 +80,18 @@ module Split
|
|
|
49
80
|
{}
|
|
50
81
|
end
|
|
51
82
|
end
|
|
52
|
-
end
|
|
53
83
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
84
|
+
def cookie_length_config
|
|
85
|
+
Split.configuration.persistence_cookie_length
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def cookie_domain_config
|
|
89
|
+
Split.configuration.persistence_cookie_domain
|
|
90
|
+
end
|
|
57
91
|
|
|
92
|
+
def action_dispatch?
|
|
93
|
+
defined?(Rails) && @response.is_a?(ActionDispatch::Response)
|
|
94
|
+
end
|
|
58
95
|
end
|
|
59
96
|
end
|
|
60
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
|
-
|
|
9
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
@
|
|
41
|
+
@active_adapter.keys
|
|
29
42
|
end
|
|
30
43
|
end
|
|
31
44
|
|
|
32
|
-
def
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
38
|
-
@
|
|
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 = {:
|
|
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.
|
|
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
|
data/lib/split/persistence.rb
CHANGED
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
module Split
|
|
4
4
|
module Persistence
|
|
5
|
-
require
|
|
6
|
-
require
|
|
7
|
-
require
|
|
8
|
-
require
|
|
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
|
-
:
|
|
12
|
-
:
|
|
11
|
+
cookie: Split::Persistence::CookieAdapter,
|
|
12
|
+
session: Split::Persistence::SessionAdapter,
|
|
13
|
+
redis: Split::Persistence::RedisAdapter,
|
|
14
|
+
dual_adapter: Split::Persistence::DualAdapter
|
|
13
15
|
}.freeze
|
|
14
16
|
|
|
15
17
|
def self.adapter
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Split
|
|
2
4
|
# Simplifies the interface to Redis.
|
|
3
5
|
class RedisInterface
|
|
@@ -6,46 +8,29 @@ module Split
|
|
|
6
8
|
end
|
|
7
9
|
|
|
8
10
|
def persist_list(list_name, list_values)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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)
|
|
15
17
|
end
|
|
16
18
|
end
|
|
17
|
-
make_list_length(list_name, list_values.length)
|
|
18
|
-
list_values
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def add_to_list(list_name, value)
|
|
22
|
-
redis.rpush(list_name, value)
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def set_list_index(list_name, index, value)
|
|
26
|
-
redis.lset(list_name, index, value)
|
|
27
|
-
end
|
|
28
19
|
|
|
29
|
-
|
|
30
|
-
redis.llen(list_name)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def remove_last_item_from_list(list_name)
|
|
34
|
-
redis.rpop(list_name)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def make_list_length(list_name, new_length)
|
|
38
|
-
while list_length(list_name) > new_length
|
|
39
|
-
remove_last_item_from_list(list_name)
|
|
40
|
-
end
|
|
20
|
+
list_values
|
|
41
21
|
end
|
|
42
22
|
|
|
43
23
|
def add_to_set(set_name, value)
|
|
44
|
-
redis.sadd(set_name, value)
|
|
24
|
+
return redis.sadd?(set_name, value) if redis.respond_to?(:sadd?)
|
|
25
|
+
|
|
26
|
+
redis.sadd(set_name, value)
|
|
45
27
|
end
|
|
46
28
|
|
|
47
29
|
private
|
|
30
|
+
attr_accessor :redis
|
|
48
31
|
|
|
49
|
-
|
|
32
|
+
def redis_namespace_used?
|
|
33
|
+
defined?(Redis::Namespace) && Split.redis.is_a?(Redis::Namespace)
|
|
34
|
+
end
|
|
50
35
|
end
|
|
51
36
|
end
|