gitlab-experiment 0.8.0 → 1.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c92b7e2d77e534a23b8233f11539fa9c2767b21adab9c247e916933724dd7f5d
4
- data.tar.gz: 7a1144676a16835f64eaefb9ea7bac2b7055c5cf8e329d55dba6c115be17eb50
3
+ metadata.gz: 45c95c2d6d85df98de92096ab00a7cb124ef3cf46f2dfa858465b3c43df8b22d
4
+ data.tar.gz: 17716054da0e7aff4c67ddea28c733ec275eaf95934d627bbea40856427ecb68
5
5
  SHA512:
6
- metadata.gz: b7a645b7d2c3cf13d7031fc61dfdaef2626d0092661093a19ff5e21d346adda8cbe031b5c360de21fda3ecee828eaa7bda0a6afe839b6c5ce7ba57f9e10232eb
7
- data.tar.gz: 1a8e4672a903746f5f31f2cf5dcf815bb6e2c3cb8c8fa73430c93462977310fcd6ed2f6f8676d5e2ccf856289c6befde6469e9f29715341c4bde7fd0b14fa51b
6
+ metadata.gz: a3549e9ec7f899a6994937545c1afcfaceac52efa373b3e7e594b4cd1a08e7df376d1bf79403704d681ea48a29362358ff36fd5cb0fdff6fca29c79c097c0fbb
7
+ data.tar.gz: 7da4141f53babfa226082190ef4c144900df52a8c1c35d03b1270dd4f3f8c2c1be16aad59a9d2da1e833d0a063177b4212744aad9e6a273637c265ea2148d317
data/README.md CHANGED
@@ -258,6 +258,73 @@ class PillColorExperiment < Gitlab::Experiment # OR ApplicationExperiment
258
258
  end
259
259
  ```
260
260
 
261
+ ### Checking assignment without assigning
262
+
263
+ Sometimes you may want to check if a user is already assigned to an experiment without assigning them to a variant if
264
+ they're not. This is useful when you want to show experiment-specific content only to users who are already
265
+ participating in the experiment, without expanding the experiment's reach.
266
+
267
+ You can use the `only_assigned` option to achieve this behavior:
268
+
269
+ ```haml
270
+ -# Only show experimental features to users already participating in the experiment
271
+ - experiment(:advanced_search, actor: current_user, only_assigned: true) do |e|
272
+ - e.control do
273
+ = render 'search_filters'
274
+ - e.candidate do
275
+ = render 'advanced_search_filters'
276
+ ```
277
+
278
+ When `only_assigned: true` is used:
279
+
280
+ - If the user has a cached variant assignment, the experiment runs normally and returns that variant
281
+ - If the user has no cached variant assignment, the experiment is excluded and returns the control behavior
282
+ - No new variant assignments are made
283
+ - Tracking is disabled for excluded cases
284
+ - Publishing still works to record the exclusion
285
+
286
+ This is particularly useful for:
287
+
288
+ - **Progressive rollouts**: Show experimental features only to users already in the experiment
289
+ - **Conditional UI**: Display experiment-specific UI elements only for assigned users
290
+ - **Feature gates**: Check experiment participation without expanding the participant pool
291
+ - **Cleanup phases**: Maintain experience for existing participants while preventing new assignments
292
+ - **Post-registration experiences**: When users are assigned to experiments during registration, you can later show
293
+ experimental features throughout their journey without expanding to existing users who weren't initially assigned
294
+
295
+ ```ruby
296
+ # During user registration - assign new users to experiment variants
297
+ class RegistrationsController < ApplicationController
298
+ def create
299
+ # ... user creation logic
300
+
301
+ # Assign new users to the experiment
302
+ experiment(:pill_color, actor: @user)
303
+ end
304
+ end
305
+ ```
306
+
307
+ ```haml
308
+ -# Later throughout the app - only show experimental features to assigned users
309
+ - experiment(:pill_color, actor: current_user, only_assigned: true) do |e|
310
+ - e.control do
311
+ - e.candidate do
312
+ = render 'quick_start_guide'
313
+ ```
314
+
315
+ You can also assign the result of the experiment to a variable:
316
+
317
+ ```ruby
318
+ # In a view helper or directly in the view
319
+ button_class = experiment(:pill_color, actor: current_user, only_assigned: true) do |e|
320
+ e.control { 'btn-default' }
321
+ e.candidate { 'btn-primary' }
322
+ end.run
323
+ ```
324
+
325
+ Note: The `only_assigned` option requires caching to be enabled in your experiment configuration, as it relies on
326
+ checking for cached variant assignments.
327
+
261
328
  ### Rollout strategies
262
329
 
263
330
  While a default rollout strategy can be defined in configuration, it's useful to be able to override this per experiment if needed. You can do this by specifying a specific `default_rollout` override in your experiment class.
@@ -274,7 +341,12 @@ Obviously random assignment might not be the best rollout strategy for you, but
274
341
 
275
342
  ## How it works
276
343
 
277
- The way experiments work is best described using the following decision tree diagram. When an experiment is run, the following logic is executed to resolve what experience should be provided, given how the experiment is defined, and using the context passed to the experiment call.
344
+ The way experiments work is best described using the following decision tree diagram. When an experiment is run, the
345
+ following logic is executed to resolve what experience should be provided, given how the experiment is defined, and
346
+ using the context passed to the experiment call.
347
+
348
+ Note: When using the `only_assigned` option, experiments that have no cached variant will be excluded, preventing new
349
+ variant assignments while maintaining existing ones.
278
350
 
279
351
  ```mermaid
