gitlab-experiment 0.6.4 → 0.7.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +410 -290
- data/lib/generators/gitlab/experiment/experiment_generator.rb +9 -4
- data/lib/generators/gitlab/experiment/install/templates/initializer.rb.tt +87 -45
- data/lib/generators/gitlab/experiment/templates/experiment.rb.tt +69 -3
- data/lib/gitlab/experiment/base_interface.rb +86 -24
- data/lib/gitlab/experiment/cache/redis_hash_store.rb +10 -10
- data/lib/gitlab/experiment/cache.rb +3 -7
- data/lib/gitlab/experiment/callbacks.rb +97 -5
- data/lib/gitlab/experiment/configuration.rb +209 -28
- data/lib/gitlab/experiment/context.rb +2 -3
- data/lib/gitlab/experiment/cookies.rb +0 -2
- data/lib/gitlab/experiment/engine.rb +2 -1
- data/lib/gitlab/experiment/errors.rb +21 -0
- data/lib/gitlab/experiment/nestable.rb +51 -0
- data/lib/gitlab/experiment/rollout/percent.rb +41 -16
- data/lib/gitlab/experiment/rollout/random.rb +25 -4
- data/lib/gitlab/experiment/rollout/round_robin.rb +27 -10
- data/lib/gitlab/experiment/rollout.rb +61 -12
- data/lib/gitlab/experiment/rspec.rb +224 -130
- data/lib/gitlab/experiment/test_behaviors/trackable.rb +69 -0
- data/lib/gitlab/experiment/version.rb +1 -1
- data/lib/gitlab/experiment.rb +118 -56
- metadata +8 -24
@@ -4,86 +4,227 @@ require 'singleton'
|
|
4
4
|
require 'logger'
|
5
5
|
require 'digest'
|
6
6
|
|
7
|
-
require 'active_support/deprecation'
|
8
|
-
|
9
7
|
module Gitlab
|
10
8
|
class Experiment
|
11
9
|
class Configuration
|
12
10
|
include Singleton
|
13
11
|
|
14
|
-
# Prefix all experiment names with a given value.
|
12
|
+
# Prefix all experiment names with a given string value.
|
13
|
+
# Use `nil` for no prefix.
|
15
14
|
@name_prefix = nil
|
16
15
|
|
17
|
-
# The logger
|
16
|
+
# The logger can be used to log various details of the experiments.
|
18
17
|
@logger = Logger.new($stdout)
|
19
18
|
|
20
19
|
# The base class that should be instantiated for basic experiments.
|
20
|
+
# It should be a string, so we can constantize it later.
|
21
21
|
@base_class = 'Gitlab::Experiment'
|
22
22
|
|
23
|
-
#
|
23
|
+
# Require experiments to be defined in a class, with variants registered.
|
24
|
+
# This will disallow any anonymous experiments that are run inline
|
25
|
+
# without previously defining a class.
|
26
|
+
@strict_registration = false
|
27
|
+
|
28
|
+
# The caching layer is expected to match the Rails.cache interface.
|
29
|
+
# If no cache is provided some rollout strategies may behave differently.
|
30
|
+
# Use `nil` for no caching.
|
24
31
|
@cache = nil
|
25
32
|
|
26
33
|
# The domain to use on cookies.
|
34
|
+
#
|
35
|
+
# When not set, it uses the current host. If you want to provide specific
|
36
|
+
# hosts, you use `:all`, or provide an array.
|
37
|
+
#
|
38
|
+
# Examples:
|
39
|
+
# nil, :all, or ['www.gitlab.com', '.gitlab.com']
|
27
40
|
@cookie_domain = :all
|
28
41
|
|
29
|
-
# The default rollout strategy
|
30
|
-
#
|
31
|
-
#
|
32
|
-
|
42
|
+
# The default rollout strategy.
|
43
|
+
#
|
44
|
+
# The recommended default rollout strategy when not using caching would
|
45
|
+
# be `Gitlab::Experiment::Rollout::Percent` as that will consistently
|
46
|
+
# assign the same variant with or without caching.
|
47
|
+
#
|
48
|
+
# Gitlab::Experiment::Rollout::Base can be inherited to implement your
|
49
|
+
# own rollout strategies.
|
50
|
+
#
|
51
|
+
# Each experiment can specify its own rollout strategy:
|
52
|
+
#
|
53
|
+
# class ExampleExperiment < ApplicationExperiment
|
54
|
+
# default_rollout :random, # :percent, :round_robin,
|
55
|
+
# include_control: true # or MyCustomRollout
|
56
|
+
# end
|
57
|
+
#
|
58
|
+
# Included rollout strategies:
|
59
|
+
# :percent, (recommended), :round_robin, or :random
|
60
|
+
@default_rollout = Rollout.resolve(:percent, include_control: true)
|
33
61
|
|
34
62
|
# Secret seed used in generating context keys.
|
63
|
+
#
|
64
|
+
# You'll typically want to use an environment variable or secret value
|
65
|
+
# for this.
|
66
|
+
#
|
67
|
+
# Consider not using one that's shared with other systems, like Rails'
|
68
|
+
# SECRET_KEY_BASE for instance. Generate a new secret and utilize that
|
69
|
+
# instead.
|
35
70
|
@context_key_secret = nil
|
36
71
|
|
37
|
-
# Bit length used by SHA2 in generating context keys
|
72
|
+
# Bit length used by SHA2 in generating context keys.
|
73
|
+
#
|
74
|
+
# Using a higher bit length would require more computation time.
|
75
|
+
#
|
76
|
+
# Valid bit lengths:
|
77
|
+
# 256, 384, or 512
|
38
78
|
@context_key_bit_length = 256
|
39
79
|
|
40
80
|
# The default base path that the middleware (or rails engine) will be
|
41
|
-
# mounted.
|
81
|
+
# mounted. The middleware enables an instrumentation url, that's similar
|
82
|
+
# to links that can be instrumented in email campaigns.
|
83
|
+
#
|
84
|
+
# Use `nil` if you don't want to mount the middleware.
|
85
|
+
#
|
86
|
+
# Examples:
|
87
|
+
# '/-/experiment', '/redirect', nil
|
42
88
|
@mount_at = nil
|
43
89
|
|
44
|
-
#
|
45
|
-
#
|
46
|
-
|
47
|
-
|
48
|
-
#
|
90
|
+
# When using the middleware, links can be instrumented and redirected
|
91
|
+
# elsewhere. This can be exploited to make a harmful url look innocuous
|
92
|
+
# or that it's a valid url on your domain. To avoid this, you can provide
|
93
|
+
# your own logic for what urls will be considered valid and redirected
|
94
|
+
# to.
|
95
|
+
#
|
49
96
|
# Expected to return a boolean value.
|
50
|
-
@
|
97
|
+
@redirect_url_validator = lambda do |_redirect_url|
|
98
|
+
true
|
99
|
+
end
|
51
100
|
|
52
101
|
# Tracking behavior can be implemented to link an event to an experiment.
|
102
|
+
#
|
103
|
+
# This block is executed within the scope of the experiment and so can
|
104
|
+
# access experiment methods, like `name`, `context`, and `signature`.
|
53
105
|
@tracking_behavior = lambda do |event, args|
|
106
|
+
# An example of using a generic logger to track events:
|
54
107
|
Configuration.logger.info("#{self.class.name}[#{name}] #{event}: #{args.merge(signature: signature)}")
|
108
|
+
|
109
|
+
# Using something like snowplow to track events (in gitlab):
|
110
|
+
#
|
111
|
+
# Gitlab::Tracking.event(name, event, **args.merge(
|
112
|
+
# context: (args[:context] || []) << SnowplowTracker::SelfDescribingJson.new(
|
113
|
+
# 'iglu:com.gitlab/gitlab_experiment/jsonschema/0-2-0', signature
|
114
|
+
# )
|
115
|
+
# ))
|
116
|
+
end
|
117
|
+
|
118
|
+
# Logic designed to respond when a given experiment is nested within
|
119
|
+
# another experiment. This can be useful to identify overlaps and when a
|
120
|
+
# code path leads to an experiment being nested within another.
|
121
|
+
#
|
122
|
+
# Reporting complexity can arise when one experiment changes rollout, and
|
123
|
+
# a downstream experiment is impacted by that.
|
124
|
+
#
|
125
|
+
# The base_class or a custom experiment can provide a `nest_experiment`
|
126
|
+
# method that implements its own logic that may allow certain experiments
|
127
|
+
# to be nested within it.
|
128
|
+
#
|
129
|
+
# This block is executed within the scope of the experiment and so can
|
130
|
+
# access experiment methods, like `name`, `context`, and `signature`.
|
131
|
+
#
|
132
|
+
# The default exception will include the where the experiment calls were
|
133
|
+
# initiated on, so for instance:
|
134
|
+
#
|
135
|
+
# Gitlab::Experiment::NestingError: unable to nest level2 within level1:
|
136
|
+
# level1 initiated by file_name.rb:2
|
137
|
+
# level2 initiated by file_name.rb:3
|
138
|
+
@nested_behavior = lambda do |nested_experiment|
|
139
|
+
raise NestingError.new(experiment: self, nested_experiment: nested_experiment)
|
55
140
|
end
|
56
141
|
|
57
142
|
# Called at the end of every experiment run, with the result.
|
143
|
+
#
|
144
|
+
# You may want to track that you've assigned a variant to a given
|
145
|
+
# context, or push the experiment into the client or publish results
|
146
|
+
# elsewhere like into redis.
|
147
|
+
#
|
148
|
+
# This block is executed within the scope of the experiment and so can
|
149
|
+
# access experiment methods, like `name`, `context`, and `signature`.
|
58
150
|
@publishing_behavior = lambda do |_result|
|
151
|
+
# Track the event using our own configured tracking logic.
|
59
152
|
track(:assignment)
|
153
|
+
|
154
|
+
# Log using our logging system, so the result (which can be large) can
|
155
|
+
# be reviewed later if we want to.
|
156
|
+
#
|
157
|
+
# Lograge::Event.log(experiment: name, result: result, signature: signature)
|
158
|
+
|
159
|
+
# Experiments that have been run during the request lifecycle can be
|
160
|
+
# pushed to the client layer by injecting the published experiments
|
161
|
+
# into javascript in a layout or view using something like:
|
162
|
+
#
|
163
|
+
# = javascript_tag(nonce: content_security_policy_nonce) do
|
164
|
+
# window.experiments = #{raw Gitlab::Experiment.published_experiments.to_json};
|
60
165
|
end
|
61
166
|
|
62
167
|
class << self
|
63
|
-
#
|
168
|
+
# @deprecated
|
64
169
|
def context_hash_strategy=(block)
|
65
|
-
|
66
|
-
|
170
|
+
deprecated(
|
171
|
+
:context_hash_strategy,
|
172
|
+
'instead use `context_key_secret` and `context_key_bit_length`',
|
173
|
+
version: '0.7.0'
|
174
|
+
)
|
175
|
+
|
67
176
|
@__context_hash_strategy = block
|
68
177
|
end
|
69
178
|
|
70
|
-
#
|
179
|
+
# @deprecated
|
71
180
|
def variant_resolver
|
72
|
-
|
73
|
-
|
74
|
-
|
181
|
+
deprecated(
|
182
|
+
:variant_resolver,
|
183
|
+
'instead use `inclusion_resolver` with a block that returns a boolean',
|
184
|
+
version: '0.6.5'
|
185
|
+
)
|
186
|
+
|
187
|
+
@__inclusion_resolver
|
75
188
|
end
|
76
189
|
|
190
|
+
# @deprecated
|
77
191
|
def variant_resolver=(block)
|
78
|
-
|
79
|
-
|
80
|
-
|
192
|
+
deprecated(
|
193
|
+
:variant_resolver,
|
194
|
+
'instead use `inclusion_resolver` with a block that returns a boolean',
|
195
|
+
version: '0.6.5'
|
196
|
+
)
|
197
|
+
|
198
|
+
@__inclusion_resolver = block
|
199
|
+
end
|
200
|
+
|
201
|
+
# @deprecated
|
202
|
+
def inclusion_resolver=(block)
|
203
|
+
deprecated(
|
204
|
+
:inclusion_resolver,
|
205
|
+
'instead put this logic into custom rollout strategies',
|
206
|
+
version: '0.7.0'
|
207
|
+
)
|
208
|
+
|
209
|
+
@__inclusion_resolver = block
|
210
|
+
end
|
211
|
+
|
212
|
+
# @deprecated
|
213
|
+
def inclusion_resolver
|
214
|
+
deprecated(
|
215
|
+
:inclusion_resolver,
|
216
|
+
'instead put this logic into custom rollout strategies',
|
217
|
+
version: '0.7.0'
|
218
|
+
)
|
219
|
+
|
220
|
+
@__inclusion_resolver
|
81
221
|
end
|
82
222
|
|
83
223
|
attr_accessor(
|
84
224
|
:name_prefix,
|
85
225
|
:logger,
|
86
226
|
:base_class,
|
227
|
+
:strict_registration,
|
87
228
|
:cache,
|
88
229
|
:cookie_domain,
|
89
230
|
:context_key_secret,
|
@@ -91,10 +232,50 @@ module Gitlab
|
|
91
232
|
:mount_at,
|
92
233
|
:default_rollout,
|
93
234
|
:redirect_url_validator,
|
94
|
-
:inclusion_resolver,
|
95
235
|
:tracking_behavior,
|
236
|
+
:nested_behavior,
|
96
237
|
:publishing_behavior
|
97
238
|
)
|
239
|
+
|
240
|
+
# Attribute method overrides.
|
241
|
+
|
242
|
+
def default_rollout=(args) # rubocop:disable Lint/DuplicateMethods
|
243
|
+
rollout, options = Array(args)
|
244
|
+
if rollout.is_a?(Rollout::Base)
|
245
|
+
options = rollout.options
|
246
|
+
rollout = rollout.class
|
247
|
+
|
248
|
+
deprecated(<<~MESSAGE, version: '0.7.0')
|
249
|
+
using a rollout instance with `default_rollout` is deprecated and will be removed from {{release}} (instead use `default_rollout = #{rollout.name}, #{options.inspect}`)
|
250
|
+
MESSAGE
|
251
|
+
end
|
252
|
+
|
253
|
+
@default_rollout = Rollout.resolve(rollout, options || {})
|
254
|
+
end
|
255
|
+
|
256
|
+
# Internal warning helpers.
|
257
|
+
|
258
|
+
def deprecated(*args, version:, stack: 0)
|
259
|
+
deprecator = deprecator(version)
|
260
|
+
args << args.pop.to_s.gsub('{{release}}', "#{deprecator.gem_name} #{deprecator.deprecation_horizon}")
|
261
|
+
args << caller_locations(4 + stack)
|
262
|
+
|
263
|
+
if args.length == 2
|
264
|
+
deprecator.warn(*args)
|
265
|
+
else
|
266
|
+
args[0] = "`#{args[0]}`"
|
267
|
+
deprecator.deprecation_warning(*args)
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
private
|
272
|
+
|
273
|
+
def deprecator(version = VERSION)
|
274
|
+
version = Gem::Version.new(version).bump.to_s
|
275
|
+
|
276
|
+
@__dep_versions ||= {}
|
277
|
+
@__dep_versions[version] ||= ActiveSupport::Deprecation.new(version, 'Gitlab::Experiment')
|
278
|
+
end
|
98
279
|
end
|
99
280
|
end
|
100
281
|
end
|
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'gitlab/experiment/cookies'
|
4
|
-
|
5
3
|
module Gitlab
|
6
4
|
class Experiment
|
7
5
|
class Context
|
@@ -27,6 +25,7 @@ module Gitlab
|
|
27
25
|
|
28
26
|
value = value.dup # dup so we don't mutate
|
29
27
|
reinitialize(value.delete(:request))
|
28
|
+
key(value.delete(:sticky_to))
|
30
29
|
|
31
30
|
@value.merge!(process_migrations(value))
|
32
31
|
end
|
@@ -34,7 +33,7 @@ module Gitlab
|
|
34
33
|
def key(key = nil)
|
35
34
|
return @key || @experiment.key_for(value) if key.nil?
|
36
35
|
|
37
|
-
@key = key
|
36
|
+
@key = @experiment.key_for(key)
|
38
37
|
end
|
39
38
|
|
40
39
|
def trackable?
|
@@ -6,7 +6,8 @@ module Gitlab
|
|
6
6
|
class Experiment
|
7
7
|
include ActiveModel::Model
|
8
8
|
|
9
|
-
#
|
9
|
+
# Used for generating routes. We've included the method and `ActiveModel::Model` here because these things don't
|
10
|
+
# make sense outside of Rails environments.
|
10
11
|
def self.model_name
|
11
12
|
ActiveModel::Name.new(self, Gitlab)
|
12
13
|
end
|
@@ -4,5 +4,26 @@ module Gitlab
|
|
4
4
|
class Experiment
|
5
5
|
Error = Class.new(StandardError)
|
6
6
|
InvalidRolloutRules = Class.new(Error)
|
7
|
+
UnregisteredExperiment = Class.new(Error)
|
8
|
+
ExistingBehaviorError = Class.new(Error)
|
9
|
+
BehaviorMissingError = Class.new(Error)
|
10
|
+
|
11
|
+
class NestingError < Error
|
12
|
+
def initialize(experiment:, nested_experiment:)
|
13
|
+
messages = []
|
14
|
+
experiments = [nested_experiment, experiment]
|
15
|
+
|
16
|
+
callers = caller_locations
|
17
|
+
callers.select.with_index do |caller, index|
|
18
|
+
next if caller.label != 'experiment'
|
19
|
+
|
20
|
+
messages << " #{experiments[messages.length].name} initiated by #{callers[index + 1]}"
|
21
|
+
end
|
22
|
+
|
23
|
+
messages << ["unable to nest #{nested_experiment.name} within #{experiment.name}:"]
|
24
|
+
|
25
|
+
super(messages.reverse.join("\n"))
|
26
|
+
end
|
27
|
+
end
|
7
28
|
end
|
8
29
|
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Gitlab
|
4
|
+
class Experiment
|
5
|
+
module Nestable
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
set_callback :run, :around, :manage_nested_stack
|
10
|
+
end
|
11
|
+
|
12
|
+
def nest_experiment(nested_experiment)
|
13
|
+
instance_exec(nested_experiment, &Configuration.nested_behavior)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def manage_nested_stack
|
19
|
+
Stack.push(self)
|
20
|
+
yield
|
21
|
+
ensure
|
22
|
+
Stack.pop
|
23
|
+
end
|
24
|
+
|
25
|
+
class Stack
|
26
|
+
include Singleton
|
27
|
+
|
28
|
+
delegate :pop, :length, :size, :[], to: :stack
|
29
|
+
|
30
|
+
class << self
|
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
|
44
|
+
|
45
|
+
def stack
|
46
|
+
Thread.current[@thread_key] ||= []
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -1,30 +1,41 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "zlib"
|
4
4
|
|
5
|
+
# The percent rollout strategy is the most comprehensive included with Gitlab::Experiment. It allows specifying the
|
6
|
+
# percentages per variant using an array, a hash, or will default to even distribution when no rules are provided.
|
7
|
+
#
|
8
|
+
# A given experiment id (context key) will always be given the same variant assignment.
|
9
|
+
#
|
10
|
+
# Example configuration usage:
|
11
|
+
#
|
12
|
+
# config.default_rollout = Gitlab::Experiment::Rollout::Percent.new
|
13
|
+
#
|
14
|
+
# Example class usage:
|
15
|
+
#
|
16
|
+
# class PillColorExperiment < ApplicationExperiment
|
17
|
+
# control { }
|
18
|
+
# variant(:red) { }
|
19
|
+
# variant(:blue) { }
|
20
|
+
#
|
21
|
+
# # Even distribution between all behaviors.
|
22
|
+
# default_rollout :percent
|
23
|
+
#
|
24
|
+
# # With specific distribution percentages.
|
25
|
+
# default_rollout :percent, distribution: { control: 25, red: 30, blue: 45 }
|
26
|
+
# end
|
27
|
+
#
|
5
28
|
module Gitlab
|
6
29
|
class Experiment
|
7
30
|
module Rollout
|
8
31
|
class Percent < Base
|
9
|
-
|
10
|
-
crc = normalized_id
|
11
|
-
total = 0
|
12
|
-
|
13
|
-
case distribution_rules
|
14
|
-
# run through the rules until finding an acceptable one
|
15
|
-
when Array then variant_names[distribution_rules.find_index { |percent| crc % 100 <= total += percent }]
|
16
|
-
# run through the variant names until finding an acceptable one
|
17
|
-
when Hash then distribution_rules.find { |_, percent| crc % 100 <= total += percent }.first
|
18
|
-
# when there are no rules, assume even distribution
|
19
|
-
else variant_names[crc % variant_names.length]
|
20
|
-
end
|
21
|
-
end
|
32
|
+
protected
|
22
33
|
|
23
34
|
def validate!
|
24
35
|
case distribution_rules
|
25
36
|
when nil then nil
|
26
37
|
when Array, Hash
|
27
|
-
if distribution_rules.length !=
|
38
|
+
if distribution_rules.length != behavior_names.length
|
28
39
|
raise InvalidRolloutRules, "the distribution rules don't match the number of variants defined"
|
29
40
|
end
|
30
41
|
else
|
@@ -32,6 +43,20 @@ module Gitlab
|
|
32
43
|
end
|
33
44
|
end
|
34
45
|
|
46
|
+
def execute_assignment
|
47
|
+
crc = normalized_id
|
48
|
+
total = 0
|
49
|
+
|
50
|
+
case distribution_rules
|
51
|
+
when Array # run through the rules until finding an acceptable one
|
52
|
+
behavior_names[distribution_rules.find_index { |percent| crc % 100 <= total += percent }]
|
53
|
+
when Hash # run through the variant names until finding an acceptable one
|
54
|
+
distribution_rules.find { |_, percent| crc % 100 <= total += percent }.first
|
55
|
+
else # assume even distribution on no rules
|
56
|
+
behavior_names.empty? ? nil : behavior_names[crc % behavior_names.length]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
35
60
|
private
|
36
61
|
|
37
62
|
def normalized_id
|
@@ -39,7 +64,7 @@ module Gitlab
|
|
39
64
|
end
|
40
65
|
|
41
66
|
def distribution_rules
|
42
|
-
|
67
|
+
options[:distribution]
|
43
68
|
end
|
44
69
|
end
|
45
70
|
end
|
@@ -1,13 +1,34 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# The random rollout strategy will randomly assign a variant when the context is determined to be within the experiment
|
4
|
+
# group.
|
5
|
+
#
|
6
|
+
# If caching is enabled this is a predicable and consistent assignment that will eventually assign a variant (since
|
7
|
+
# control isn't cached) but if caching isn't enabled, assignment will be random each time.
|
8
|
+
#
|
9
|
+
# Example configuration usage:
|
10
|
+
#
|
11
|
+
# config.default_rollout = Gitlab::Experiment::Rollout::Random.new
|
12
|
+
#
|
13
|
+
# Example class usage:
|
14
|
+
#
|
15
|
+
# class PillColorExperiment < ApplicationExperiment
|
16
|
+
# control { }
|
17
|
+
# variant(:red) { }
|
18
|
+
# variant(:blue) { }
|
19
|
+
#
|
20
|
+
# # Randomize between all behaviors, with a mostly even distribution).
|
21
|
+
# default_rollout :random
|
22
|
+
# end
|
23
|
+
#
|
3
24
|
module Gitlab
|
4
25
|
class Experiment
|
5
26
|
module Rollout
|
6
27
|
class Random < Base
|
7
|
-
|
8
|
-
|
9
|
-
def
|
10
|
-
|
28
|
+
protected
|
29
|
+
|
30
|
+
def execute_assignment
|
31
|
+
behavior_names.sample # pick a random variant
|
11
32
|
end
|
12
33
|
end
|
13
34
|
end
|
@@ -1,21 +1,38 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# The round robin strategy will assign the next variant in the list, looping back to the first variant after all
|
4
|
+
# variants have been assigned. This is useful for very small sample sizes where very even distribution can be required.
|
5
|
+
#
|
6
|
+
# Requires a cache to be configured.
|
7
|
+
#
|
8
|
+
# Keeps track of the number of assignments into the experiment group, and uses this to rotate "round robin" style
|
9
|
+
# through the variants that are defined.
|
10
|
+
#
|
11
|
+
# Example configuration usage:
|
12
|
+
#
|
13
|
+
# config.default_rollout = Gitlab::Experiment::Rollout::RoundRobin.new
|
14
|
+
#
|
15
|
+
# Example class usage:
|
16
|
+
#
|
17
|
+
# class PillColorExperiment < ApplicationExperiment
|
18
|
+
# control { }
|
19
|
+
# variant(:red) { }
|
20
|
+
# variant(:blue) { }
|
21
|
+
#
|
22
|
+
# # Rotate evenly between all behaviors.
|
23
|
+
# default_rollout :round_robin
|
24
|
+
# end
|
25
|
+
#
|
3
26
|
module Gitlab
|
4
27
|
class Experiment
|
5
28
|
module Rollout
|
6
29
|
class RoundRobin < Base
|
7
30
|
KEY_NAME = :last_round_robin_variant
|
8
31
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
# that are defined.
|
14
|
-
#
|
15
|
-
# Relatively performant, but requires a cache, and is dependent on the
|
16
|
-
# performance of that cache store.
|
17
|
-
def execute
|
18
|
-
variant_names[(cache.attr_inc(KEY_NAME) - 1) % variant_names.size]
|
32
|
+
protected
|
33
|
+
|
34
|
+
def execute_assignment
|
35
|
+
behavior_names[(cache.attr_inc(KEY_NAME) - 1) % behavior_names.size]
|
19
36
|
end
|
20
37
|
end
|
21
38
|
end
|