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.
- checksums.yaml +4 -4
- data/.github/FUNDING.yml +1 -0
- data/.github/dependabot.yml +7 -0
- data/.github/workflows/ci.yml +76 -0
- data/.rubocop.yml +177 -4
- data/CHANGELOG.md +87 -0
- data/CONTRIBUTING.md +1 -1
- data/Gemfile +2 -1
- data/README.md +37 -9
- data/Rakefile +5 -5
- data/gemfiles/5.2.gemfile +1 -3
- data/gemfiles/6.0.gemfile +1 -3
- data/gemfiles/{5.0.gemfile → 6.1.gemfile} +2 -4
- data/gemfiles/{5.1.gemfile → 7.0.gemfile} +2 -4
- data/lib/split/algorithms/block_randomization.rb +6 -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 +22 -22
- data/lib/split/cache.rb +27 -0
- data/lib/split/combined_experiments_helper.rb +5 -4
- data/lib/split/configuration.rb +89 -94
- data/lib/split/dashboard/helpers.rb +7 -7
- data/lib/split/dashboard/pagination_helpers.rb +54 -54
- 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.rb +42 -21
- data/lib/split/encapsulated_helper.rb +15 -8
- data/lib/split/engine.rb +1 -0
- data/lib/split/exceptions.rb +1 -0
- data/lib/split/experiment.rb +151 -124
- data/lib/split/experiment_catalog.rb +7 -8
- data/lib/split/extensions/string.rb +2 -1
- data/lib/split/goals_collection.rb +9 -10
- data/lib/split/helper.rb +50 -23
- data/lib/split/metric.rb +6 -6
- data/lib/split/persistence/cookie_adapter.rb +46 -44
- data/lib/split/persistence/dual_adapter.rb +7 -8
- 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 +15 -29
- data/lib/split/trial.rb +43 -34
- data/lib/split/user.rb +25 -14
- data/lib/split/version.rb +2 -4
- data/lib/split/zscore.rb +2 -3
- data/lib/split.rb +34 -27
- 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 +69 -67
- data/spec/dashboard/paginator_spec.rb +10 -9
- data/spec/dashboard_helpers_spec.rb +19 -18
- data/spec/dashboard_spec.rb +122 -38
- data/spec/encapsulated_helper_spec.rb +46 -22
- data/spec/experiment_catalog_spec.rb +14 -13
- data/spec/experiment_spec.rb +198 -118
- data/spec/goals_collection_spec.rb +18 -16
- data/spec/helper_spec.rb +454 -385
- data/spec/metric_spec.rb +14 -14
- data/spec/persistence/cookie_adapter_spec.rb +26 -11
- data/spec/persistence/dual_adapter_spec.rb +71 -71
- 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 +35 -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 +60 -29
- data/split.gemspec +22 -21
- metadata +43 -40
- data/.rubocop_todo.yml +0 -679
- data/.travis.yml +0 -60
- data/Appraisals +0 -19
- 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(:
|
|
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,17 +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 })
|
|
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
|
-
|
|
52
|
+
true
|
|
52
53
|
else
|
|
53
54
|
alternative_name = ab_user[experiment.key]
|
|
54
|
-
trial = Trial.new(
|
|
55
|
-
|
|
56
|
-
|
|
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 = {:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
149
|
+
request_present? && request.user_agent =~ Split.configuration.robot_regex
|
|
131
150
|
end
|
|
132
151
|
|
|
133
152
|
def is_preview?
|
|
134
|
-
|
|
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
|
|
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(:
|
|
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,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
|
-
|
|
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
|
+
delete_cookie_header!(@response.header, key, value)
|
|
51
|
+
Rack::Utils.set_cookie_header!(@response.header, key, value)
|
|
52
|
+
end
|
|
49
53
|
|
|
50
|
-
|
|
51
|
-
delete_cookie_header!(
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
|
66
|
+
cookies.reject! { |cookie| cookie =~ /\A#{Rack::Utils.escape(key)}=/ }
|
|
67
|
+
header["Set-Cookie"] = cookies.join("\n")
|
|
65
68
|
end
|
|
66
69
|
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
83
|
+
def cookie_length_config
|
|
84
|
+
Split.configuration.persistence_cookie_length
|
|
85
|
+
end
|
|
88
86
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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 = {:
|
|
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,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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
@
|
|
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
|
-
|
|
25
|
-
|
|
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!(
|
|
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 @
|
|
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
|
-
|
|
77
|
+
if @experiment.cohorting_disabled?
|
|
78
|
+
self.alternative = @experiment.control
|
|
79
|
+
else
|
|
80
|
+
self.alternative = @experiment.next_alternative
|
|
74
81
|
|
|
75
|
-
|
|
76
|
-
|
|
82
|
+
# Increment the number of participants since we are actually choosing a new alternative
|
|
83
|
+
self.alternative.increment_participation
|
|
77
84
|
|
|
78
|
-
|
|
85
|
+
run_callback context, Split.configuration.on_trial_choose
|
|
86
|
+
end
|
|
79
87
|
end
|
|
80
88
|
end
|
|
81
89
|
end
|
|
82
90
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|