280
352
  graph TD
@@ -292,11 +364,6 @@ graph TD
292
364
  Rollout -->|Cached| VariantB
293
365
  Rollout -->|Cached| VariantN
294
366
 
295
- classDef included fill:#380d75,color:#ffffff,stroke:none
296
- classDef excluded fill:#fca121,stroke:none
297
- classDef cached fill:#2e2e2e,color:#ffffff,stroke:none
298
- classDef default fill:#fff,stroke:#6e49cb
299
-
300
367
  class VariantA,VariantB,VariantN included
301
368
  class Control,Excluded excluded
302
369
  class Cached cached
@@ -499,9 +566,7 @@ Anyway, now you can use your custom `Flipper` rollout strategy by instantiating
499
566
 
500
567
  ```ruby
501
568
  Gitlab::Experiment.configure do |config|
502
- config.default_rollout = Gitlab::Experiment::Rollout::Flipper.new(
503
- include_control: true # specify to include control, which we want to do
504
- )
569
+ config.default_rollout = Gitlab::Experiment::Rollout::Flipper.new
505
570
  end
506
571
  ```
507
572
 
@@ -512,12 +577,11 @@ class PillColorExperiment < Gitlab::Experiment # OR ApplicationExperiment
512
577
  # ...registered behaviors
513
578
 
514
579
  default_rollout :flipper,
515
- include_control: true, # optionally specify to include control
516
580
  distribution: { control: 26, red: 37, blue: 37 } # optionally specify distribution
517
581
  end
518
582
  ```
519
583
 
520
- Now, enabling or disabling the Flipper feature flag will control if the experiment is enabled or not. If the experiment is enabled, as determined by our custom rollout strategy, the standard resolutuon logic will be executed, and a variant (or control) will be assigned.
584
+ Now, enabling or disabling the Flipper feature flag will control if the experiment is enabled or not. If the experiment is enabled, as determined by our custom rollout strategy, the standard resolution logic will be executed, and a variant (or control) will be assigned.
521
585
 
522
586
  ```ruby
523
587
  experiment(:pill_color).enabled? # => false
@@ -604,6 +668,30 @@ it "stubs experiments while allowing the rollout strategy to assign the variant"
604
668
  end
605
669
  ```
606
670
 
671
+ You can also test the `only_assigned` behavior by stubbing cache states:
672
+
673
+ ```ruby
674
+ it "tests only_assigned behavior with cached variants" do
675
+ # Stub a cached variant to exist
676
+ allow_any_instance_of(Gitlab::Experiment).to receive(:find_variant).and_return('red')
677
+
678
+ experiment_instance = experiment(:pill_color, actor: user, only_assigned: true)
679
+
680
+ expect(experiment_instance).not_to be_excluded
681
+ expect(experiment_instance.run).to eq('red')
682
+ end
683
+
684
+ it "tests only_assigned behavior without cached variants" do
685
+ # Stub no cached variant
686
+ allow_any_instance_of(Gitlab::Experiment).to receive(:find_variant).and_return(nil)
687
+
688
+ experiment_instance = experiment(:pill_color, actor: user, only_assigned: true)
689
+
690
+ expect(experiment_instance).to be_excluded
691
+ expect(experiment_instance.run).to eq('blue') # control behavior
692
+ end
693
+ ```
694
+
607
695
  ### Registered behaviors matcher
608
696
 
609
697
  It's useful to test our registered behaviors, as well as their return values when we implement anything complex in them. The `register_behavior` matcher is useful for this.
@@ -619,7 +707,7 @@ end
619
707
 
620
708
  ### Exclusion and segmentation matchers
621
709
 
622
- You can also easily test your experiment classes using the `exclude`, `segment` metchers.
710
+ You can also easily test your experiment classes using the `exclude`, `segment` matchers.
623
711
 
624
712
  ```ruby
