gitlab-experiment 1.4.0 → 1.6.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/README.md +36 -2
- data/lib/generators/gitlab/experiment/install/templates/initializer.rb.tt +14 -0
- data/lib/gitlab/experiment/base_interface.rb +1 -1
- data/lib/gitlab/experiment/cache.rb +4 -4
- data/lib/gitlab/experiment/configuration.rb +31 -2
- data/lib/gitlab/experiment/context.rb +1 -1
- data/lib/gitlab/experiment/cookies.rb +2 -2
- data/lib/gitlab/experiment/errors.rb +1 -0
- data/lib/gitlab/experiment/rollout/random.rb +9 -3
- data/lib/gitlab/experiment/rollout.rb +20 -1
- data/lib/gitlab/experiment/rspec.rb +98 -2
- data/lib/gitlab/experiment/version.rb +1 -1
- data/lib/gitlab/experiment.rb +20 -11
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0e0e710a80403ae390bb66fd552656996ca271891cbc4b2cd758f064538442e5
|
|
4
|
+
data.tar.gz: 564ea70f574ebabef344f4f428cfad52a6213a35522834956db8eb8073a498e9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b76011392395d1d0b0e1d7beeec6dc085ea6d7444c231b9dbbdac7f909248cce4ca16f551bb11215b2611ad504a63a593669b31e544d69d1658da0e0ee6004eb
|
|
7
|
+
data.tar.gz: e9c96959bc9c30b2684bb4eb13665032753f97463f11192b66f2c5592bbe66c29a49663d1fa4a11b2dc64493d10b462dc24b6a6b2380899467df68cdac26ccae
|
data/README.md
CHANGED
|
@@ -680,11 +680,17 @@ end
|
|
|
680
680
|
```
|
|
681
681
|
|
|
682
682
|
**Behavior with `only_assigned: true`:**
|
|
683
|
-
- ✅ If user
|
|
684
|
-
- ✅ If user
|
|
683
|
+
- ✅ If user has a cached assignment (control or candidate) → returns that variant and tracks
|
|
684
|
+
- ✅ If user has no cached assignment → returns control, no tracking
|
|
685
685
|
- ✅ Experiment reach stays controlled
|
|
686
686
|
- ✅ Perfect for multi-page experimental experiences
|
|
687
687
|
|
|
688
|
+
> **Note:** `only_assigned: true` relies on cached assignments. Control variants are cached
|
|
689
|
+
> by default (see [Advanced: Caching Configuration](#advanced-caching-configuration)) so that
|
|
690
|
+
> bucketed control users are included in `only_assigned` tracking. If you set
|
|
691
|
+
> `cache_control: false`, control users will appear unassigned at secondary sites and be
|
|
692
|
+
> excluded from tracking there.
|
|
693
|
+
|
|
688
694
|
**Real-world use cases:**
|
|
689
695
|
- **Post-signup experiences**: Assign at signup, show features throughout the app
|
|
690
696
|
- **Gradual feature expansion**: Roll out to 5%, then add more touchpoints without expanding population
|
|
@@ -1318,6 +1324,34 @@ documented in its implementation.
|
|
|
1318
1324
|
|
|
1319
1325
|
**Important:** Caching changes how rollout strategies behave. Once cached, subsequent calls return the cached value regardless of rollout strategy changes.
|
|
1320
1326
|
|
|
1327
|
+
**Control variant caching:**
|
|
1328
|
+
|
|
1329
|
+
By default, the gem caches `:control` assignments alongside variant assignments. This is what
|
|
1330
|
+
makes `only_assigned: true` lookups include bucketed control users -- without it, control
|
|
1331
|
+
users would appear unassigned at secondary tracking sites and would be silently excluded.
|
|
1332
|
+
|
|
1333
|
+
```ruby
|
|
1334
|
+
# Disable globally (don't cache :control)
|
|
1335
|
+
Gitlab::Experiment.configure do |config|
|
|
1336
|
+
config.cache_control = false
|
|
1337
|
+
end
|
|
1338
|
+
|
|
1339
|
+
# Or override per-rollout
|
|
1340
|
+
class MyExperiment < ApplicationExperiment
|
|
1341
|
+
default_rollout :percent, cache_control: false
|
|
1342
|
+
end
|
|
1343
|
+
```
|
|
1344
|
+
|
|
1345
|
+
The `Random` rollout strategy intentionally opts out of caching `:control` regardless of this
|
|
1346
|
+
setting, because its convergence model depends on re-rolling control assignments on every
|
|
1347
|
+
visit until a non-control variant is picked.
|
|
1348
|
+
|
|
1349
|
+
**Operational note:** With control caching enabled, `Percent` and `RoundRobin` cache hashes
|
|
1350
|
+
grow with every bucketed user, not just users assigned to a candidate variant. For
|
|
1351
|
+
long-running experiments the cache will be roughly `1 / candidate_percentage` the size it
|
|
1352
|
+
would have been before. Plan periodic cleanup using your store's deletion API -- for the
|
|
1353
|
+
`RedisHashStore`, that's `experiment.cache.store.clear(key: experiment.name)`.
|
|
1354
|
+
|
|
1321
1355
|
### Advanced: Custom Rollout Strategies
|
|
1322
1356
|
|
|
1323
1357
|
Build custom integrations with your existing infrastructure:
|
|
@@ -22,6 +22,20 @@ Gitlab::Experiment.configure do |config|
|
|
|
22
22
|
# Use `nil` for no caching.
|
|
23
23
|
config.cache = nil
|
|
24
24
|
|
|
25
|
+
# Cache `:control` assignments alongside variant assignments.
|
|
26
|
+
#
|
|
27
|
+
# When set to true (the default), bucketed control users are cached and
|
|
28
|
+
# therefore visible to `only_assigned: true` tracking calls. Set to false
|
|
29
|
+
# to skip caching control -- control users will be treated as unassigned
|
|
30
|
+
# at `only_assigned` sites and excluded from tracking there.
|
|
31
|
+
#
|
|
32
|
+
# Can be overridden per-rollout:
|
|
33
|
+
#
|
|
34
|
+
# class ExampleExperiment < ApplicationExperiment
|
|
35
|
+
# default_rollout :percent, cache_control: false
|
|
36
|
+
# end
|
|
37
|
+
config.cache_control = true
|
|
38
|
+
|
|
25
39
|
# The domain to use on cookies.
|
|
26
40
|
#
|
|
27
41
|
# When not set, it uses the current host. If you want to provide specific
|
|
@@ -43,18 +43,18 @@ module Gitlab
|
|
|
43
43
|
@cache ||= Interface.new(self, Configuration.cache)
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
def cache_variant(specified = nil, &
|
|
46
|
+
def cache_variant(specified = nil, &)
|
|
47
47
|
return (specified.presence || yield) unless cache.store
|
|
48
48
|
|
|
49
|
-
result = migrated_cache_fetch(cache.store) || find_variant(&
|
|
49
|
+
result = migrated_cache_fetch(cache.store) || find_variant(&)
|
|
50
50
|
return result unless specified.present?
|
|
51
51
|
|
|
52
52
|
cache.write(specified) if result.to_s != specified.to_s
|
|
53
53
|
specified
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
-
def find_variant(&
|
|
57
|
-
cache.store.fetch(cache_key, &
|
|
56
|
+
def find_variant(&)
|
|
57
|
+
cache.store.fetch(cache_key, &)
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
def cache_key(key = nil, suffix: nil)
|
|
@@ -30,6 +30,21 @@ module Gitlab
|
|
|
30
30
|
# Use `nil` for no caching.
|
|
31
31
|
@cache = nil
|
|
32
32
|
|
|
33
|
+
# Whether `:control` assignments should be written to the cache.
|
|
34
|
+
#
|
|
35
|
+
# When true (default), control variants are cached alongside other
|
|
36
|
+
# variants, which is required for `only_assigned: true` tracking to
|
|
37
|
+
# work correctly for the control arm of an experiment.
|
|
38
|
+
#
|
|
39
|
+
# When false, control variants are not cached. This preserves the
|
|
40
|
+
# behavior of versions prior to this option being introduced. Any
|
|
41
|
+
# experiment using `only_assigned: true` will silently exclude
|
|
42
|
+
# control users from tracking.
|
|
43
|
+
#
|
|
44
|
+
# Individual rollouts can override this via the `cache_control:`
|
|
45
|
+
# option, e.g. `default_rollout :percent, cache_control: false`.
|
|
46
|
+
@cache_control = true
|
|
47
|
+
|
|
33
48
|
# The domain to use on cookies.
|
|
34
49
|
#
|
|
35
50
|
# When not set, it uses the current host. If you want to provide specific
|
|
@@ -126,7 +141,7 @@ module Gitlab
|
|
|
126
141
|
# access experiment methods, like `name`, `context`, and `signature`.
|
|
127
142
|
@tracking_behavior = lambda do |event, args|
|
|
128
143
|
# An example of using a generic logger to track events:
|
|
129
|
-
Configuration.logger.info("#{self.class.name}[#{name}] #{event}: #{args.merge(signature:
|
|
144
|
+
Configuration.logger.info("#{self.class.name}[#{name}] #{event}: #{args.merge(signature:)}")
|
|
130
145
|
|
|
131
146
|
# Using something like snowplow to track events (in gitlab):
|
|
132
147
|
#
|
|
@@ -158,7 +173,7 @@ module Gitlab
|
|
|
158
173
|
# level1 initiated by file_name.rb:2
|
|
159
174
|
# level2 initiated by file_name.rb:3
|
|
160
175
|
@nested_behavior = lambda do |nested_experiment|
|
|
161
|
-
raise NestingError.new(experiment: self, nested_experiment:
|
|
176
|
+
raise NestingError.new(experiment: self, nested_experiment:)
|
|
162
177
|
end
|
|
163
178
|
|
|
164
179
|
# Called at the end of every experiment run, with the result.
|
|
@@ -193,6 +208,7 @@ module Gitlab
|
|
|
193
208
|
:base_class,
|
|
194
209
|
:strict_registration,
|
|
195
210
|
:cache,
|
|
211
|
+
:cache_control,
|
|
196
212
|
:cookie_domain,
|
|
197
213
|
:cookie_name,
|
|
198
214
|
:secure_cookie,
|
|
@@ -213,6 +229,19 @@ module Gitlab
|
|
|
213
229
|
@default_rollout = Rollout.resolve(*args)
|
|
214
230
|
end
|
|
215
231
|
|
|
232
|
+
def cache_control=(value) # rubocop:disable Lint/DuplicateMethods
|
|
233
|
+
if value != true
|
|
234
|
+
deprecated(
|
|
235
|
+
:cache_control,
|
|
236
|
+
"setting `cache_control` to a non-true value is deprecated and will be removed in version 2.0. " \
|
|
237
|
+
"Control variants will always be cached.",
|
|
238
|
+
version: '1.4'
|
|
239
|
+
)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
@cache_control = value
|
|
243
|
+
end
|
|
244
|
+
|
|
216
245
|
# Internal warning helpers.
|
|
217
246
|
|
|
218
247
|
def deprecated(*args, version:, stack: 0)
|
|
@@ -22,7 +22,7 @@ module Gitlab
|
|
|
22
22
|
return hash.merge(key => cookie) if hash[key].nil?
|
|
23
23
|
|
|
24
24
|
add_unmerged_migration(key => cookie)
|
|
25
|
-
cookie_jar.delete(cookie_name, domain:
|
|
25
|
+
cookie_jar.delete(cookie_name, domain:)
|
|
26
26
|
|
|
27
27
|
hash
|
|
28
28
|
end
|
|
@@ -32,7 +32,7 @@ module Gitlab
|
|
|
32
32
|
|
|
33
33
|
cookie ||= SecureRandom.uuid
|
|
34
34
|
cookie_jar.permanent.signed[cookie_name] = {
|
|
35
|
-
value: cookie, secure: Configuration.secure_cookie, domain
|
|
35
|
+
value: cookie, secure: Configuration.secure_cookie, domain:, httponly: true
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
hash.merge(key => cookie)
|
|
@@ -7,6 +7,7 @@ module Gitlab
|
|
|
7
7
|
UnregisteredExperiment = Class.new(Error)
|
|
8
8
|
ExistingBehaviorError = Class.new(Error)
|
|
9
9
|
BehaviorMissingError = Class.new(Error)
|
|
10
|
+
ControlNotCachedError = Class.new(Error)
|
|
10
11
|
|
|
11
12
|
class NestingError < Error
|
|
12
13
|
def initialize(experiment:, nested_experiment:)
|
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
# The random rollout strategy will randomly assign a variant when the context is determined to be within the experiment
|
|
4
4
|
# group.
|
|
5
5
|
#
|
|
6
|
-
# If caching is enabled this is a
|
|
7
|
-
#
|
|
6
|
+
# If caching is enabled this is a predictable and consistent assignment that will eventually assign a non-control
|
|
7
|
+
# variant. This relies on the strategy intentionally NOT caching control assignments -- so when a context is sampled
|
|
8
|
+
# into control, it gets re-rolled on the next visit until it lands on a non-control variant, at which point the
|
|
9
|
+
# assignment is cached and stable. Without caching, assignment is random on every call.
|
|
8
10
|
#
|
|
11
|
+
# This is the only rollout that opts out of caching control: every other rollout caches whatever variant it resolves to.
|
|
9
12
|
# Example configuration usage:
|
|
10
13
|
#
|
|
11
14
|
# config.default_rollout = Gitlab::Experiment::Rollout::Random.new
|
|
@@ -28,7 +31,10 @@ module Gitlab
|
|
|
28
31
|
protected
|
|
29
32
|
|
|
30
33
|
def execute_assignment
|
|
31
|
-
behavior_names.sample # pick a random variant
|
|
34
|
+
result = behavior_names.sample # pick a random variant
|
|
35
|
+
# Drop :control so it's never cached, even if cache_control is true.
|
|
36
|
+
# The context will be re-rolled on the next visit and eventually converge on a non-control variant.
|
|
37
|
+
result == :control ? nil : result
|
|
32
38
|
end
|
|
33
39
|
end
|
|
34
40
|
end
|
|
@@ -31,6 +31,15 @@ module Gitlab
|
|
|
31
31
|
|
|
32
32
|
@experiment = experiment
|
|
33
33
|
@options = options
|
|
34
|
+
|
|
35
|
+
return if !options.key?(:cache_control) || options[:cache_control] == true
|
|
36
|
+
|
|
37
|
+
Configuration.deprecated(
|
|
38
|
+
:cache_control,
|
|
39
|
+
"setting `cache_control` to a non-true value on a rollout is deprecated and " \
|
|
40
|
+
"will be removed in version 2.0. Control variants will always be cached.",
|
|
41
|
+
version: '1.4'
|
|
42
|
+
)
|
|
34
43
|
end
|
|
35
44
|
|
|
36
45
|
def enabled?
|
|
@@ -41,7 +50,17 @@ module Gitlab
|
|
|
41
50
|
validate! # allow the rollout strategy to validate itself
|
|
42
51
|
|
|
43
52
|
assignment = execute_assignment
|
|
44
|
-
|
|
53
|
+
# Cache control assignments unless explicitly opted out. The per-rollout `cache_control:`
|
|
54
|
+
# option takes precedence over the global `Configuration.cache_control` default.
|
|
55
|
+
return assignment if cache_control_enabled?
|
|
56
|
+
|
|
57
|
+
assignment == :control ? nil : assignment
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Returns true when this rollout caches `:control` assignments. Per-rollout
|
|
61
|
+
# `cache_control:` option takes precedence over the global default.
|
|
62
|
+
def cache_control_enabled?
|
|
63
|
+
options.fetch(:cache_control, Configuration.cache_control)
|
|
45
64
|
end
|
|
46
65
|
|
|
47
66
|
private
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'active_support/core_ext/enumerable'
|
|
4
|
+
require 'active_support/core_ext/object/blank'
|
|
5
|
+
|
|
3
6
|
module Gitlab
|
|
4
7
|
class Experiment
|
|
5
8
|
module TestBehaviors
|
|
@@ -24,7 +27,7 @@ module Gitlab
|
|
|
24
27
|
end
|
|
25
28
|
|
|
26
29
|
module MethodDouble
|
|
27
|
-
def proxy_method_invoked(receiver, *args, &
|
|
30
|
+
def proxy_method_invoked(receiver, *args, &)
|
|
28
31
|
RSpecMocks.track_gitlab_experiment_receiver(original_method, receiver)
|
|
29
32
|
super
|
|
30
33
|
end
|
|
@@ -121,6 +124,38 @@ module Gitlab
|
|
|
121
124
|
end
|
|
122
125
|
end
|
|
123
126
|
|
|
127
|
+
# Records experiment tracking calls captured via Configuration.tracking_behavior, so the
|
|
128
|
+
# `have_tracked_experiment` matcher can assert on the ordered sequence of emitted events. Only
|
|
129
|
+
# GLEX-native data is captured (the action plus the experiment's signature), so it works for any
|
|
130
|
+
# consumer regardless of how the host application wires up tracking.
|
|
131
|
+
class TrackedExperiments
|
|
132
|
+
attr_reader :events
|
|
133
|
+
|
|
134
|
+
def initialize(max_segmentations = 1)
|
|
135
|
+
@events = []
|
|
136
|
+
@max_segmentations = max_segmentations
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def record(action, signature)
|
|
140
|
+
event = { action: }.merge(signature)
|
|
141
|
+
verify_single_assignment(event)
|
|
142
|
+
@events << event
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
private
|
|
146
|
+
|
|
147
|
+
def verify_single_assignment(event)
|
|
148
|
+
return unless event[:action] == :assignment && event[:key].present?
|
|
149
|
+
|
|
150
|
+
keys = @events.select { |e| e[:action] == :assignment && e[:key].present? }
|
|
151
|
+
.pluck(:key).push(event[:key]).uniq
|
|
152
|
+
return unless keys.size > @max_segmentations
|
|
153
|
+
|
|
154
|
+
raise "#{event[:experiment]} was segmented #{keys.size} " \
|
|
155
|
+
"#{keys.size == 1 ? 'time' : 'times'} (expected #{@max_segmentations}) - #{keys.join(', ')}."
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
124
159
|
module RSpecMatchers
|
|
125
160
|
extend RSpec::Matchers::DSL
|
|
126
161
|
|
|
@@ -135,6 +170,10 @@ module Gitlab
|
|
|
135
170
|
experiment
|
|
136
171
|
end
|
|
137
172
|
|
|
173
|
+
def tracked_experiments
|
|
174
|
+
@_tracked_experiments
|
|
175
|
+
end
|
|
176
|
+
|
|
138
177
|
matcher :register_behavior do |behavior_name|
|
|
139
178
|
match do |experiment|
|
|
140
179
|
@experiment = require_experiment(experiment, 'register_behavior')
|
|
@@ -262,7 +301,7 @@ module Gitlab
|
|
|
262
301
|
chain(:on_next_instance) { @on_next_instance = true }
|
|
263
302
|
|
|
264
303
|
def set_expectations(event, *event_args, negated:)
|
|
265
|
-
failure_message = failure_message_with_details(event, negated:
|
|
304
|
+
failure_message = failure_message_with_details(event, negated:)
|
|
266
305
|
expectations = proc do |e|
|
|
267
306
|
allow(e).to receive(:track).and_call_original
|
|
268
307
|
|
|
@@ -306,6 +345,43 @@ module Gitlab
|
|
|
306
345
|
details.unshift(base).join("\n")
|
|
307
346
|
end
|
|
308
347
|
end
|
|
348
|
+
|
|
349
|
+
matcher :have_tracked_experiment do |experiment_name, expectation = []|
|
|
350
|
+
match do
|
|
351
|
+
@experiment_name = experiment_name
|
|
352
|
+
@expectation = expectation
|
|
353
|
+
|
|
354
|
+
expectation == formatted_events
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
failure_message do
|
|
358
|
+
"Expected to track ordered events:\n#{events_to_s(@expectation)}\n" \
|
|
359
|
+
"but tracked:\n#{events_to_s(formatted_events)}"
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def formatted_events
|
|
363
|
+
unless tracked_experiments
|
|
364
|
+
raise 'have_tracked_experiment requires the :experiment_tracking metadata tag on the example'
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
tracked_experiments.events
|
|
368
|
+
.select { |event| event[:experiment] == @experiment_name.to_s }
|
|
369
|
+
.uniq { |event| event[:action] }
|
|
370
|
+
.map.with_index { |event, index| format_event(event, @expectation[index] || {}) }
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# A bare expectation entry (eg. `:assignment`) asserts only the action; a Hash entry (eg.
|
|
374
|
+
# `{ action: :assignment, migration_keys: [...] }`) asserts each named signature field.
|
|
375
|
+
def format_event(event, expected_event)
|
|
376
|
+
return event[:action] unless expected_event.is_a?(Hash)
|
|
377
|
+
|
|
378
|
+
expected_event.keys.index_with { |key| event[key] }
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def events_to_s(events)
|
|
382
|
+
events.map { |event| " #{event}" }.join(",\n")
|
|
383
|
+
end
|
|
384
|
+
end
|
|
309
385
|
end
|
|
310
386
|
end
|
|
311
387
|
end
|
|
@@ -326,6 +402,26 @@ RSpec.configure do |config|
|
|
|
326
402
|
|
|
327
403
|
config.include Gitlab::Experiment::RSpecMatchers, :experiment
|
|
328
404
|
config.include Gitlab::Experiment::RSpecMatchers, type: :experiment
|
|
405
|
+
config.include Gitlab::Experiment::RSpecMatchers, :experiment_tracking
|
|
406
|
+
|
|
407
|
+
# Capture experiment tracking calls at the gem's own emission seam (Configuration.tracking_behavior),
|
|
408
|
+
# so `have_tracked_experiment` can assert on them without coupling to any host tracking implementation.
|
|
409
|
+
# The metadata value, when numeric, sets the maximum number of segmentations allowed (defaults to 1).
|
|
410
|
+
config.before(:each, :experiment_tracking) do |example|
|
|
411
|
+
max_segmentations = example.metadata[:experiment_tracking]
|
|
412
|
+
max_segmentations = 1 unless max_segmentations.is_a?(Numeric)
|
|
413
|
+
recorder = @_tracked_experiments = Gitlab::Experiment::TrackedExperiments.new(max_segmentations)
|
|
414
|
+
|
|
415
|
+
@_original_tracking_behavior = Gitlab::Experiment::Configuration.tracking_behavior
|
|
416
|
+
Gitlab::Experiment::Configuration.tracking_behavior = lambda do |action, _args|
|
|
417
|
+
# `instance_exec`'d on the experiment by `Experiment#track`, so `signature` is in scope here.
|
|
418
|
+
recorder.record(action, signature)
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
config.after(:each, :experiment_tracking) do
|
|
423
|
+
Gitlab::Experiment::Configuration.tracking_behavior = @_original_tracking_behavior
|
|
424
|
+
end
|
|
329
425
|
|
|
330
426
|
config.define_derived_metadata(file_path: Regexp.new('spec/experiments/')) do |metadata|
|
|
331
427
|
metadata[:type] ||= :experiment
|
data/lib/gitlab/experiment.rb
CHANGED
|
@@ -33,16 +33,16 @@ module Gitlab
|
|
|
33
33
|
class << self
|
|
34
34
|
# Class level behavior registration methods.
|
|
35
35
|
|
|
36
|
-
def control(*filter_list, **options, &
|
|
37
|
-
variant(:control, *filter_list, **options, &
|
|
36
|
+
def control(*filter_list, **options, &)
|
|
37
|
+
variant(:control, *filter_list, **options, &)
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
def candidate(*filter_list, **options, &
|
|
41
|
-
variant(:candidate, *filter_list, **options, &
|
|
40
|
+
def candidate(*filter_list, **options, &)
|
|
41
|
+
variant(:candidate, *filter_list, **options, &)
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
-
def variant(variant, *filter_list, **options, &
|
|
45
|
-
build_behavior_callback(filter_list, variant, **options, &
|
|
44
|
+
def variant(variant, *filter_list, **options, &)
|
|
45
|
+
build_behavior_callback(filter_list, variant, **options, &)
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
# Class level callback registration methods.
|
|
@@ -86,12 +86,12 @@ module Gitlab
|
|
|
86
86
|
[Configuration.name_prefix, @_name].compact.join('_')
|
|
87
87
|
end
|
|
88
88
|
|
|
89
|
-
def control(&
|
|
90
|
-
variant(:control, &
|
|
89
|
+
def control(&)
|
|
90
|
+
variant(:control, &)
|
|
91
91
|
end
|
|
92
92
|
|
|
93
|
-
def candidate(&
|
|
94
|
-
variant(:candidate, &
|
|
93
|
+
def candidate(&)
|
|
94
|
+
variant(:candidate, &)
|
|
95
95
|
end
|
|
96
96
|
|
|
97
97
|
def variant(name, &block)
|
|
@@ -166,7 +166,16 @@ module Gitlab
|
|
|
166
166
|
end
|
|
167
167
|
|
|
168
168
|
def only_assigned?
|
|
169
|
-
|
|
169
|
+
return false unless context.only_assigned
|
|
170
|
+
|
|
171
|
+
unless rollout.cache_control_enabled?
|
|
172
|
+
raise ControlNotCachedError,
|
|
173
|
+
"`only_assigned: true` requires `cache_control: true`, but the rollout for " \
|
|
174
|
+
"experiment `#{name}` has it disabled. Control users would be silently excluded " \
|
|
175
|
+
"from tracking. Enable cache_control or remove `only_assigned: true`."
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
find_variant.blank?
|
|
170
179
|
end
|
|
171
180
|
|
|
172
181
|
def should_track?
|
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: 1.
|
|
4
|
+
version: 1.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- GitLab
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-06-23 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -246,7 +246,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
246
246
|
requirements:
|
|
247
247
|
- - ">="
|
|
248
248
|
- !ruby/object:Gem::Version
|
|
249
|
-
version: '3.
|
|
249
|
+
version: '3.2'
|
|
250
250
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
251
251
|
requirements:
|
|
252
252
|
- - ">="
|