gitlab-experiment 0.6.4 → 0.7.1
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/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
|