gitlab-experiment 1.1.0 → 1.3.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.
@@ -44,6 +44,15 @@ module Gitlab
44
44
  "#{experiment.name}_id"
45
45
  end
46
46
 
47
+ # Allow forced variant assignment via query parameter and cookie.
48
+ #
49
+ # When enabled, experiments will check for a `glex_force` query
50
+ # parameter and a `_glex_force` cookie to override variant assignment.
51
+ # This is intended for QA/UAT testing in staging and production.
52
+ #
53
+ # Defaults to false (disabled).
54
+ @allow_forced_assignment = false
55
+
47
56
  # Mark experiment cookies as secure (HTTPS only).
48
57
  #
49
58
  # When set to true, cookies will have the secure flag set, meaning they
@@ -187,6 +196,7 @@ module Gitlab
187
196
  :cookie_domain,
188
197
  :cookie_name,
189
198
  :secure_cookie,
199
+ :allow_forced_assignment,
190
200
  :context_key_secret,
191
201
  :context_key_bit_length,
192
202
  :mount_at,
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gitlab
4
+ class Experiment
5
+ module ForceAssignment
6
+ PARAM_NAME = 'glex_force'
7
+
8
+ private
9
+
10
+ def forced_variant_name
11
+ return unless Configuration.allow_forced_assignment
12
+ return unless enabled?
13
+
14
+ param = context&.request&.params.try(:[], PARAM_NAME)
15
+ return if param.blank?
16
+
17
+ experiment_name, variant_name = param.split(':', 2)
18
+ return if experiment_name.blank? || variant_name.blank?
19
+ return unless experiment_name == name
20
+
21
+ variant_sym = variant_name.to_sym
22
+ return unless behaviors.key?(variant_sym)
23
+
24
+ variant_sym
25
+ end
26
+ end
27
+ end
28
+ end
@@ -6,7 +6,7 @@ module Gitlab
6
6
  autoload :Trackable, 'gitlab/experiment/test_behaviors/trackable.rb'
7
7
  end
8
8
 
9
- WrappedExperiment = Struct.new(:klass, :experiment_name, :variant_name, :expectation_chain, :blocks)
9
+ WrappedExperiment = Struct.new(:klass, :experiment_name, :variant_name, :expectation_chain, :blocks, :assigned)
10
10
 
11
11
  module RSpecMocks
12
12
  @__gitlab_experiment_receivers = {}
@@ -44,6 +44,12 @@ module Gitlab
44
44
  # Call the original method if we specified simply `true`.
45
45
  wrapped.variant_name == true ? method.call : wrapped.variant_name
46
46
  }
47
+
48
+ # Stub find_variant only if caching is not enabled
49
+ unless Configuration.cache
50
+ variant_return_value = wrapped.assigned ? wrapped.variant_name.to_s : nil
51
+ allow(instance).to receive(:find_variant).and_return(variant_return_value)
52
+ end
47
53
  end
48
54
  end
49
55
 
@@ -51,11 +57,11 @@ module Gitlab
51
57
  end
52
58
 
53
59
  def wrapped_experiment(experiment, remock: false, &block)
54
- klass, experiment_name, variant_name = *extract_experiment_details(experiment)
60
+ klass, experiment_name, variant_name, assigned = *extract_experiment_details(experiment)
55
61
 
56
62
  wrapped_experiment = wrapped_experiments[experiment_name] =
57
63
  (!remock && wrapped_experiments[experiment_name]) ||
58
- WrappedExperiment.new(klass, experiment_name, variant_name, wrapped_experiment_chain_for(klass), [])
64
+ WrappedExperiment.new(klass, experiment_name, variant_name, wrapped_experiment_chain_for(klass), [], assigned)
59
65
 
60
66
  wrapped_experiment.blocks << block if block
61
67
  wrapped_experiment
@@ -84,9 +90,16 @@ module Gitlab
84
90
  def extract_experiment_details(experiment)
85
91
  experiment_name = nil
86
92
  variant_name = nil
87
-
88
- experiment_name = experiment if experiment.is_a?(Symbol)
89
- experiment_name, variant_name = *experiment if experiment.is_a?(Array)
93
+ assigned = nil
94
+
95
+ if experiment.is_a?(Array)
96
+ # From normalize_experiments: [experiment_name, variant_name_or_config]
97
+ experiment_name, variant_name = *experiment
98
+ assigned = variant_name.is_a?(Hash) ? variant_name.delete(:assigned) : nil
99
+ variant_name = variant_name[:variant] if variant_name.is_a?(Hash)
100
+ elsif experiment.is_a?(Symbol)
101
+ experiment_name = experiment
102
+ end
90
103
 
91
104
  base_klass = Configuration.base_class.constantize
92
105
  variant_name = experiment.assigned.name if experiment.is_a?(base_klass)
@@ -94,7 +107,7 @@ module Gitlab
94
107
  resolved_klass = experiment_klass(experiment) { base_klass.constantize(experiment_name) }
95
108
  experiment_name ||= experiment.instance_variable_get(:@_name)
96
109
 
97
- [resolved_klass, experiment_name.to_s, variant_name]
110
+ [resolved_klass, experiment_name.to_s, variant_name, assigned]
98
111
  end
99
112
 
100
113
  def experiment_klass(experiment, &block)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Gitlab
4
4
  class Experiment
5
- VERSION = '1.1.0'
5
+ VERSION = '1.3.0'
6
6
  end
7
7
  end
@@ -13,6 +13,7 @@ require 'gitlab/experiment/callbacks'
13
13
  require 'gitlab/experiment/rollout'
14
14
  require 'gitlab/experiment/configuration'
15
15
  require 'gitlab/experiment/cookies'
16
+ require 'gitlab/experiment/force_assignment'
16
17
  require 'gitlab/experiment/context'
17
18
  require 'gitlab/experiment/dsl'
18
19
  require 'gitlab/experiment/middleware'
@@ -26,6 +27,7 @@ module Gitlab
26
27
  include BaseInterface
27
28
  include Cache
28
29
  include Callbacks
30
+ include ForceAssignment
29
31
  include Nestable
30
32
 
31
33
  class << self
@@ -107,6 +109,8 @@ module Gitlab
107
109
  end
108
110
 
109
111
  def assigned(value = nil)
112
+ # Skip force assignment if a variant was already set (e.g., via constructor or explicit #assigned call).
113
+ value ||= forced_variant_name unless @_assigned_variant_name
110
114
  @_assigned_variant_name = cache_variant(value) if value.present?
111
115
  return Variant.new(name: @_assigned_variant_name || :unresolved) if @_assigned_variant_name || @_resolving_variant
112
116
 
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.1.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitLab
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-11-27 00:00:00.000000000 Z
11
+ date: 2026-02-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -223,6 +223,7 @@ files:
223
223
  - lib/gitlab/experiment/dsl.rb
224
224
  - lib/gitlab/experiment/engine.rb
225
225
  - lib/gitlab/experiment/errors.rb
226
+ - lib/gitlab/experiment/force_assignment.rb
226
227
  - lib/gitlab/experiment/middleware.rb
227
228
  - lib/gitlab/experiment/nestable.rb
228
229
  - lib/gitlab/experiment/rollout.rb