gitlab-experiment 1.3.0 → 1.5.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/configuration.rb +29 -0
- data/lib/gitlab/experiment/dsl.rb +20 -1
- 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/version.rb +1 -1
- data/lib/gitlab/experiment.rb +10 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 453f39a65f699c77881655a8dbfd40d15133baa47e063b76278e2953c4b1de7c
|
|
4
|
+
data.tar.gz: 3cd7e33850a1d0725bd7f4c82bbcd9d8769c01c1e6663226dcc07acdf68d24ec
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9ba7cc88703891e0ff10a885b55e0f59911459ffa6de2a7102dececf803cf27ea0d0e62912409b543a537c6df1266e403c18d4aa96d6767cfa1f7ae64d638dd3
|
|
7
|
+
data.tar.gz: 5f292e0a952811292354e1d9e84705b88eb225b79819bdb00b3fcf55f7a06754ff87a8502ecd8b5016278629a2c6d83609566dad373bdb2f11d44eca4ed7cdd3
|
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
|
|
@@ -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
|
|
@@ -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)
|
|
@@ -7,10 +7,19 @@ module Gitlab
|
|
|
7
7
|
klass.include(self).tap { |base| base.helper_method(:experiment) if with_helper }
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
+
# An explicit `request:` keyword argument always takes precedence; when
|
|
11
|
+
# present, no auto-detection runs.
|
|
12
|
+
#
|
|
13
|
+
# Otherwise, `request` is auto-detected through implicit_experiment_request in this order:
|
|
14
|
+
# 1. A `request` method on `self` (the controller pattern).
|
|
15
|
+
# 2. A `@request` instance variable on `self` (the service pattern).
|
|
16
|
+
#
|
|
17
|
+
# Callers with `request` as a pure local variable should pass
|
|
18
|
+
# `request: request` explicitly.
|
|
10
19
|
def experiment(name, variant_name = nil, **context, &block)
|
|
11
20
|
raise ArgumentError, 'name is required' if name.nil?
|
|
12
21
|
|
|
13
|
-
context[:request] ||=
|
|
22
|
+
context[:request] ||= implicit_experiment_request
|
|
14
23
|
|
|
15
24
|
base = Configuration.base_class.constantize
|
|
16
25
|
klass = base.constantize(name) || base
|
|
@@ -20,6 +29,16 @@ module Gitlab
|
|
|
20
29
|
|
|
21
30
|
instance.context.frozen? ? instance.run : instance.tap(&:run)
|
|
22
31
|
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def implicit_experiment_request
|
|
36
|
+
if respond_to?(:request)
|
|
37
|
+
request
|
|
38
|
+
elsif instance_variable_defined?(:@request)
|
|
39
|
+
instance_variable_get(:@request)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
23
42
|
end
|
|
24
43
|
end
|
|
25
44
|
end
|
|
@@ -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
|
data/lib/gitlab/experiment.rb
CHANGED
|
@@ -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.5.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-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|