gitlab-experiment 0.7.0 → 0.8.0
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/lib/generators/gitlab/experiment/install/templates/initializer.rb.tt +4 -6
- data/lib/gitlab/experiment/base_interface.rb +17 -63
- data/lib/gitlab/experiment/cache.rb +3 -5
- data/lib/gitlab/experiment/callbacks.rb +3 -3
- data/lib/gitlab/experiment/configuration.rb +10 -62
- data/lib/gitlab/experiment/engine.rb +1 -0
- data/lib/gitlab/experiment/nestable.rb +16 -6
- data/lib/gitlab/experiment/rollout/percent.rb +4 -2
- data/lib/gitlab/experiment/rollout/random.rb +1 -1
- data/lib/gitlab/experiment/rollout/round_robin.rb +1 -1
- data/lib/gitlab/experiment/rollout.rb +25 -34
- data/lib/gitlab/experiment/rspec.rb +19 -17
- data/lib/gitlab/experiment/version.rb +1 -1
- data/lib/gitlab/experiment.rb +23 -69
- metadata +32 -47
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c92b7e2d77e534a23b8233f11539fa9c2767b21adab9c247e916933724dd7f5d
|
4
|
+
data.tar.gz: 7a1144676a16835f64eaefb9ea7bac2b7055c5cf8e329d55dba6c115be17eb50
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b7a645b7d2c3cf13d7031fc61dfdaef2626d0092661093a19ff5e21d346adda8cbe031b5c360de21fda3ecee828eaa7bda0a6afe839b6c5ce7ba57f9e10232eb
|
7
|
+
data.tar.gz: 1a8e4672a903746f5f31f2cf5dcf815bb6e2c3cb8c8fa73430c93462977310fcd6ed2f6f8676d5e2ccf856289c6befde6469e9f29715341c4bde7fd0b14fa51b
|
@@ -48,12 +48,10 @@ Gitlab::Experiment.configure do |config|
|
|
48
48
|
# end
|
49
49
|
#
|
50
50
|
# Included rollout strategies:
|
51
|
-
#
|
52
|
-
|
53
|
-
# Gitlab::Experiment::Rollout::Random
|
54
|
-
config.default_rollout = Gitlab::Experiment::Rollout::Percent.new(
|
51
|
+
# :percent (recommended), :round_robin, or :random
|
52
|
+
config.default_rollout = :percent, {
|
55
53
|
include_control: true # include control in possible assignments
|
56
|
-
|
54
|
+
}
|
57
55
|
|
58
56
|
# Secret seed used in generating context keys.
|
59
57
|
#
|
@@ -132,7 +130,7 @@ Gitlab::Experiment.configure do |config|
|
|
132
130
|
# level1 initiated by file_name.rb:2
|
133
131
|
# level2 initiated by file_name.rb:3
|
134
132
|
config.nested_behavior = lambda do |nested_experiment|
|
135
|
-
raise NestingError.new(experiment: self, nested_experiment: nested_experiment)
|
133
|
+
raise Gitlab::Experiment::NestingError.new(experiment: self, nested_experiment: nested_experiment)
|
136
134
|
end
|
137
135
|
|
138
136
|
# Called at the end of every experiment run, with the result.
|
@@ -78,79 +78,33 @@ module Gitlab
|
|
78
78
|
|
79
79
|
alias_method :to_param, :id
|
80
80
|
|
81
|
-
def
|
82
|
-
|
83
|
-
end
|
81
|
+
def process_redirect_url(url)
|
82
|
+
return unless Configuration.redirect_url_validator&.call(url)
|
84
83
|
|
85
|
-
|
86
|
-
|
84
|
+
track('visited', url: url)
|
85
|
+
url # return the url, which allows for mutation
|
87
86
|
end
|
88
87
|
|
89
|
-
|
90
|
-
|
91
|
-
named_variants = %w[control candidate]
|
92
|
-
public_methods.each_with_object(behaviors) do |name, behaviors|
|
93
|
-
name = name.to_s # fixes compatibility for ruby 2.6.x
|
94
|
-
next unless name.end_with?('_behavior')
|
95
|
-
|
96
|
-
behavior_name = name.sub(/_behavior$/, '')
|
97
|
-
registration = named_variants.include?(behavior_name) ? behavior_name : "variant :#{behavior_name}"
|
98
|
-
|
99
|
-
Configuration.deprecated(<<~MESSAGE, version: '0.7.0', stack: 2)
|
100
|
-
using a public `#{name}` method is deprecated and will be removed from {{release}}, instead register variants using:
|
101
|
-
|
102
|
-
class #{self.class.name} < #{Configuration.base_class}
|
103
|
-
#{registration}
|
88
|
+
def key_for(source, seed = name)
|
89
|
+
return source if source.is_a?(String)
|
104
90
|
|
105
|
-
|
91
|
+
source = source.keys + source.values if source.is_a?(Hash)
|
106
92
|
|
107
|
-
|
108
|
-
|
109
|
-
end
|
110
|
-
end
|
111
|
-
MESSAGE
|
112
|
-
|
113
|
-
behaviors[behavior_name] ||= -> { send(name) } # rubocop:disable GitlabSecurity/PublicSend
|
114
|
-
end
|
115
|
-
end
|
93
|
+
ingredients = Array(source).map { |v| identify(v) }
|
94
|
+
ingredients.unshift(seed).unshift(Configuration.context_key_secret)
|
116
95
|
|
117
|
-
|
118
|
-
def session_id
|
119
|
-
Configuration.deprecated(:session_id, 'instead use `id` or use a custom rollout strategy', version: '0.7.0')
|
120
|
-
id
|
96
|
+
Digest::SHA2.new(Configuration.context_key_bit_length).hexdigest(ingredients.join('|')) # rubocop:disable Fips/OpenSSL
|
121
97
|
end
|
122
98
|
|
123
99
|
# @deprecated
|
124
|
-
def
|
125
|
-
Configuration.deprecated(
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
def use(&block)
|
131
|
-
Configuration.deprecated(:use, 'instead use `control`', version: '0.7.0')
|
132
|
-
|
133
|
-
control(&block)
|
134
|
-
end
|
135
|
-
|
136
|
-
# @deprecated
|
137
|
-
def try(name = nil, &block)
|
138
|
-
if name.present?
|
139
|
-
Configuration.deprecated(:try, "instead use `variant(:#{name})`", version: '0.7.0')
|
140
|
-
variant(name, &block)
|
141
|
-
else
|
142
|
-
Configuration.deprecated(:try, 'instead use `candidate`', version: '0.7.0')
|
143
|
-
candidate(&block)
|
144
|
-
end
|
145
|
-
end
|
146
|
-
|
147
|
-
protected
|
148
|
-
|
149
|
-
def cached_variant_resolver(provided_variant)
|
150
|
-
return :control if excluded?
|
100
|
+
def variant_names
|
101
|
+
Configuration.deprecated(
|
102
|
+
:variant_names,
|
103
|
+
'instead use `behavior.names`, which includes :control',
|
104
|
+
version: '0.8.0'
|
105
|
+
)
|
151
106
|
|
152
|
-
|
153
|
-
result.to_sym if result.present?
|
107
|
+
behaviors.keys - [:control]
|
154
108
|
end
|
155
109
|
end
|
156
110
|
end
|
@@ -49,7 +49,7 @@ module Gitlab
|
|
49
49
|
result = migrated_cache_fetch(cache.store, &block)
|
50
50
|
return result unless specified.present?
|
51
51
|
|
52
|
-
cache.write(specified) if result != specified
|
52
|
+
cache.write(specified) if result.to_s != specified.to_s
|
53
53
|
specified
|
54
54
|
end
|
55
55
|
|
@@ -66,10 +66,8 @@ module Gitlab
|
|
66
66
|
|
67
67
|
store.write(cache_key, value)
|
68
68
|
store.delete(old_key)
|
69
|
-
|
70
|
-
end
|
71
|
-
|
72
|
-
store.fetch(cache_key, &block)
|
69
|
+
break value
|
70
|
+
end || store.fetch(cache_key, &block)
|
73
71
|
end
|
74
72
|
end
|
75
73
|
end
|
@@ -47,14 +47,14 @@ module Gitlab
|
|
47
47
|
private
|
48
48
|
|
49
49
|
def build_behavior_callback(filters, variant, **options, &block)
|
50
|
-
if registered_behavior_callbacks[variant
|
50
|
+
if registered_behavior_callbacks[variant]
|
51
51
|
raise ExistingBehaviorError, "a behavior for the `#{variant}` variant has already been registered"
|
52
52
|
end
|
53
53
|
|
54
54
|
callback_behavior = "#{variant}_behavior".to_sym
|
55
55
|
|
56
56
|
# Register a the behavior so we can define the block later.
|
57
|
-
registered_behavior_callbacks[variant
|
57
|
+
registered_behavior_callbacks[variant] = callback_behavior
|
58
58
|
|
59
59
|
# Add our block or default behavior method.
|
60
60
|
filters.push(block) if block.present?
|
@@ -82,7 +82,7 @@ module Gitlab
|
|
82
82
|
end
|
83
83
|
|
84
84
|
def build_run_callback(filters, **options)
|
85
|
-
set_callback(:run, *filters, **options)
|
85
|
+
set_callback(:run, *filters.compact, **options)
|
86
86
|
end
|
87
87
|
|
88
88
|
def build_callback(chain, *filters, **options)
|
@@ -56,12 +56,8 @@ module Gitlab
|
|
56
56
|
# end
|
57
57
|
#
|
58
58
|
# Included rollout strategies:
|
59
|
-
#
|
60
|
-
|
61
|
-
# Gitlab::Experiment::Rollout::Random
|
62
|
-
@default_rollout = Gitlab::Experiment::Rollout::Percent.new(
|
63
|
-
include_control: true # include control in possible assignments
|
64
|
-
)
|
59
|
+
# :percent, (recommended), :round_robin, or :random
|
60
|
+
@default_rollout = Rollout.resolve(:percent)
|
65
61
|
|
66
62
|
# Secret seed used in generating context keys.
|
67
63
|
#
|
@@ -169,61 +165,6 @@ module Gitlab
|
|
169
165
|
end
|
170
166
|
|
171
167
|
class << self
|
172
|
-
# @deprecated
|
173
|
-
def context_hash_strategy=(block)
|
174
|
-
deprecated(
|
175
|
-
:context_hash_strategy,
|
176
|
-
'instead use `context_key_secret` and `context_key_bit_length`',
|
177
|
-
version: '0.7.0'
|
178
|
-
)
|
179
|
-
|
180
|
-
@__context_hash_strategy = block
|
181
|
-
end
|
182
|
-
|
183
|
-
# @deprecated
|
184
|
-
def variant_resolver
|
185
|
-
deprecated(
|
186
|
-
:variant_resolver,
|
187
|
-
'instead use `inclusion_resolver` with a block that returns a boolean',
|
188
|
-
version: '0.6.5'
|
189
|
-
)
|
190
|
-
|
191
|
-
@__inclusion_resolver
|
192
|
-
end
|
193
|
-
|
194
|
-
# @deprecated
|
195
|
-
def variant_resolver=(block)
|
196
|
-
deprecated(
|
197
|
-
:variant_resolver,
|
198
|
-
'instead use `inclusion_resolver` with a block that returns a boolean',
|
199
|
-
version: '0.6.5'
|
200
|
-
)
|
201
|
-
|
202
|
-
@__inclusion_resolver = block
|
203
|
-
end
|
204
|
-
|
205
|
-
# @deprecated
|
206
|
-
def inclusion_resolver=(block)
|
207
|
-
deprecated(
|
208
|
-
:inclusion_resolver,
|
209
|
-
'instead put this logic into custom rollout strategies',
|
210
|
-
version: '0.7.0'
|
211
|
-
)
|
212
|
-
|
213
|
-
@__inclusion_resolver = block
|
214
|
-
end
|
215
|
-
|
216
|
-
# @deprecated
|
217
|
-
def inclusion_resolver
|
218
|
-
deprecated(
|
219
|
-
:inclusion_resolver,
|
220
|
-
'instead put this logic into custom rollout strategies',
|
221
|
-
version: '0.7.0'
|
222
|
-
)
|
223
|
-
|
224
|
-
@__inclusion_resolver
|
225
|
-
end
|
226
|
-
|
227
168
|
attr_accessor(
|
228
169
|
:name_prefix,
|
229
170
|
:logger,
|
@@ -241,7 +182,14 @@ module Gitlab
|
|
241
182
|
:publishing_behavior
|
242
183
|
)
|
243
184
|
|
244
|
-
#
|
185
|
+
# Attribute method overrides.
|
186
|
+
|
187
|
+
def default_rollout=(args) # rubocop:disable Lint/DuplicateMethods
|
188
|
+
@default_rollout = Rollout.resolve(*args)
|
189
|
+
end
|
190
|
+
|
191
|
+
# Internal warning helpers.
|
192
|
+
|
245
193
|
def deprecated(*args, version:, stack: 0)
|
246
194
|
deprecator = deprecator(version)
|
247
195
|
args << args.pop.to_s.gsub('{{release}}', "#{deprecator.gem_name} #{deprecator.deprecation_horizon}")
|
@@ -21,6 +21,7 @@ module Gitlab
|
|
21
21
|
private
|
22
22
|
|
23
23
|
def include_dsl
|
24
|
+
Dsl.include_in(ActionController::API, with_helper: false) if defined?(ActionController)
|
24
25
|
Dsl.include_in(ActionController::Base, with_helper: true) if defined?(ActionController)
|
25
26
|
Dsl.include_in(ActionMailer::Base, with_helper: true) if defined?(ActionMailer)
|
26
27
|
end
|
@@ -25,15 +25,25 @@ module Gitlab
|
|
25
25
|
class Stack
|
26
26
|
include Singleton
|
27
27
|
|
28
|
-
|
28
|
+
delegate :pop, :length, :size, :[], to: :stack
|
29
29
|
|
30
30
|
class << self
|
31
|
-
delegate :pop, :length, :size, :[], to:
|
31
|
+
delegate :pop, :push, :length, :size, :[], to: :instance
|
32
|
+
end
|
33
|
+
|
34
|
+
def initialize
|
35
|
+
@thread_key = "#{self.class};#{object_id}".to_sym
|
36
|
+
end
|
37
|
+
|
38
|
+
def push(instance)
|
39
|
+
stack.last&.nest_experiment(instance)
|
40
|
+
stack.push(instance)
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
32
44
|
|
33
|
-
|
34
|
-
|
35
|
-
@stack.push(instance)
|
36
|
-
end
|
45
|
+
def stack
|
46
|
+
Thread.current[@thread_key] ||= []
|
37
47
|
end
|
38
48
|
end
|
39
49
|
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "zlib"
|
4
|
+
|
3
5
|
# The percent rollout strategy is the most comprehensive included with Gitlab::Experiment. It allows specifying the
|
4
6
|
# percentages per variant using an array, a hash, or will default to even distribution when no rules are provided.
|
5
7
|
#
|
@@ -34,14 +36,14 @@ module Gitlab
|
|
34
36
|
when nil then nil
|
35
37
|
when Array, Hash
|
36
38
|
if distribution_rules.length != behavior_names.length
|
37
|
-
raise InvalidRolloutRules, "the distribution rules don't match the number of
|
39
|
+
raise InvalidRolloutRules, "the distribution rules don't match the number of behaviors defined"
|
38
40
|
end
|
39
41
|
else
|
40
42
|
raise InvalidRolloutRules, 'unknown distribution options type'
|
41
43
|
end
|
42
44
|
end
|
43
45
|
|
44
|
-
def
|
46
|
+
def execute_assignment
|
45
47
|
crc = normalized_id
|
46
48
|
total = 0
|
47
49
|
|
@@ -7,70 +7,61 @@ module Gitlab
|
|
7
7
|
autoload :Random, 'gitlab/experiment/rollout/random.rb'
|
8
8
|
autoload :RoundRobin, 'gitlab/experiment/rollout/round_robin.rb'
|
9
9
|
|
10
|
-
def self.resolve(klass)
|
11
|
-
|
12
|
-
|
13
|
-
|
10
|
+
def self.resolve(klass, options = {})
|
11
|
+
options ||= {}
|
12
|
+
case klass
|
13
|
+
when String
|
14
|
+
Strategy.new(klass.classify.constantize, options)
|
15
|
+
when Symbol
|
16
|
+
Strategy.new("#{name}::#{klass.to_s.classify}".constantize, options)
|
17
|
+
when Class
|
18
|
+
Strategy.new(klass, options)
|
19
|
+
else
|
20
|
+
raise ArgumentError, "unable to resolve rollout from #{klass.inspect}"
|
21
|
+
end
|
14
22
|
end
|
15
23
|
|
16
24
|
class Base
|
17
|
-
DEFAULT_OPTIONS = {
|
18
|
-
include_control: false
|
19
|
-
}.freeze
|
20
|
-
|
21
25
|
attr_reader :experiment, :options
|
22
26
|
|
23
|
-
delegate :
|
24
|
-
|
25
|
-
def initialize(options = {})
|
26
|
-
@options = DEFAULT_OPTIONS.merge(options)
|
27
|
-
end
|
27
|
+
delegate :cache, :id, to: :experiment
|
28
28
|
|
29
|
-
def
|
29
|
+
def initialize(experiment, options = {})
|
30
30
|
raise ArgumentError, 'you must provide an experiment instance' unless experiment.class <= Gitlab::Experiment
|
31
31
|
|
32
32
|
@experiment = experiment
|
33
|
-
|
34
|
-
self
|
33
|
+
@options = options
|
35
34
|
end
|
36
35
|
|
37
36
|
def enabled?
|
38
|
-
require_experiment(__method__)
|
39
|
-
|
40
37
|
true
|
41
38
|
end
|
42
39
|
|
43
40
|
def resolve
|
44
|
-
require_experiment(__method__)
|
45
|
-
|
46
|
-
return nil if @experiment.respond_to?(:experiment_group?) && !@experiment.experiment_group?
|
47
|
-
|
48
41
|
validate! # allow the rollout strategy to validate itself
|
49
42
|
|
50
|
-
assignment =
|
51
|
-
assignment == :control ? nil : assignment # avoid caching control
|
43
|
+
assignment = execute_assignment
|
44
|
+
assignment == :control ? nil : assignment # avoid caching control by returning nil
|
52
45
|
end
|
53
46
|
|
54
|
-
|
47
|
+
private
|
55
48
|
|
56
49
|
def validate!
|
57
50
|
# base is always valid
|
58
51
|
end
|
59
52
|
|
60
|
-
def
|
53
|
+
def execute_assignment
|
61
54
|
behavior_names.first
|
62
55
|
end
|
63
56
|
|
64
|
-
|
65
|
-
|
66
|
-
def require_experiment(method_name)
|
67
|
-
return if @experiment.present?
|
68
|
-
|
69
|
-
raise ArgumentError, "you need to call `for` with an experiment instance before chaining `#{method_name}`"
|
57
|
+
def behavior_names
|
58
|
+
experiment.behaviors.keys
|
70
59
|
end
|
60
|
+
end
|
71
61
|
|
72
|
-
|
73
|
-
|
62
|
+
Strategy = Struct.new(:klass, :options) do
|
63
|
+
def for(experiment)
|
64
|
+
klass.new(experiment, options)
|
74
65
|
end
|
75
66
|
end
|
76
67
|
end
|
@@ -14,13 +14,13 @@ module Gitlab
|
|
14
14
|
def self.track_gitlab_experiment_receiver(method, receiver)
|
15
15
|
# Leverage the `>=` method on Gitlab::Experiment to determine if the receiver is an experiment, not the other
|
16
16
|
# way round -- `receiver.<=` could be mocked and we want to be extra careful.
|
17
|
-
(@__gitlab_experiment_receivers[method
|
17
|
+
(@__gitlab_experiment_receivers[method] ||= []) << receiver if Gitlab::Experiment >= receiver
|
18
18
|
rescue StandardError # again, let's just be extra careful
|
19
19
|
false
|
20
20
|
end
|
21
21
|
|
22
22
|
def self.bind_gitlab_experiment_receiver(method)
|
23
|
-
method.unbind.bind(@__gitlab_experiment_receivers[method
|
23
|
+
method.unbind.bind(@__gitlab_experiment_receivers[method].pop)
|
24
24
|
end
|
25
25
|
|
26
26
|
module MethodDouble
|
@@ -28,6 +28,7 @@ module Gitlab
|
|
28
28
|
RSpecMocks.track_gitlab_experiment_receiver(original_method, receiver)
|
29
29
|
super
|
30
30
|
end
|
31
|
+
ruby2_keywords :proxy_method_invoked if respond_to?(:ruby2_keywords, true)
|
31
32
|
end
|
32
33
|
end
|
33
34
|
|
@@ -37,9 +38,6 @@ module Gitlab
|
|
37
38
|
wrapped_experiment(experiment, remock: true) do |instance, wrapped|
|
38
39
|
# Stub internal methods that will make it behave as we've instructed.
|
39
40
|
allow(instance).to receive(:enabled?) { wrapped.variant_name != false }
|
40
|
-
if instance.respond_to?(:experiment_group?, true)
|
41
|
-
allow(instance).to receive(:experiment_group?) { !(wrapped.variant_name == false) }
|
42
|
-
end
|
43
41
|
|
44
42
|
# Stub the variant resolution logic to handle true/false, and named variants.
|
45
43
|
allow(instance).to receive(:resolve_variant_name).and_wrap_original { |method|
|
@@ -72,8 +70,8 @@ module Gitlab
|
|
72
70
|
def wrapped_experiment_chain_for(klass)
|
73
71
|
@__wrapped_experiment_chains ||= {}
|
74
72
|
@__wrapped_experiment_chains[klass.name || klass.object_id] ||= begin
|
75
|
-
allow(klass).to receive(:new).and_wrap_original do |method, *args, &original_block|
|
76
|
-
RSpecMocks.bind_gitlab_experiment_receiver(method).call(*args).tap do |instance|
|
73
|
+
allow(klass).to receive(:new).and_wrap_original do |method, *args, **kwargs, &original_block|
|
74
|
+
RSpecMocks.bind_gitlab_experiment_receiver(method).call(*args, **kwargs).tap do |instance|
|
77
75
|
wrapped = @__wrapped_experiments[instance.instance_variable_get(:@_name)]
|
78
76
|
wrapped&.blocks&.each { |b| b.call(instance, wrapped) }
|
79
77
|
|
@@ -128,7 +126,7 @@ module Gitlab
|
|
128
126
|
match do |experiment|
|
129
127
|
@experiment = require_experiment(experiment, 'register_behavior')
|
130
128
|
|
131
|
-
block = @experiment.behaviors[behavior_name
|
129
|
+
block = @experiment.behaviors[behavior_name]
|
132
130
|
@return_expected = false unless block
|
133
131
|
|
134
132
|
if @return_expected
|
@@ -193,13 +191,13 @@ module Gitlab
|
|
193
191
|
@experiment.run_callbacks(:segmentation)
|
194
192
|
|
195
193
|
@actual_variant = @experiment.instance_variable_get(:@_assigned_variant_name)
|
196
|
-
@expected_variant ? @actual_variant
|
194
|
+
@expected_variant ? @actual_variant == @expected_variant : @actual_variant.present?
|
197
195
|
end
|
198
196
|
|
199
197
|
chain :into do |expected|
|
200
198
|
raise ArgumentError, 'variant name must be provided' if expected.blank?
|
201
199
|
|
202
|
-
@expected_variant = expected
|
200
|
+
@expected_variant = expected
|
203
201
|
end
|
204
202
|
|
205
203
|
failure_message do
|
@@ -239,7 +237,7 @@ module Gitlab
|
|
239
237
|
chain(:for) do |expected|
|
240
238
|
raise ArgumentError, 'variant name must be provided' if expected.blank?
|
241
239
|
|
242
|
-
@expected_variant = expected
|
240
|
+
@expected_variant = expected
|
243
241
|
end
|
244
242
|
|
245
243
|
chain(:with_context) do |expected|
|
@@ -303,17 +301,21 @@ RSpec.configure do |config|
|
|
303
301
|
config.include Gitlab::Experiment::RSpecHelpers
|
304
302
|
config.include Gitlab::Experiment::Dsl
|
305
303
|
|
306
|
-
config.before(:each
|
307
|
-
|
304
|
+
config.before(:each) do |example|
|
305
|
+
if example.metadata[:experiment] == true || example.metadata[:type] == :experiment
|
306
|
+
RequestStore.clear!
|
308
307
|
|
309
|
-
|
310
|
-
|
308
|
+
if defined?(Gitlab::Experiment::TestBehaviors::TrackedStructure)
|
309
|
+
Gitlab::Experiment::TestBehaviors::TrackedStructure.reset!
|
310
|
+
end
|
311
311
|
end
|
312
312
|
end
|
313
313
|
|
314
314
|
config.include Gitlab::Experiment::RSpecMatchers, :experiment
|
315
|
-
config.
|
316
|
-
|
315
|
+
config.include Gitlab::Experiment::RSpecMatchers, type: :experiment
|
316
|
+
|
317
|
+
config.define_derived_metadata(file_path: Regexp.new('spec/experiments/')) do |metadata|
|
318
|
+
metadata[:type] ||= :experiment
|
317
319
|
end
|
318
320
|
|
319
321
|
# We need to monkeypatch rspec-mocks because there's an issue around stubbing class methods that impacts us here.
|
data/lib/gitlab/experiment.rb
CHANGED
@@ -3,6 +3,8 @@
|
|
3
3
|
require 'request_store'
|
4
4
|
require 'active_support'
|
5
5
|
require 'active_support/core_ext/module/delegation'
|
6
|
+
require 'active_support/core_ext/object/blank'
|
7
|
+
require 'active_support/core_ext/string/inflections'
|
6
8
|
|
7
9
|
require 'gitlab/experiment/errors'
|
8
10
|
require 'gitlab/experiment/base_interface'
|
@@ -68,7 +70,7 @@ module Gitlab
|
|
68
70
|
def default_rollout(rollout = nil, options = {})
|
69
71
|
return @_rollout ||= Configuration.default_rollout if rollout.blank?
|
70
72
|
|
71
|
-
@_rollout = Rollout.resolve(rollout
|
73
|
+
@_rollout = Rollout.resolve(rollout, options)
|
72
74
|
end
|
73
75
|
|
74
76
|
# Class level accessor methods.
|
@@ -86,34 +88,15 @@ module Gitlab
|
|
86
88
|
variant(:control, &block)
|
87
89
|
end
|
88
90
|
|
89
|
-
def candidate(
|
90
|
-
|
91
|
-
Configuration.deprecated(<<~MESSAGE, version: '0.7.0')
|
92
|
-
passing name to `candidate` is deprecated and will be removed from {{release}} (instead use `variant(#{name.inspect})`)
|
93
|
-
MESSAGE
|
94
|
-
end
|
95
|
-
|
96
|
-
variant(name || :candidate, &block)
|
91
|
+
def candidate(&block)
|
92
|
+
variant(:candidate, &block)
|
97
93
|
end
|
98
94
|
|
99
|
-
def variant(name
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
return behaviors[name.to_s] = block
|
104
|
-
end
|
105
|
-
|
106
|
-
if name.present?
|
107
|
-
Configuration.deprecated(<<~MESSAGE, version: '0.7.0')
|
108
|
-
setting the variant using `variant` is deprecated and will be removed from {{release}} (instead use `assigned(#{name.inspect})`)
|
109
|
-
MESSAGE
|
110
|
-
else
|
111
|
-
Configuration.deprecated(<<~MESSAGE, version: '0.7.0')
|
112
|
-
getting the assigned variant using `variant` is deprecated and will be removed from {{release}} (instead use `assigned`)
|
113
|
-
MESSAGE
|
114
|
-
end
|
95
|
+
def variant(name, &block)
|
96
|
+
raise ArgumentError, 'name required' if name.blank?
|
97
|
+
raise ArgumentError, 'block required' unless block.present?
|
115
98
|
|
116
|
-
|
99
|
+
behaviors[name] = block
|
117
100
|
end
|
118
101
|
|
119
102
|
def context(value = nil)
|
@@ -125,9 +108,7 @@ module Gitlab
|
|
125
108
|
|
126
109
|
def assigned(value = nil)
|
127
110
|
@_assigned_variant_name = cache_variant(value) if value.present?
|
128
|
-
if @_assigned_variant_name || @_resolving_variant
|
129
|
-
return Variant.new(name: (@_assigned_variant_name || :unresolved).to_s)
|
130
|
-
end
|
111
|
+
return Variant.new(name: @_assigned_variant_name || :unresolved) if @_assigned_variant_name || @_resolving_variant
|
131
112
|
|
132
113
|
if enabled?
|
133
114
|
@_resolving_variant = true
|
@@ -136,7 +117,7 @@ module Gitlab
|
|
136
117
|
|
137
118
|
run_callbacks(segmentation_callback_chain) do
|
138
119
|
@_assigned_variant_name ||= :control
|
139
|
-
Variant.new(name: @_assigned_variant_name
|
120
|
+
Variant.new(name: @_assigned_variant_name)
|
140
121
|
end
|
141
122
|
ensure
|
142
123
|
@_resolving_variant = false
|
@@ -145,7 +126,7 @@ module Gitlab
|
|
145
126
|
def rollout(rollout = nil, options = {})
|
146
127
|
return @_rollout ||= self.class.default_rollout(nil, options).for(self) if rollout.blank?
|
147
128
|
|
148
|
-
@_rollout = Rollout.resolve(rollout
|
129
|
+
@_rollout = Rollout.resolve(rollout, options).for(self)
|
149
130
|
end
|
150
131
|
|
151
132
|
def exclude!
|
@@ -170,13 +151,6 @@ module Gitlab
|
|
170
151
|
instance_exec(action, tracking_context(event_args).try(:compact) || {}, &Configuration.tracking_behavior)
|
171
152
|
end
|
172
153
|
|
173
|
-
def process_redirect_url(url)
|
174
|
-
return unless Configuration.redirect_url_validator&.call(url)
|
175
|
-
|
176
|
-
track('visited', url: url)
|
177
|
-
url # return the url, which allows for mutation
|
178
|
-
end
|
179
|
-
|
180
154
|
def enabled?
|
181
155
|
rollout.enabled?
|
182
156
|
end
|
@@ -192,23 +166,11 @@ module Gitlab
|
|
192
166
|
end
|
193
167
|
|
194
168
|
def signature
|
195
|
-
{ variant: assigned.name, experiment: name }.merge(context.signature)
|
169
|
+
{ variant: assigned.name.to_s, experiment: name }.merge(context.signature)
|
196
170
|
end
|
197
171
|
|
198
|
-
def
|
199
|
-
|
200
|
-
if (block = Configuration.instance_variable_get(:@__context_hash_strategy))
|
201
|
-
return instance_exec(source, seed, &block)
|
202
|
-
end
|
203
|
-
|
204
|
-
return source if source.is_a?(String)
|
205
|
-
|
206
|
-
source = source.keys + source.values if source.is_a?(Hash)
|
207
|
-
|
208
|
-
ingredients = Array(source).map { |v| identify(v) }
|
209
|
-
ingredients.unshift(seed).unshift(Configuration.context_key_secret)
|
210
|
-
|
211
|
-
Digest::SHA2.new(Configuration.context_key_bit_length).hexdigest(ingredients.join('|'))
|
172
|
+
def behaviors
|
173
|
+
@_behaviors ||= registered_behavior_callbacks
|
212
174
|
end
|
213
175
|
|
214
176
|
protected
|
@@ -217,23 +179,15 @@ module Gitlab
|
|
217
179
|
(object.respond_to?(:to_global_id) ? object.to_global_id : object).to_s
|
218
180
|
end
|
219
181
|
|
182
|
+
def cached_variant_resolver(provided_variant)
|
183
|
+
return :control if excluded?
|
184
|
+
|
185
|
+
result = cache_variant(provided_variant) { resolve_variant_name }
|
186
|
+
result.to_sym if result.present?
|
187
|
+
end
|
188
|
+
|
220
189
|
def resolve_variant_name
|
221
|
-
|
222
|
-
# TODO: Remove - deprecated in release 0.7.0
|
223
|
-
Configuration.deprecated(:experiment_group?, <<~MESSAGE, version: '0.7.0')
|
224
|
-
instead put this logic into custom rollout strategies
|
225
|
-
MESSAGE
|
226
|
-
|
227
|
-
rollout.resolve if experiment_group?
|
228
|
-
elsif (block = Configuration.instance_variable_get(:@__inclusion_resolver))
|
229
|
-
# TODO: Remove - deprecated in release 0.7.0
|
230
|
-
rollout.resolve if instance_exec(@_assigned_variant_name, &block)
|
231
|
-
elsif (block = Configuration.instance_variable_get(:@__variant_resolver))
|
232
|
-
# TODO: Remove - deprecated in release 0.6.5
|
233
|
-
instance_exec(@_assigned_variant_name, &block)
|
234
|
-
else
|
235
|
-
rollout.resolve # this is the end result of all deprecations
|
236
|
-
end
|
190
|
+
rollout.resolve
|
237
191
|
end
|
238
192
|
|
239
193
|
def tracking_context(event_args)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: gitlab-experiment
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- GitLab
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-03-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -38,67 +38,52 @@ dependencies:
|
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '1.0'
|
41
|
-
description:
|
41
|
+
description:
|
42
42
|
email:
|
43
43
|
- gitlab_rubygems@gitlab.com
|
44
44
|
executables: []
|
45
45
|
extensions: []
|
46
46
|
extra_rdoc_files: []
|
47
47
|
files:
|
48
|
-
-
|
49
|
-
-
|
50
|
-
- lib/generators/test_unit/experiment/templates
|
51
|
-
- lib/generators/test_unit/experiment/templates/experiment_test.rb.tt
|
52
|
-
- lib/generators/test_unit/experiment/experiment_generator.rb
|
53
|
-
- lib/generators/rspec
|
54
|
-
- lib/generators/rspec/experiment
|
55
|
-
- lib/generators/rspec/experiment/templates
|
56
|
-
- lib/generators/rspec/experiment/templates/experiment_spec.rb.tt
|
57
|
-
- lib/generators/rspec/experiment/experiment_generator.rb
|
58
|
-
- lib/generators/gitlab
|
59
|
-
- lib/generators/gitlab/experiment
|
48
|
+
- LICENSE.txt
|
49
|
+
- README.md
|
60
50
|
- lib/generators/gitlab/experiment/USAGE
|
61
|
-
- lib/generators/gitlab/experiment/
|
62
|
-
- lib/generators/gitlab/experiment/
|
63
|
-
- lib/generators/gitlab/experiment/install
|
64
|
-
- lib/generators/gitlab/experiment/install/templates
|
51
|
+
- lib/generators/gitlab/experiment/experiment_generator.rb
|
52
|
+
- lib/generators/gitlab/experiment/install/install_generator.rb
|
53
|
+
- lib/generators/gitlab/experiment/install/templates/POST_INSTALL
|
65
54
|
- lib/generators/gitlab/experiment/install/templates/application_experiment.rb.tt
|
66
55
|
- lib/generators/gitlab/experiment/install/templates/initializer.rb.tt
|
67
|
-
- lib/generators/gitlab/experiment/
|
68
|
-
- lib/generators/
|
69
|
-
- lib/generators/
|
70
|
-
- lib/
|
71
|
-
- lib/
|
72
|
-
- lib/gitlab/experiment
|
73
|
-
- lib/gitlab/experiment/context.rb
|
74
|
-
- lib/gitlab/experiment/nestable.rb
|
75
|
-
- lib/gitlab/experiment/configuration.rb
|
76
|
-
- lib/gitlab/experiment/rollout.rb
|
77
|
-
- lib/gitlab/experiment/cache
|
78
|
-
- lib/gitlab/experiment/cache/redis_hash_store.rb
|
79
|
-
- lib/gitlab/experiment/engine.rb
|
56
|
+
- lib/generators/gitlab/experiment/templates/experiment.rb.tt
|
57
|
+
- lib/generators/rspec/experiment/experiment_generator.rb
|
58
|
+
- lib/generators/rspec/experiment/templates/experiment_spec.rb.tt
|
59
|
+
- lib/generators/test_unit/experiment/experiment_generator.rb
|
60
|
+
- lib/generators/test_unit/experiment/templates/experiment_test.rb.tt
|
61
|
+
- lib/gitlab/experiment.rb
|
80
62
|
- lib/gitlab/experiment/base_interface.rb
|
81
|
-
- lib/gitlab/experiment/
|
82
|
-
- lib/gitlab/experiment/
|
63
|
+
- lib/gitlab/experiment/cache.rb
|
64
|
+
- lib/gitlab/experiment/cache/redis_hash_store.rb
|
65
|
+
- lib/gitlab/experiment/callbacks.rb
|
66
|
+
- lib/gitlab/experiment/configuration.rb
|
67
|
+
- lib/gitlab/experiment/context.rb
|
83
68
|
- lib/gitlab/experiment/cookies.rb
|
69
|
+
- lib/gitlab/experiment/dsl.rb
|
70
|
+
- lib/gitlab/experiment/engine.rb
|
84
71
|
- lib/gitlab/experiment/errors.rb
|
85
|
-
- lib/gitlab/experiment/
|
86
|
-
- lib/gitlab/experiment/
|
87
|
-
- lib/gitlab/experiment/rollout
|
88
|
-
- lib/gitlab/experiment/rollout/random.rb
|
72
|
+
- lib/gitlab/experiment/middleware.rb
|
73
|
+
- lib/gitlab/experiment/nestable.rb
|
74
|
+
- lib/gitlab/experiment/rollout.rb
|
89
75
|
- lib/gitlab/experiment/rollout/percent.rb
|
76
|
+
- lib/gitlab/experiment/rollout/random.rb
|
90
77
|
- lib/gitlab/experiment/rollout/round_robin.rb
|
91
|
-
- lib/gitlab/experiment/
|
92
|
-
- lib/gitlab/experiment/test_behaviors
|
78
|
+
- lib/gitlab/experiment/rspec.rb
|
93
79
|
- lib/gitlab/experiment/test_behaviors/trackable.rb
|
94
|
-
- lib/gitlab/experiment.rb
|
95
|
-
-
|
96
|
-
- README.md
|
80
|
+
- lib/gitlab/experiment/variant.rb
|
81
|
+
- lib/gitlab/experiment/version.rb
|
97
82
|
homepage: https://gitlab.com/gitlab-org/ruby/gems/gitlab-experiment
|
98
83
|
licenses:
|
99
84
|
- MIT
|
100
85
|
metadata: {}
|
101
|
-
post_install_message:
|
86
|
+
post_install_message:
|
102
87
|
rdoc_options: []
|
103
88
|
require_paths:
|
104
89
|
- lib
|
@@ -113,8 +98,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
113
98
|
- !ruby/object:Gem::Version
|
114
99
|
version: '0'
|
115
100
|
requirements: []
|
116
|
-
rubygems_version: 3.
|
117
|
-
signing_key:
|
101
|
+
rubygems_version: 3.4.7
|
102
|
+
signing_key:
|
118
103
|
specification_version: 4
|
119
104
|
summary: GitLab experimentation library.
|
120
105
|
test_files: []
|