gitlab-experiment 1.5.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/lib/gitlab/experiment/base_interface.rb +1 -1
- data/lib/gitlab/experiment/cache.rb +4 -4
- data/lib/gitlab/experiment/configuration.rb +2 -2
- data/lib/gitlab/experiment/context.rb +1 -1
- data/lib/gitlab/experiment/cookies.rb +2 -2
- data/lib/gitlab/experiment/rspec.rb +98 -2
- data/lib/gitlab/experiment/version.rb +1 -1
- data/lib/gitlab/experiment.rb +10 -10
- 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
|
|
@@ -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)
|
|
@@ -141,7 +141,7 @@ module Gitlab
|
|
|
141
141
|
# access experiment methods, like `name`, `context`, and `signature`.
|
|
142
142
|
@tracking_behavior = lambda do |event, args|
|
|
143
143
|
# An example of using a generic logger to track events:
|
|
144
|
-
Configuration.logger.info("#{self.class.name}[#{name}] #{event}: #{args.merge(signature:
|
|
144
|
+
Configuration.logger.info("#{self.class.name}[#{name}] #{event}: #{args.merge(signature:)}")
|
|
145
145
|
|
|
146
146
|
# Using something like snowplow to track events (in gitlab):
|
|
147
147
|
#
|
|
@@ -173,7 +173,7 @@ module Gitlab
|
|
|
173
173
|
# level1 initiated by file_name.rb:2
|
|
174
174
|
# level2 initiated by file_name.rb:3
|
|
175
175
|
@nested_behavior = lambda do |nested_experiment|
|
|
176
|
-
raise NestingError.new(experiment: self, nested_experiment:
|
|
176
|
+
raise NestingError.new(experiment: self, nested_experiment:)
|
|
177
177
|
end
|
|
178
178
|
|
|
179
179
|
# Called at the end of every experiment run, with the result.
|
|
@@ -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)
|
|
@@ -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)
|
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-06-
|
|
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
|
- - ">="
|