split 4.0.1 → 4.0.2
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/workflows/ci.yml +6 -3
- data/.rubocop.yml +2 -5
- data/CHANGELOG.md +23 -0
- data/CONTRIBUTING.md +1 -1
- data/Gemfile +2 -1
- data/README.md +4 -2
- data/Rakefile +4 -5
- data/gemfiles/5.2.gemfile +1 -3
- data/gemfiles/6.0.gemfile +1 -3
- data/gemfiles/6.1.gemfile +1 -3
- data/gemfiles/7.0.gemfile +2 -3
- data/lib/split/algorithms/block_randomization.rb +5 -6
- data/lib/split/algorithms/whiplash.rb +16 -18
- data/lib/split/algorithms.rb +22 -0
- data/lib/split/alternative.rb +21 -22
- data/lib/split/cache.rb +0 -1
- data/lib/split/combined_experiments_helper.rb +4 -4
- data/lib/split/configuration.rb +83 -84
- data/lib/split/dashboard/helpers.rb +6 -7
- data/lib/split/dashboard/pagination_helpers.rb +53 -54
- data/lib/split/dashboard/public/style.css +5 -2
- data/lib/split/dashboard/views/index.erb +19 -4
- data/lib/split/dashboard.rb +29 -23
- data/lib/split/encapsulated_helper.rb +4 -6
- data/lib/split/experiment.rb +84 -88
- data/lib/split/experiment_catalog.rb +6 -5
- data/lib/split/extensions/string.rb +1 -1
- data/lib/split/goals_collection.rb +8 -10
- data/lib/split/helper.rb +19 -19
- data/lib/split/metric.rb +4 -5
- data/lib/split/persistence/cookie_adapter.rb +44 -47
- data/lib/split/persistence/dual_adapter.rb +7 -8
- data/lib/split/persistence/redis_adapter.rb +2 -3
- data/lib/split/persistence/session_adapter.rb +0 -2
- data/lib/split/persistence.rb +4 -4
- data/lib/split/redis_interface.rb +1 -2
- data/lib/split/trial.rb +23 -24
- data/lib/split/user.rb +12 -13
- data/lib/split/version.rb +1 -1
- data/lib/split/zscore.rb +1 -3
- data/lib/split.rb +26 -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 +15 -19
- data/spec/combined_experiments_helper_spec.rb +18 -17
- data/spec/configuration_spec.rb +32 -38
- 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 +67 -35
- data/spec/encapsulated_helper_spec.rb +12 -14
- data/spec/experiment_catalog_spec.rb +14 -13
- data/spec/experiment_spec.rb +121 -123
- data/spec/goals_collection_spec.rb +17 -15
- data/spec/helper_spec.rb +379 -382
- data/spec/metric_spec.rb +14 -14
- data/spec/persistence/cookie_adapter_spec.rb +23 -8
- data/spec/persistence/dual_adapter_spec.rb +71 -71
- data/spec/persistence/redis_adapter_spec.rb +25 -26
- data/spec/persistence/session_adapter_spec.rb +2 -3
- data/spec/persistence_spec.rb +1 -2
- data/spec/redis_interface_spec.rb +16 -14
- data/spec/spec_helper.rb +15 -13
- data/spec/split_spec.rb +11 -11
- data/spec/support/cookies_mock.rb +1 -2
- data/spec/trial_spec.rb +61 -60
- data/spec/user_spec.rb +36 -36
- data/split.gemspec +20 -20
- metadata +7 -10
- data/.rubocop_todo.yml +0 -226
- data/Appraisals +0 -19
- data/gemfiles/5.0.gemfile +0 -9
- data/gemfiles/5.1.gemfile +0 -9
@@ -1,15 +1,16 @@
|
|
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)
|
@@ -19,14 +20,14 @@ module Split
|
|
19
20
|
def self.find_or_initialize(metric_descriptor, control = nil, *alternatives)
|
20
21
|
# Check if array is passed to ab_test
|
21
22
|
# e.g. ab_test('name', ['Alt 1', 'Alt 2', 'Alt 3'])
|
22
|
-
if control.is_a?
|
23
|
+
if control.is_a?(Array) && alternatives.length.zero?
|
23
24
|
control, alternatives = control.first, control[1..-1]
|
24
25
|
end
|
25
26
|
|
26
27
|
experiment_name_with_version, goals = normalize_experiment(metric_descriptor)
|
27
|
-
experiment_name = experiment_name_with_version.to_s.split(
|
28
|
+
experiment_name = experiment_name_with_version.to_s.split(":")[0]
|
28
29
|
Split::Experiment.new(experiment_name,
|
29
|
-
:
|
30
|
+
alternatives: [control].compact + alternatives, goals: goals)
|
30
31
|
end
|
31
32
|
|
32
33
|
def self.find_or_create(metric_descriptor, control = nil, *alternatives)
|
@@ -4,7 +4,7 @@ class String
|
|
4
4
|
# Constatntize is often provided by ActiveSupport, but ActiveSupport is not a dependency of Split.
|
5
5
|
unless method_defined?(:constantize)
|
6
6
|
def constantize
|
7
|
-
names = self.split(
|
7
|
+
names = self.split("::")
|
8
8
|
names.shift if names.empty? || names.first.empty?
|
9
9
|
|
10
10
|
constant = Object
|
@@ -2,8 +2,7 @@
|
|
2
2
|
|
3
3
|
module Split
|
4
4
|
class GoalsCollection
|
5
|
-
|
6
|
-
def initialize(experiment_name, goals=nil)
|
5
|
+
def initialize(experiment_name, goals = nil)
|
7
6
|
@experiment_name = experiment_name
|
8
7
|
@goals = goals
|
9
8
|
end
|
@@ -15,10 +14,10 @@ module Split
|
|
15
14
|
def load_from_configuration
|
16
15
|
goals = Split.configuration.experiment_for(@experiment_name)[:goals]
|
17
16
|
|
18
|
-
if goals
|
19
|
-
goals = []
|
20
|
-
else
|
17
|
+
if goals
|
21
18
|
goals.flatten
|
19
|
+
else
|
20
|
+
[]
|
22
21
|
end
|
23
22
|
end
|
24
23
|
|
@@ -29,7 +28,7 @@ module Split
|
|
29
28
|
|
30
29
|
def validate!
|
31
30
|
unless @goals.nil? || @goals.kind_of?(Array)
|
32
|
-
raise ArgumentError,
|
31
|
+
raise ArgumentError, "Goals must be an array"
|
33
32
|
end
|
34
33
|
end
|
35
34
|
|
@@ -38,9 +37,8 @@ module Split
|
|
38
37
|
end
|
39
38
|
|
40
39
|
private
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
end
|
40
|
+
def goals_key
|
41
|
+
"#{@experiment_name}:goals"
|
42
|
+
end
|
45
43
|
end
|
46
44
|
end
|
data/lib/split/helper.rb
CHANGED
@@ -12,9 +12,9 @@ module Split
|
|
12
12
|
alternative = if Split.configuration.enabled && !exclude_visitor?
|
13
13
|
experiment.save
|
14
14
|
raise(Split::InvalidExperimentsFormatError) unless (Split.configuration.experiments || {}).fetch(experiment.name.to_sym, {})[:combined_experiments].nil?
|
15
|
-
trial = Trial.new(:
|
16
|
-
:
|
17
|
-
:
|
15
|
+
trial = Trial.new(user: ab_user, experiment: experiment,
|
16
|
+
override: override_alternative(experiment.name), exclude: exclude_visitor?,
|
17
|
+
disabled: split_generically_disabled?)
|
18
18
|
alt = trial.choose!(self)
|
19
19
|
alt ? alt.name : nil
|
20
20
|
else
|
@@ -44,21 +44,21 @@ module Split
|
|
44
44
|
ab_user.delete(experiment.key)
|
45
45
|
end
|
46
46
|
|
47
|
-
def finish_experiment(experiment, options = {:
|
47
|
+
def finish_experiment(experiment, options = { reset: true })
|
48
48
|
return false if active_experiments[experiment.name].nil?
|
49
49
|
return true if experiment.has_winner?
|
50
50
|
should_reset = experiment.resettable? && options[:reset]
|
51
51
|
if ab_user[experiment.finished_key] && !should_reset
|
52
|
-
|
52
|
+
true
|
53
53
|
else
|
54
54
|
alternative_name = ab_user[experiment.key]
|
55
55
|
trial = Trial.new(
|
56
|
-
:
|
57
|
-
:
|
58
|
-
:
|
59
|
-
:
|
60
|
-
)
|
61
|
-
|
56
|
+
user: ab_user,
|
57
|
+
experiment: experiment,
|
58
|
+
alternative: alternative_name,
|
59
|
+
goals: options[:goals],
|
60
|
+
)
|
61
|
+
|
62
62
|
trial.complete!(self)
|
63
63
|
|
64
64
|
if should_reset
|
@@ -69,7 +69,7 @@ module Split
|
|
69
69
|
end
|
70
70
|
end
|
71
71
|
|
72
|
-
def ab_finished(metric_descriptor, options = {:
|
72
|
+
def ab_finished(metric_descriptor, options = { reset: true })
|
73
73
|
return if exclude_visitor? || Split.configuration.disabled?
|
74
74
|
metric_descriptor, goals = normalize_metric(metric_descriptor)
|
75
75
|
experiments = Metric.possible_experiments(metric_descriptor)
|
@@ -77,7 +77,7 @@ module Split
|
|
77
77
|
if experiments.any?
|
78
78
|
experiments.each do |experiment|
|
79
79
|
next if override_present?(experiment.key)
|
80
|
-
finish_experiment(experiment, options.merge(:
|
80
|
+
finish_experiment(experiment, options.merge(goals: goals))
|
81
81
|
end
|
82
82
|
end
|
83
83
|
rescue => e
|
@@ -95,7 +95,7 @@ module Split
|
|
95
95
|
alternative_name = ab_user[experiment.key]
|
96
96
|
|
97
97
|
if alternative_name
|
98
|
-
alternative = experiment.alternatives.find{|alt| alt.name == alternative_name}
|
98
|
+
alternative = experiment.alternatives.find { |alt| alt.name == alternative_name }
|
99
99
|
alternative.record_extra_info(key, value) if alternative
|
100
100
|
end
|
101
101
|
end
|
@@ -105,7 +105,7 @@ module Split
|
|
105
105
|
Split.configuration.db_failover_on_db_error.call(e)
|
106
106
|
end
|
107
107
|
|
108
|
-
def ab_active_experiments
|
108
|
+
def ab_active_experiments
|
109
109
|
ab_user.active_experiments
|
110
110
|
rescue => e
|
111
111
|
raise unless Split.configuration.db_failover
|
@@ -127,14 +127,14 @@ module Split
|
|
127
127
|
def override_alternative_by_cookies(experiment_name)
|
128
128
|
return unless defined?(request)
|
129
129
|
|
130
|
-
if request.cookies && request.cookies.key?(
|
131
|
-
experiments = JSON.parse(request.cookies[
|
130
|
+
if request.cookies && request.cookies.key?("split_override")
|
131
|
+
experiments = JSON.parse(request.cookies["split_override"]) rescue {}
|
132
132
|
experiments[experiment_name]
|
133
133
|
end
|
134
134
|
end
|
135
135
|
|
136
136
|
def split_generically_disabled?
|
137
|
-
defined?(params) && params[
|
137
|
+
defined?(params) && params["SPLIT_DISABLE"]
|
138
138
|
end
|
139
139
|
|
140
140
|
def ab_user
|
@@ -150,7 +150,7 @@ module Split
|
|
150
150
|
end
|
151
151
|
|
152
152
|
def is_preview?
|
153
|
-
defined?(request) && defined?(request.headers) && request.headers[
|
153
|
+
defined?(request) && defined?(request.headers) && request.headers["x-purpose"] == "preview"
|
154
154
|
end
|
155
155
|
|
156
156
|
def is_ignored_ip_address?
|
data/lib/split/metric.rb
CHANGED
@@ -16,13 +16,13 @@ module Split
|
|
16
16
|
def self.load_from_redis(name)
|
17
17
|
metric = Split.redis.hget(:metrics, name)
|
18
18
|
if metric
|
19
|
-
experiment_names = metric.split(
|
19
|
+
experiment_names = metric.split(",")
|
20
20
|
|
21
21
|
experiments = experiment_names.collect do |experiment_name|
|
22
22
|
Split::ExperimentCatalog.find(experiment_name)
|
23
23
|
end
|
24
24
|
|
25
|
-
Split::Metric.new(:
|
25
|
+
Split::Metric.new(name: name, experiments: experiments)
|
26
26
|
else
|
27
27
|
nil
|
28
28
|
end
|
@@ -31,7 +31,7 @@ module Split
|
|
31
31
|
def self.load_from_configuration(name)
|
32
32
|
metrics = Split.configuration.metrics
|
33
33
|
if metrics && metrics[name]
|
34
|
-
Split::Metric.new(:
|
34
|
+
Split::Metric.new(experiments: metrics[name], name: name)
|
35
35
|
else
|
36
36
|
nil
|
37
37
|
end
|
@@ -77,7 +77,7 @@ module Split
|
|
77
77
|
end
|
78
78
|
|
79
79
|
def save
|
80
|
-
Split.redis.hset(:metrics, name, experiments.map(&:name).join(
|
80
|
+
Split.redis.hset(:metrics, name, experiments.map(&:name).join(","))
|
81
81
|
end
|
82
82
|
|
83
83
|
def complete!
|
@@ -97,6 +97,5 @@ module Split
|
|
97
97
|
return metric_name, goals
|
98
98
|
end
|
99
99
|
private_class_method :normalize_metric
|
100
|
-
|
101
100
|
end
|
102
101
|
end
|
@@ -5,7 +5,6 @@ require "json"
|
|
5
5
|
module Split
|
6
6
|
module Persistence
|
7
7
|
class CookieAdapter
|
8
|
-
|
9
8
|
def initialize(context)
|
10
9
|
@context = context
|
11
10
|
@request, @response = context.request, context.response
|
@@ -30,50 +29,49 @@ module Split
|
|
30
29
|
end
|
31
30
|
|
32
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
|
33
44
|
|
34
|
-
|
35
|
-
|
36
|
-
cookie_value = default_options.merge(value: JSON.generate(value))
|
37
|
-
if action_dispatch?
|
38
|
-
# The "send" is necessary when we call ab_test from the controller
|
39
|
-
# and thus @context is a rails controller, because then "cookies" is
|
40
|
-
# a private method.
|
41
|
-
@context.send(:cookies)[cookie_key] = cookie_value
|
42
|
-
else
|
43
|
-
set_cookie_via_rack(cookie_key, cookie_value)
|
45
|
+
def default_options
|
46
|
+
{ expires: @expires, path: "/", domain: cookie_domain_config }.compact
|
44
47
|
end
|
45
|
-
end
|
46
48
|
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
50
53
|
|
51
|
-
|
52
|
-
delete_cookie_header!(
|
53
|
-
|
54
|
-
|
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
|
55
65
|
|
56
|
-
|
57
|
-
|
58
|
-
cookie_header = header['Set-Cookie']
|
59
|
-
case cookie_header
|
60
|
-
when nil, ''
|
61
|
-
cookies = []
|
62
|
-
when String
|
63
|
-
cookies = cookie_header.split("\n")
|
64
|
-
when Array
|
65
|
-
cookies = cookie_header
|
66
|
+
cookies.reject! { |cookie| cookie =~ /\A#{Rack::Utils.escape(key)}=/ }
|
67
|
+
header["Set-Cookie"] = cookies.join("\n")
|
66
68
|
end
|
67
69
|
|
68
|
-
|
69
|
-
|
70
|
-
end
|
71
|
-
|
72
|
-
def hash
|
73
|
-
@hash ||= begin
|
74
|
-
if cookies = @cookies[:split.to_s]
|
70
|
+
def hash
|
71
|
+
@hash ||= if cookies = @cookies[:split.to_s]
|
75
72
|
begin
|
76
|
-
JSON.parse(cookies)
|
73
|
+
parsed = JSON.parse(cookies)
|
74
|
+
parsed.is_a?(Hash) ? parsed : {}
|
77
75
|
rescue JSON::ParserError
|
78
76
|
{}
|
79
77
|
end
|
@@ -81,19 +79,18 @@ module Split
|
|
81
79
|
{}
|
82
80
|
end
|
83
81
|
end
|
84
|
-
end
|
85
82
|
|
86
|
-
|
87
|
-
|
88
|
-
|
83
|
+
def cookie_length_config
|
84
|
+
Split.configuration.persistence_cookie_length
|
85
|
+
end
|
89
86
|
|
90
|
-
|
91
|
-
|
92
|
-
|
87
|
+
def cookie_domain_config
|
88
|
+
Split.configuration.persistence_cookie_domain
|
89
|
+
end
|
93
90
|
|
94
|
-
|
95
|
-
|
96
|
-
|
91
|
+
def action_dispatch?
|
92
|
+
defined?(Rails) && @response.is_a?(ActionDispatch::Response)
|
93
|
+
end
|
97
94
|
end
|
98
95
|
end
|
99
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
|
@@ -3,7 +3,7 @@
|
|
3
3
|
module Split
|
4
4
|
module Persistence
|
5
5
|
class RedisAdapter
|
6
|
-
DEFAULT_CONFIG = {:
|
6
|
+
DEFAULT_CONFIG = { namespace: "persistence" }.freeze
|
7
7
|
|
8
8
|
attr_reader :redis_key
|
9
9
|
|
@@ -44,7 +44,7 @@ module Split
|
|
44
44
|
new(nil, user_id)
|
45
45
|
end
|
46
46
|
|
47
|
-
def self.with_config(options={})
|
47
|
+
def self.with_config(options = {})
|
48
48
|
self.config.merge!(options)
|
49
49
|
self
|
50
50
|
end
|
@@ -56,7 +56,6 @@ module Split
|
|
56
56
|
def self.reset_config!
|
57
57
|
@config = DEFAULT_CONFIG.dup
|
58
58
|
end
|
59
|
-
|
60
59
|
end
|
61
60
|
end
|
62
61
|
end
|
data/lib/split/persistence.rb
CHANGED
@@ -2,10 +2,10 @@
|
|
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
11
|
cookie: Split::Persistence::CookieAdapter,
|
data/lib/split/trial.rb
CHANGED
@@ -24,15 +24,15 @@ module Split
|
|
24
24
|
|
25
25
|
def alternative
|
26
26
|
@alternative ||= if @experiment.has_winner?
|
27
|
-
|
28
|
-
|
27
|
+
@experiment.winner
|
28
|
+
end
|
29
29
|
end
|
30
30
|
|
31
31
|
def alternative=(alternative)
|
32
32
|
@alternative = if alternative.kind_of?(Split::Alternative)
|
33
33
|
alternative
|
34
34
|
else
|
35
|
-
@experiment.alternatives.find{|a| a.name == alternative }
|
35
|
+
@experiment.alternatives.find { |a| a.name == alternative }
|
36
36
|
end
|
37
37
|
end
|
38
38
|
|
@@ -41,7 +41,7 @@ module Split
|
|
41
41
|
if Array(goals).empty?
|
42
42
|
alternative.increment_completion
|
43
43
|
else
|
44
|
-
Array(goals).each {|g| alternative.increment_completion(g) }
|
44
|
+
Array(goals).each { |g| alternative.increment_completion(g) }
|
45
45
|
end
|
46
46
|
|
47
47
|
run_callback context, Split.configuration.on_trial_complete
|
@@ -97,31 +97,30 @@ module Split
|
|
97
97
|
end
|
98
98
|
|
99
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
|
100
103
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
def override_is_alternative?
|
106
|
-
@experiment.alternatives.map(&:name).include?(@options[:override])
|
107
|
-
end
|
104
|
+
def override_is_alternative?
|
105
|
+
@experiment.alternatives.map(&:name).include?(@options[:override])
|
106
|
+
end
|
108
107
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
108
|
+
def should_store_alternative?
|
109
|
+
if @options[:override] || @options[:disabled]
|
110
|
+
Split.configuration.store_override
|
111
|
+
else
|
112
|
+
!exclude_user?
|
113
|
+
end
|
114
114
|
end
|
115
|
-
end
|
116
115
|
|
117
|
-
|
118
|
-
|
119
|
-
|
116
|
+
def cleanup_old_versions
|
117
|
+
if @experiment.version > 0
|
118
|
+
@user.cleanup_old_versions!(@experiment)
|
119
|
+
end
|
120
120
|
end
|
121
|
-
end
|
122
121
|
|
123
|
-
|
124
|
-
|
125
|
-
|
122
|
+
def exclude_user?
|
123
|
+
@options[:exclude] || @experiment.start_time.nil? || @user.max_experiments_reached?(@experiment.key)
|
124
|
+
end
|
126
125
|
end
|
127
126
|
end
|
data/lib/split/user.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "forwardable"
|
4
4
|
|
5
5
|
module Split
|
6
6
|
class User
|
@@ -26,10 +26,10 @@ module Split
|
|
26
26
|
end
|
27
27
|
|
28
28
|
def max_experiments_reached?(experiment_key)
|
29
|
-
if Split.configuration.allow_multiple_experiments ==
|
29
|
+
if Split.configuration.allow_multiple_experiments == "control"
|
30
30
|
experiments = active_experiments
|
31
31
|
experiment_key_without_version = key_without_version(experiment_key)
|
32
|
-
count_control = experiments.count {|k, v| k == experiment_key_without_version || v ==
|
32
|
+
count_control = experiments.count { |k, v| k == experiment_key_without_version || v == "control" }
|
33
33
|
experiments.size > count_control
|
34
34
|
else
|
35
35
|
!Split.configuration.allow_multiple_experiments &&
|
@@ -65,17 +65,16 @@ module Split
|
|
65
65
|
end
|
66
66
|
|
67
67
|
private
|
68
|
+
def keys_without_experiment(keys, experiment_key)
|
69
|
+
keys.reject { |k| k.match(Regexp.new("^#{experiment_key}(:finished)?$")) }
|
70
|
+
end
|
68
71
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
def keys_without_finished(keys)
|
74
|
-
keys.reject { |k| k.include?(":finished") }
|
75
|
-
end
|
72
|
+
def keys_without_finished(keys)
|
73
|
+
keys.reject { |k| k.include?(":finished") }
|
74
|
+
end
|
76
75
|
|
77
|
-
|
78
|
-
|
79
|
-
|
76
|
+
def key_without_version(key)
|
77
|
+
key.split(/\:\d(?!\:)/)[0]
|
78
|
+
end
|
80
79
|
end
|
81
80
|
end
|
data/lib/split/version.rb
CHANGED
data/lib/split/zscore.rb
CHANGED