625
713
  let(:excluded) { double(first_name: 'Richard', created_at: Time.current) }
@@ -688,6 +776,8 @@ Run `bundle exec rake` to run the tests. You can also run `bundle exec pry` for
688
776
 
689
777
  Bug reports and merge requests are welcome on GitLab at https://gitlab.com/gitlab-org/ruby/gems/gitlab-experiment. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
690
778
 
779
+ Make sure to include a changelog entry in your commit message and read the [changelog entries section](https://docs.gitlab.com/ee/development/changelog.html).
780
+
691
781
  ## Release process
692
782
 
693
783
  Please refer to the [Release Process](docs/release_process.md).
@@ -8,15 +8,9 @@ module Gitlab
8
8
  source_root File.expand_path('templates/', __dir__)
9
9
  check_class_collision suffix: 'Experiment'
10
10
 
11
- argument :variants,
12
- type: :array,
13
- default: %w[control candidate],
14
- banner: 'variant variant'
15
-
16
- class_option :skip_comments,
17
- type: :boolean,
18
- default: false,
19
- desc: 'Omit helpful comments from generated files'
11
+ argument :variants, type: :array, default: %w[control candidate], banner: 'variant variant'
12
+
13
+ class_option :skip_comments, type: :boolean, default: false, desc: 'Omit helpful comments from generated files'
20
14
 
21
15
  def create_experiment
22
16
  template 'experiment.rb', File.join('app/experiments', class_path, "#{file_name}_experiment.rb")
@@ -11,14 +11,9 @@ module Gitlab
11
11
  desc 'Installs the Gitlab::Experiment initializer and optional ApplicationExperiment into your application.'
12
12
 
13
13
  class_option :skip_initializer,
14
- type: :boolean,
15
- default: false,
16
- desc: 'Skip the initializer with default configuration'
17
-
18
- class_option :skip_baseclass,
19
- type: :boolean,
20
- default: false,
21
- desc: 'Skip the ApplicationExperiment base class'
14
+ type: :boolean, default: false, desc: 'Skip the initializer with default configuration'
15
+
16
+ class_option :skip_baseclass, type: :boolean, default: false, desc: 'Skip the ApplicationExperiment base class'
22
17
 
23
18
  def create_initializer
24
19
  return if options[:skip_initializer]
@@ -43,15 +43,12 @@ Gitlab::Experiment.configure do |config|
43
43
  # Each experiment can specify its own rollout strategy:
44
44
  #
45
45
  # class ExampleExperiment < ApplicationExperiment
46
- # default_rollout :random, # :percent, :round_robin,
47
- # include_control: true # or MyCustomRollout
46
+ # default_rollout :random # :percent, :round_robin, or MyCustomRollout
48
47
  # end
49
48
  #
50
49
  # Included rollout strategies:
51
50
  # :percent (recommended), :round_robin, or :random
52
- config.default_rollout = :percent, {
53
- include_control: true # include control in possible assignments
54
- }
51
+ config.default_rollout = :percent
55
52
 
56
53
  # Secret seed used in generating context keys.
57
54
  #
@@ -48,18 +48,18 @@ module Gitlab
48
48
  key.to_s.split(':') # this assumes the default strategy in gitlab-experiment
49
49
  end
50
50
 
51
- def read_entry(key, **options)
51
+ def read_entry(key, **_options)
52
52
  value = pool { |redis| redis.hget(*hkey(key)) }
53
53
  value.nil? ? nil : ActiveSupport::Cache::Entry.new(value)
54
54
  end
55
55
 
56
- def write_entry(key, entry, **options)
56
+ def write_entry(key, entry, **_options)
57
57
  return false if entry.value.blank? # don't cache any empty values
58
58
 
59
59
  pool { |redis| redis.hset(*hkey(key), entry.value) }
60
60
  end
61
61
 
62
- def delete_entry(key, **options)
62
+ def delete_entry(key, **_options)
63
63
  pool { |redis| redis.hdel(*hkey(key)) }
64
64
  end
65
65
  end
@@ -46,28 +46,34 @@ module Gitlab
46
46
  def cache_variant(specified = nil, &block)
47
47
  return (specified.presence || yield) unless cache.store
48
48
 
49
- result = migrated_cache_fetch(cache.store, &block)
49
+ result = migrated_cache_fetch(cache.store) || find_variant(&block)
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(&block)
57
+ cache.store.fetch(cache_key, &block)
58
+ end
59
+
56
60
  def cache_key(key = nil, suffix: nil)
57
61
  "#{[name, suffix].compact.join('_')}:#{key || context.signature[:key]}"
58
62
  end
59
63
 
60
64
  private
61
65
 
62
- def migrated_cache_fetch(store, &block)
66
+ def migrated_cache_fetch(store)
63
67
  migrations = context.signature[:migration_keys]&.map { |key| cache_key(key) } || []
64
68
  migrations.find do |old_key|
65
- next unless (value = store.read(old_key))
69
+ value = store.read(old_key)
70
+
71
+ next unless value
66
72
 
67
73
  store.write(cache_key, value)
68
74
  store.delete(old_key)
69
75
  break value
70
- end || store.fetch(cache_key, &block)
76
+ end
71
77
  end
72
78
  end
73
79
  end
@@ -39,6 +39,11 @@ module Gitlab
39
39
  # nil, :all, or ['www.gitlab.com', '.gitlab.com']
40
40
  @cookie_domain = :all
41
41
 
42
+ # The cookie name for an experiment.
43
+ @cookie_name = lambda do |experiment|
44
+ "#{experiment.name}_id"
45
+ end
46
+
42
47
  # The default rollout strategy.
43
48
  #
44
49
  # The recommended default rollout strategy when not using caching would
@@ -51,8 +56,7 @@ module Gitlab
51
56
  # Each experiment can specify its own rollout strategy:
52
57
  #
53
58
  # class ExampleExperiment < ApplicationExperiment
54
- # default_rollout :random, # :percent, :round_robin,
55
- # include_control: true # or MyCustomRollout
59
+ # default_rollout :random # :percent, :round_robin, or MyCustomRollout
56
60
  # end
57
61
  #
58
62
  # Included rollout strategies:
@@ -172,6 +176,7 @@ module Gitlab
172
176
  :strict_registration,
173
177
  :cache,
174
178
  :cookie_domain,
179
+ :cookie_name,
175
180
  :context_key_secret,
176
181
  :context_key_bit_length,
177
182
  :mount_at,
@@ -5,7 +5,9 @@ module Gitlab
5
5
  class Context
6
6
  include Cookies
7
7
 
8
- DNT_REGEXP = /^(true|t|yes|y|1|on)$/i.freeze
8
+ DNT_REGEXP = /^(true|t|yes|y|1|on)$/i
9
+
10
+ attr_reader :request, :only_assigned
9
11
 
10
12
  def initialize(experiment, **initial_value)
11
13
  @experiment = experiment
@@ -24,6 +26,7 @@ module Gitlab
24
26
  return @value if value.nil?
25
27
 
26
28
  value = value.dup # dup so we don't mutate
29
+ @only_assigned = value.delete(:only_assigned)
27
30
  reinitialize(value.delete(:request))
28
31
  key(value.delete(:sticky_to))
29
32
 
@@ -63,7 +66,7 @@ module Gitlab
63
66
  add_unmerged_migration(value.delete(:migrated_from))
64
67
  add_merged_migration(value.delete(:migrated_with))
65
68
 
66
- migrate_cookie(value, "#{@experiment.name}_id")
69
+ migrate_cookie(value, @experiment.instance_exec(@experiment, &Configuration.cookie_name))
67
70
  end
68
71
 
69
72
  def add_unmerged_migration(value = {})
@@ -34,10 +34,10 @@ module Gitlab
34
34
  def validate!
35
35
  case distribution_rules
36
36
  when nil then nil
37
- when Array, Hash
38
- if distribution_rules.length != behavior_names.length
39
- raise InvalidRolloutRules, "the distribution rules don't match the number of behaviors defined"
40
- end
37
+ when Array
38
+ validate_distribution_rules(distribution_rules)
39
+ when Hash
40
+ validate_distribution_rules(distribution_rules.values)
41
41
  else
42
42
  raise InvalidRolloutRules, 'unknown distribution options type'
43
43
  end
@@ -49,9 +49,10 @@ module Gitlab
49
49
 
50
50
  case distribution_rules
51
51
  when Array # run through the rules until finding an acceptable one
52
- behavior_names[distribution_rules.find_index { |percent| crc % 100 <= total += percent }]
52
+ index = distribution_rules.find_index { |percent| crc % 100 < total += percent unless percent == 0 }
53
+ behavior_names[index]
53
54
  when Hash # run through the variant names until finding an acceptable one
54
- distribution_rules.find { |_, percent| crc % 100 <= total += percent }.first
55
+ distribution_rules.find { |_, percent| crc % 100 < total += percent unless percent == 0 }.first
55
56
  else # assume even distribution on no rules
56
57
  behavior_names.empty? ? nil : behavior_names[crc % behavior_names.length]
57
58
  end
@@ -66,6 +67,20 @@ module Gitlab
66
67
  def distribution_rules
67
68
  options[:distribution]
68
69
  end
70
+
71
+ def validate_distribution_rules(distributions)
72
+ if distributions.length != behavior_names.length
73
+ raise InvalidRolloutRules, "the distribution rules don't match the number of behaviors defined"
74
+ end
75
+
76
+ if distributions.any? { |percent| percent < 0 }
77
+ raise InvalidRolloutRules, "the distribution percentage cannot be negative"
78
+ end
79
+
80
+ return if distributions.sum == 100
81
+
82
+ raise InvalidRolloutRules, 'the distribution percentages should add up to 100'
83
+ end
69
84
  end
70
85
  end
71
86
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Gitlab
4
4
  class Experiment
5
- VERSION = '0.8.0'
5
+ VERSION = '1.0.0'
6
6
  end
7
7
  end
@@ -158,7 +158,11 @@ module Gitlab
158
158
  def excluded?
159
159
  return @_excluded if defined?(@_excluded)
160
160
 
161
- @_excluded = !run_callbacks(exclusion_callback_chain) { :not_excluded }
161
+ @_excluded = !run_callbacks(exclusion_callback_chain) { :not_excluded } || only_assigned?
162
+ end
163
+
164
+ def only_assigned?
165
+ !!context.only_assigned && find_variant.blank?
162
166
  end
163
167
 
164
168
  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: 0.8.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitLab
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-03-07 00:00:00.000000000 Z
11
+ date: 2025-09-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -38,6 +38,160 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: gitlab-dangerfiles
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 4.1.0
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 4.1.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: gitlab-styles
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 12.0.1
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 12.0.1
69
+ - !ruby/object:Gem::Dependency
70
+ name: lefthook
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 1.4.7
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 1.4.7
83
+ - !ruby/object:Gem::Dependency
84
+ name: pry-byebug
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 3.10.1
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 3.10.1
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop-rails
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 2.24.0
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 2.24.0
111
+ - !ruby/object:Gem::Dependency
112
+ name: rubocop-rspec
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: 2.27.1
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: 2.27.1
125
+ - !ruby/object:Gem::Dependency
126
+ name: flipper
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: 0.26.2
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: 0.26.2
139
+ - !ruby/object:Gem::Dependency
140
+ name: generator_spec
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: 0.9.4
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: 0.9.4
153
+ - !ruby/object:Gem::Dependency
154
+ name: rspec-parameterized-table_syntax
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: 1.0.0
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: 1.0.0
167
+ - !ruby/object:Gem::Dependency
168
+ name: rspec-rails
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: 6.0.3
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: 6.0.3
181
+ - !ruby/object:Gem::Dependency
182
+ name: simplecov-cobertura
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - "~>"
186
+ - !ruby/object:Gem::Version
187
+ version: 3.1.0
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - "~>"
193
+ - !ruby/object:Gem::Version
194
+ version: 3.1.0
41
195
  description:
42
196
  email:
43
197
  - gitlab_rubygems@gitlab.com
@@ -91,14 +245,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
91
245
  requirements:
92
246
  - - ">="
93
247
  - !ruby/object:Gem::Version
94
- version: '2.6'
248
+ version: '3.0'
95
249
  required_rubygems_version: !ruby/object:Gem::Requirement
96
250
  requirements:
97
251
  - - ">="
98
252
  - !ruby/object:Gem::Version
99
253
  version: '0'
100
254
  requirements: []
101
- rubygems_version: 3.4.7
255
+ rubygems_version: 3.5.22
102
256
  signing_key:
103
257
  specification_version: 4
104
258
  summary: GitLab experimentation library.