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.
- checksums.yaml +4 -4
- 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 +121 -0
- data/CODE_OF_CONDUCT.md +3 -3
- data/CONTRIBUTING.md +1 -1
- data/Gemfile +6 -1
- data/README.md +51 -21
- 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 +5 -4
- data/lib/split/configuration.rb +94 -96
- data/lib/split/dashboard/helpers.rb +7 -7
- data/lib/split/dashboard/pagination_helpers.rb +56 -57
- data/lib/split/dashboard/paginator.rb +1 -0
- data/lib/split/dashboard/public/dashboard.js +10 -0
- data/lib/split/dashboard/public/style.css +10 -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 +19 -4
- data/lib/split/dashboard/views/layout.erb +1 -1
- data/lib/split/dashboard.rb +46 -21
- 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 +52 -24
- data/lib/split/metric.rb +6 -6
- data/lib/split/persistence/cookie_adapter.rb +47 -44
- 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 -29
- data/lib/split/trial.rb +44 -35
- 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 +35 -28
- 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 +71 -67
- data/spec/dashboard/paginator_spec.rb +10 -9
- 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 +531 -424
- data/spec/metric_spec.rb +14 -14
- data/spec/persistence/cookie_adapter_spec.rb +26 -11
- 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 +11 -11
- data/spec/support/cookies_mock.rb +1 -2
- data/spec/trial_spec.rb +102 -75
- data/spec/user_spec.rb +69 -27
- data/split.gemspec +26 -23
- metadata +68 -42
- data/.travis.yml +0 -66
- data/Appraisals +0 -19
- data/gemfiles/4.2.gemfile +0 -9
- data/gemfiles/5.0.gemfile +0 -9
- data/gemfiles/5.1.gemfile +0 -9
- data/gemfiles/5.2.gemfile +0 -9
- 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
|
-
|
|
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?
|
|
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(
|
|
28
|
+
experiment_name = experiment_name_with_version.to_s.split(":")[0]
|
|
29
29
|
Split::Experiment.new(experiment_name,
|
|
30
|
-
:
|
|
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
|
|
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,
|
|
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
|
-
|
|
41
|
-
|
|
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(:
|
|
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,26 +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
|
|
130
150
|
end
|
|
131
151
|
|
|
132
152
|
def is_preview?
|
|
133
|
-
|
|
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
|
|
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(:
|
|
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,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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
51
|
-
delete_cookie_header!(
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
def cookie_length_config
|
|
85
|
+
Split.configuration.persistence_cookie_length
|
|
86
|
+
end
|
|
88
87
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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
|