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 +4 -4
- data/README.md +102 -12
- data/lib/generators/gitlab/experiment/experiment_generator.rb +3 -9
- data/lib/generators/gitlab/experiment/install/install_generator.rb +3 -8
- data/lib/generators/gitlab/experiment/install/templates/initializer.rb.tt +2 -5
- data/lib/gitlab/experiment/cache/redis_hash_store.rb +3 -3
- data/lib/gitlab/experiment/cache.rb +10 -4
- data/lib/gitlab/experiment/configuration.rb +7 -2
- data/lib/gitlab/experiment/context.rb +5 -2
- data/lib/gitlab/experiment/rollout/percent.rb +21 -6
- data/lib/gitlab/experiment/version.rb +1 -1
- data/lib/gitlab/experiment.rb +5 -1
- metadata +158 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 45c95c2d6d85df98de92096ab00a7cb124ef3cf46f2dfa858465b3c43df8b22d
|
4
|
+
data.tar.gz: 17716054da0e7aff4c67ddea28c733ec275eaf95934d627bbea40856427ecb68
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
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`
|
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
|
12
|
-
|
13
|
-
|
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
|
-
|
15
|
-
|
16
|
-
|
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
|
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, **
|
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, **
|
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, **
|
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
|
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
|
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
|
-
|
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
|
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
|
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
|
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,
|
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
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
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
|
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
|
data/lib/gitlab/experiment.rb
CHANGED
@@ -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.
|
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:
|
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: '
|
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.
|
255
|
+
rubygems_version: 3.5.22
|
102
256
|
signing_key:
|
103
257
|
specification_version: 4
|
104
258
|
summary: GitLab experimentation library.
|