gitlab-experiment 0.7.1 → 0.9.1
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 +1 -4
- 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/base_interface.rb +17 -63
- data/lib/gitlab/experiment/cache/redis_hash_store.rb +3 -3
- data/lib/gitlab/experiment/cache.rb +4 -2
- data/lib/gitlab/experiment/callbacks.rb +2 -2
- data/lib/gitlab/experiment/configuration.rb +9 -69
- data/lib/gitlab/experiment/context.rb +3 -1
- data/lib/gitlab/experiment/engine.rb +1 -0
- data/lib/gitlab/experiment/rollout/percent.rb +14 -4
- data/lib/gitlab/experiment/rollout.rb +8 -30
- data/lib/gitlab/experiment/rspec.rb +15 -18
- data/lib/gitlab/experiment/version.rb +1 -1
- data/lib/gitlab/experiment.rb +20 -67
- metadata +184 -31
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2ff3ecacc0f83a605ecaad79cef7368802307b9bc1c106eca1c74967ade3a00c
|
4
|
+
data.tar.gz: a72ceadbbcd689a290bdf7f85b9dababa7f7ae252bc3886a6fcd42170e561ee7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6d7f1804cf3503fd0fcf5be75ca6c4d3bfebcdccd3baf5aa3937a7c080e8336c15491a3476a9a7aef30060d0a526f4f71c6803d0a8ae8c08b0192e1401e1070c
|
7
|
+
data.tar.gz: 4f739b5852733e789ab23ce0b96215d023e4483ef32ae055373bfe3083d212fb5f01aa8b45ee55bfbac0a8a734e5ec429eac175a49a0219256d6f8f62151027a
|
data/README.md
CHANGED
@@ -499,9 +499,7 @@ Anyway, now you can use your custom `Flipper` rollout strategy by instantiating
|
|
499
499
|
|
500
500
|
```ruby
|
501
501
|
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
|
-
)
|
502
|
+
config.default_rollout = Gitlab::Experiment::Rollout::Flipper.new
|
505
503
|
end
|
506
504
|
```
|
507
505
|
|
@@ -512,7 +510,6 @@ class PillColorExperiment < Gitlab::Experiment # OR ApplicationExperiment
|
|
512
510
|
# ...registered behaviors
|
513
511
|
|
514
512
|
default_rollout :flipper,
|
515
|
-
include_control: true, # optionally specify to include control
|
516
513
|
distribution: { control: 26, red: 37, blue: 37 } # optionally specify distribution
|
517
514
|
end
|
518
515
|
```
|
@@ -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
|
#
|
@@ -78,79 +78,33 @@ module Gitlab
|
|
78
78
|
|
79
79
|
alias_method :to_param, :id
|
80
80
|
|
81
|
-
def
|
82
|
-
|
83
|
-
end
|
81
|
+
def process_redirect_url(url)
|
82
|
+
return unless Configuration.redirect_url_validator&.call(url)
|
84
83
|
|
85
|
-
|
86
|
-
|
84
|
+
track('visited', url: url)
|
85
|
+
url # return the url, which allows for mutation
|
87
86
|
end
|
88
87
|
|
89
|
-
|
90
|
-
|
91
|
-
named_variants = %w[control candidate]
|
92
|
-
public_methods.each_with_object(behaviors) do |name, behaviors|
|
93
|
-
name = name.to_s # fixes compatibility for ruby 2.6.x
|
94
|
-
next unless name.end_with?('_behavior')
|
95
|
-
|
96
|
-
behavior_name = name.sub(/_behavior$/, '')
|
97
|
-
registration = named_variants.include?(behavior_name) ? behavior_name : "variant :#{behavior_name}"
|
98
|
-
|
99
|
-
Configuration.deprecated(<<~MESSAGE, version: '0.7.0', stack: 2)
|
100
|
-
using a public `#{name}` method is deprecated and will be removed from {{release}}, instead register variants using:
|
101
|
-
|
102
|
-
class #{self.class.name} < #{Configuration.base_class}
|
103
|
-
#{registration}
|
88
|
+
def key_for(source, seed = name)
|
89
|
+
return source if source.is_a?(String)
|
104
90
|
|
105
|
-
|
91
|
+
source = source.keys + source.values if source.is_a?(Hash)
|
106
92
|
|
107
|
-
|
108
|
-
|
109
|
-
end
|
110
|
-
end
|
111
|
-
MESSAGE
|
112
|
-
|
113
|
-
behaviors[behavior_name] ||= -> { send(name) } # rubocop:disable GitlabSecurity/PublicSend
|
114
|
-
end
|
115
|
-
end
|
93
|
+
ingredients = Array(source).map { |v| identify(v) }
|
94
|
+
ingredients.unshift(seed).unshift(Configuration.context_key_secret)
|
116
95
|
|
117
|
-
|
118
|
-
def session_id
|
119
|
-
Configuration.deprecated(:session_id, 'instead use `id` or use a custom rollout strategy', version: '0.7.0')
|
120
|
-
id
|
96
|
+
Digest::SHA2.new(Configuration.context_key_bit_length).hexdigest(ingredients.join('|')) # rubocop:disable Fips/OpenSSL
|
121
97
|
end
|
122
98
|
|
123
99
|
# @deprecated
|
124
|
-
def
|
125
|
-
Configuration.deprecated(
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
def use(&block)
|
131
|
-
Configuration.deprecated(:use, 'instead use `control`', version: '0.7.0')
|
132
|
-
|
133
|
-
control(&block)
|
134
|
-
end
|
135
|
-
|
136
|
-
# @deprecated
|
137
|
-
def try(name = nil, &block)
|
138
|
-
if name.present?
|
139
|
-
Configuration.deprecated(:try, "instead use `variant(:#{name})`", version: '0.7.0')
|
140
|
-
variant(name, &block)
|
141
|
-
else
|
142
|
-
Configuration.deprecated(:try, 'instead use `candidate`', version: '0.7.0')
|
143
|
-
candidate(&block)
|
144
|
-
end
|
145
|
-
end
|
146
|
-
|
147
|
-
protected
|
148
|
-
|
149
|
-
def cached_variant_resolver(provided_variant)
|
150
|
-
return :control if excluded?
|
100
|
+
def variant_names
|
101
|
+
Configuration.deprecated(
|
102
|
+
:variant_names,
|
103
|
+
'instead use `behavior.names`, which includes :control',
|
104
|
+
version: '0.8.0'
|
105
|
+
)
|
151
106
|
|
152
|
-
|
153
|
-
result.to_sym if result.present?
|
107
|
+
behaviors.keys - [:control]
|
154
108
|
end
|
155
109
|
end
|
156
110
|
end
|
@@ -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
|
@@ -49,7 +49,7 @@ module Gitlab
|
|
49
49
|
result = migrated_cache_fetch(cache.store, &block)
|
50
50
|
return result unless specified.present?
|
51
51
|
|
52
|
-
cache.write(specified) if result != specified
|
52
|
+
cache.write(specified) if result.to_s != specified.to_s
|
53
53
|
specified
|
54
54
|
end
|
55
55
|
|
@@ -62,7 +62,9 @@ module Gitlab
|
|
62
62
|
def migrated_cache_fetch(store, &block)
|
63
63
|
migrations = context.signature[:migration_keys]&.map { |key| cache_key(key) } || []
|
64
64
|
migrations.find do |old_key|
|
65
|
-
|
65
|
+
value = store.read(old_key)
|
66
|
+
|
67
|
+
next unless value
|
66
68
|
|
67
69
|
store.write(cache_key, value)
|
68
70
|
store.delete(old_key)
|
@@ -47,14 +47,14 @@ module Gitlab
|
|
47
47
|
private
|
48
48
|
|
49
49
|
def build_behavior_callback(filters, variant, **options, &block)
|
50
|
-
if registered_behavior_callbacks[variant
|
50
|
+
if registered_behavior_callbacks[variant]
|
51
51
|
raise ExistingBehaviorError, "a behavior for the `#{variant}` variant has already been registered"
|
52
52
|
end
|
53
53
|
|
54
54
|
callback_behavior = "#{variant}_behavior".to_sym
|
55
55
|
|
56
56
|
# Register a the behavior so we can define the block later.
|
57
|
-
registered_behavior_callbacks[variant
|
57
|
+
registered_behavior_callbacks[variant] = callback_behavior
|
58
58
|
|
59
59
|
# Add our block or default behavior method.
|
60
60
|
filters.push(block) if block.present?
|
@@ -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,13 +56,12 @@ 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:
|
59
63
|
# :percent, (recommended), :round_robin, or :random
|
60
|
-
@default_rollout = Rollout.resolve(:percent
|
64
|
+
@default_rollout = Rollout.resolve(:percent)
|
61
65
|
|
62
66
|
# Secret seed used in generating context keys.
|
63
67
|
#
|
@@ -165,61 +169,6 @@ module Gitlab
|
|
165
169
|
end
|
166
170
|
|
167
171
|
class << self
|
168
|
-
# @deprecated
|
169
|
-
def context_hash_strategy=(block)
|
170
|
-
deprecated(
|
171
|
-
:context_hash_strategy,
|
172
|
-
'instead use `context_key_secret` and `context_key_bit_length`',
|
173
|
-
version: '0.7.0'
|
174
|
-
)
|
175
|
-
|
176
|
-
@__context_hash_strategy = block
|
177
|
-
end
|
178
|
-
|
179
|
-
# @deprecated
|
180
|
-
def variant_resolver
|
181
|
-
deprecated(
|
182
|
-
:variant_resolver,
|
183
|
-
'instead use `inclusion_resolver` with a block that returns a boolean',
|
184
|
-
version: '0.6.5'
|
185
|
-
)
|
186
|
-
|
187
|
-
@__inclusion_resolver
|
188
|
-
end
|
189
|
-
|
190
|
-
# @deprecated
|
191
|
-
def variant_resolver=(block)
|
192
|
-
deprecated(
|
193
|
-
:variant_resolver,
|
194
|
-
'instead use `inclusion_resolver` with a block that returns a boolean',
|
195
|
-
version: '0.6.5'
|
196
|
-
)
|
197
|
-
|
198
|
-
@__inclusion_resolver = block
|
199
|
-
end
|
200
|
-
|
201
|
-
# @deprecated
|
202
|
-
def inclusion_resolver=(block)
|
203
|
-
deprecated(
|
204
|
-
:inclusion_resolver,
|
205
|
-
'instead put this logic into custom rollout strategies',
|
206
|
-
version: '0.7.0'
|
207
|
-
)
|
208
|
-
|
209
|
-
@__inclusion_resolver = block
|
210
|
-
end
|
211
|
-
|
212
|
-
# @deprecated
|
213
|
-
def inclusion_resolver
|
214
|
-
deprecated(
|
215
|
-
:inclusion_resolver,
|
216
|
-
'instead put this logic into custom rollout strategies',
|
217
|
-
version: '0.7.0'
|
218
|
-
)
|
219
|
-
|
220
|
-
@__inclusion_resolver
|
221
|
-
end
|
222
|
-
|
223
172
|
attr_accessor(
|
224
173
|
:name_prefix,
|
225
174
|
:logger,
|
@@ -227,6 +176,7 @@ module Gitlab
|
|
227
176
|
:strict_registration,
|
228
177
|
:cache,
|
229
178
|
:cookie_domain,
|
179
|
+
:cookie_name,
|
230
180
|
:context_key_secret,
|
231
181
|
:context_key_bit_length,
|
232
182
|
:mount_at,
|
@@ -240,17 +190,7 @@ module Gitlab
|
|
240
190
|
# Attribute method overrides.
|
241
191
|
|
242
192
|
def default_rollout=(args) # rubocop:disable Lint/DuplicateMethods
|
243
|
-
|
244
|
-
if rollout.is_a?(Rollout::Base)
|
245
|
-
options = rollout.options
|
246
|
-
rollout = rollout.class
|
247
|
-
|
248
|
-
deprecated(<<~MESSAGE, version: '0.7.0')
|
249
|
-
using a rollout instance with `default_rollout` is deprecated and will be removed from {{release}} (instead use `default_rollout = #{rollout.name}, #{options.inspect}`)
|
250
|
-
MESSAGE
|
251
|
-
end
|
252
|
-
|
253
|
-
@default_rollout = Rollout.resolve(rollout, options || {})
|
193
|
+
@default_rollout = Rollout.resolve(*args)
|
254
194
|
end
|
255
195
|
|
256
196
|
# Internal warning helpers.
|
@@ -7,6 +7,8 @@ module Gitlab
|
|
7
7
|
|
8
8
|
DNT_REGEXP = /^(true|t|yes|y|1|on)$/i.freeze
|
9
9
|
|
10
|
+
attr_reader :request
|
11
|
+
|
10
12
|
def initialize(experiment, **initial_value)
|
11
13
|
@experiment = experiment
|
12
14
|
@value = {}
|
@@ -63,7 +65,7 @@ module Gitlab
|
|
63
65
|
add_unmerged_migration(value.delete(:migrated_from))
|
64
66
|
add_merged_migration(value.delete(:migrated_with))
|
65
67
|
|
66
|
-
migrate_cookie(value,
|
68
|
+
migrate_cookie(value, @experiment.instance_exec(@experiment, &Configuration.cookie_name))
|
67
69
|
end
|
68
70
|
|
69
71
|
def add_unmerged_migration(value = {})
|
@@ -21,6 +21,7 @@ module Gitlab
|
|
21
21
|
private
|
22
22
|
|
23
23
|
def include_dsl
|
24
|
+
Dsl.include_in(ActionController::API, with_helper: false) if defined?(ActionController)
|
24
25
|
Dsl.include_in(ActionController::Base, with_helper: true) if defined?(ActionController)
|
25
26
|
Dsl.include_in(ActionMailer::Base, with_helper: true) if defined?(ActionMailer)
|
26
27
|
end
|
@@ -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
|
@@ -66,6 +66,16 @@ module Gitlab
|
|
66
66
|
def distribution_rules
|
67
67
|
options[:distribution]
|
68
68
|
end
|
69
|
+
|
70
|
+
def validate_distribution_rules(distributions)
|
71
|
+
if distributions.length != behavior_names.length
|
72
|
+
raise InvalidRolloutRules, "the distribution rules don't match the number of behaviors defined"
|
73
|
+
end
|
74
|
+
|
75
|
+
return if distributions.sum == 100
|
76
|
+
|
77
|
+
raise InvalidRolloutRules, 'the distribution percentages should add up to 100'
|
78
|
+
end
|
69
79
|
end
|
70
80
|
end
|
71
81
|
end
|
@@ -8,6 +8,7 @@ module Gitlab
|
|
8
8
|
autoload :RoundRobin, 'gitlab/experiment/rollout/round_robin.rb'
|
9
9
|
|
10
10
|
def self.resolve(klass, options = {})
|
11
|
+
options ||= {}
|
11
12
|
case klass
|
12
13
|
when String
|
13
14
|
Strategy.new(klass.classify.constantize, options)
|
@@ -21,44 +22,29 @@ module Gitlab
|
|
21
22
|
end
|
22
23
|
|
23
24
|
class Base
|
24
|
-
DEFAULT_OPTIONS = {
|
25
|
-
include_control: false
|
26
|
-
}.freeze
|
27
|
-
|
28
25
|
attr_reader :experiment, :options
|
29
26
|
|
30
|
-
delegate :
|
31
|
-
|
32
|
-
def initialize(options = {})
|
33
|
-
@options = DEFAULT_OPTIONS.merge(options)
|
34
|
-
end
|
27
|
+
delegate :cache, :id, to: :experiment
|
35
28
|
|
36
|
-
def
|
29
|
+
def initialize(experiment, options = {})
|
37
30
|
raise ArgumentError, 'you must provide an experiment instance' unless experiment.class <= Gitlab::Experiment
|
38
31
|
|
39
32
|
@experiment = experiment
|
40
|
-
|
41
|
-
self
|
33
|
+
@options = options
|
42
34
|
end
|
43
35
|
|
44
36
|
def enabled?
|
45
|
-
require_experiment(__method__)
|
46
|
-
|
47
37
|
true
|
48
38
|
end
|
49
39
|
|
50
40
|
def resolve
|
51
|
-
require_experiment(__method__)
|
52
|
-
|
53
|
-
return nil if @experiment.respond_to?(:experiment_group?) && !@experiment.experiment_group?
|
54
|
-
|
55
41
|
validate! # allow the rollout strategy to validate itself
|
56
42
|
|
57
43
|
assignment = execute_assignment
|
58
|
-
assignment == :control ? nil : assignment # avoid caching control
|
44
|
+
assignment == :control ? nil : assignment # avoid caching control by returning nil
|
59
45
|
end
|
60
46
|
|
61
|
-
|
47
|
+
private
|
62
48
|
|
63
49
|
def validate!
|
64
50
|
# base is always valid
|
@@ -68,22 +54,14 @@ module Gitlab
|
|
68
54
|
behavior_names.first
|
69
55
|
end
|
70
56
|
|
71
|
-
private
|
72
|
-
|
73
|
-
def require_experiment(method_name)
|
74
|
-
return if @experiment.present?
|
75
|
-
|
76
|
-
raise ArgumentError, "you need to call `for` with an experiment instance before chaining `#{method_name}`"
|
77
|
-
end
|
78
|
-
|
79
57
|
def behavior_names
|
80
|
-
|
58
|
+
experiment.behaviors.keys
|
81
59
|
end
|
82
60
|
end
|
83
61
|
|
84
62
|
Strategy = Struct.new(:klass, :options) do
|
85
63
|
def for(experiment)
|
86
|
-
klass.new(options)
|
64
|
+
klass.new(experiment, options)
|
87
65
|
end
|
88
66
|
end
|
89
67
|
end
|
@@ -14,13 +14,13 @@ module Gitlab
|
|
14
14
|
def self.track_gitlab_experiment_receiver(method, receiver)
|
15
15
|
# Leverage the `>=` method on Gitlab::Experiment to determine if the receiver is an experiment, not the other
|
16
16
|
# way round -- `receiver.<=` could be mocked and we want to be extra careful.
|
17
|
-
(@__gitlab_experiment_receivers[method
|
17
|
+
(@__gitlab_experiment_receivers[method] ||= []) << receiver if Gitlab::Experiment >= receiver
|
18
18
|
rescue StandardError # again, let's just be extra careful
|
19
19
|
false
|
20
20
|
end
|
21
21
|
|
22
22
|
def self.bind_gitlab_experiment_receiver(method)
|
23
|
-
method.unbind.bind(@__gitlab_experiment_receivers[method
|
23
|
+
method.unbind.bind(@__gitlab_experiment_receivers[method].pop)
|
24
24
|
end
|
25
25
|
|
26
26
|
module MethodDouble
|
@@ -28,6 +28,7 @@ module Gitlab
|
|
28
28
|
RSpecMocks.track_gitlab_experiment_receiver(original_method, receiver)
|
29
29
|
super
|
30
30
|
end
|
31
|
+
ruby2_keywords :proxy_method_invoked if respond_to?(:ruby2_keywords, true)
|
31
32
|
end
|
32
33
|
end
|
33
34
|
|
@@ -37,9 +38,6 @@ module Gitlab
|
|
37
38
|
wrapped_experiment(experiment, remock: true) do |instance, wrapped|
|
38
39
|
# Stub internal methods that will make it behave as we've instructed.
|
39
40
|
allow(instance).to receive(:enabled?) { wrapped.variant_name != false }
|
40
|
-
if instance.respond_to?(:experiment_group?, true)
|
41
|
-
allow(instance).to receive(:experiment_group?) { !(wrapped.variant_name == false) }
|
42
|
-
end
|
43
41
|
|
44
42
|
# Stub the variant resolution logic to handle true/false, and named variants.
|
45
43
|
allow(instance).to receive(:resolve_variant_name).and_wrap_original { |method|
|
@@ -72,8 +70,8 @@ module Gitlab
|
|
72
70
|
def wrapped_experiment_chain_for(klass)
|
73
71
|
@__wrapped_experiment_chains ||= {}
|
74
72
|
@__wrapped_experiment_chains[klass.name || klass.object_id] ||= begin
|
75
|
-
allow(klass).to receive(:new).and_wrap_original do |method, *args, &original_block|
|
76
|
-
RSpecMocks.bind_gitlab_experiment_receiver(method).call(*args).tap do |instance|
|
73
|
+
allow(klass).to receive(:new).and_wrap_original do |method, *args, **kwargs, &original_block|
|
74
|
+
RSpecMocks.bind_gitlab_experiment_receiver(method).call(*args, **kwargs).tap do |instance|
|
77
75
|
wrapped = @__wrapped_experiments[instance.instance_variable_get(:@_name)]
|
78
76
|
wrapped&.blocks&.each { |b| b.call(instance, wrapped) }
|
79
77
|
|
@@ -128,7 +126,7 @@ module Gitlab
|
|
128
126
|
match do |experiment|
|
129
127
|
@experiment = require_experiment(experiment, 'register_behavior')
|
130
128
|
|
131
|
-
block = @experiment.behaviors[behavior_name
|
129
|
+
block = @experiment.behaviors[behavior_name]
|
132
130
|
@return_expected = false unless block
|
133
131
|
|
134
132
|
if @return_expected
|
@@ -193,13 +191,13 @@ module Gitlab
|
|
193
191
|
@experiment.run_callbacks(:segmentation)
|
194
192
|
|
195
193
|
@actual_variant = @experiment.instance_variable_get(:@_assigned_variant_name)
|
196
|
-
@expected_variant ? @actual_variant
|
194
|
+
@expected_variant ? @actual_variant == @expected_variant : @actual_variant.present?
|
197
195
|
end
|
198
196
|
|
199
197
|
chain :into do |expected|
|
200
198
|
raise ArgumentError, 'variant name must be provided' if expected.blank?
|
201
199
|
|
202
|
-
@expected_variant = expected
|
200
|
+
@expected_variant = expected
|
203
201
|
end
|
204
202
|
|
205
203
|
failure_message do
|
@@ -239,7 +237,7 @@ module Gitlab
|
|
239
237
|
chain(:for) do |expected|
|
240
238
|
raise ArgumentError, 'variant name must be provided' if expected.blank?
|
241
239
|
|
242
|
-
@expected_variant = expected
|
240
|
+
@expected_variant = expected
|
243
241
|
end
|
244
242
|
|
245
243
|
chain(:with_context) do |expected|
|
@@ -303,17 +301,16 @@ RSpec.configure do |config|
|
|
303
301
|
config.include Gitlab::Experiment::RSpecHelpers
|
304
302
|
config.include Gitlab::Experiment::Dsl
|
305
303
|
|
306
|
-
|
307
|
-
|
304
|
+
config.before(:each) do |example|
|
305
|
+
if example.metadata[:experiment] == true || example.metadata[:type] == :experiment
|
306
|
+
RequestStore.clear!
|
308
307
|
|
309
|
-
|
310
|
-
|
308
|
+
if defined?(Gitlab::Experiment::TestBehaviors::TrackedStructure)
|
309
|
+
Gitlab::Experiment::TestBehaviors::TrackedStructure.reset!
|
310
|
+
end
|
311
311
|
end
|
312
312
|
end
|
313
313
|
|
314
|
-
config.before(:each, :experiment, &clear_cache)
|
315
|
-
config.before(:each, type: :experiment, &clear_cache)
|
316
|
-
|
317
314
|
config.include Gitlab::Experiment::RSpecMatchers, :experiment
|
318
315
|
config.include Gitlab::Experiment::RSpecMatchers, type: :experiment
|
319
316
|
|
data/lib/gitlab/experiment.rb
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
require 'request_store'
|
4
4
|
require 'active_support'
|
5
5
|
require 'active_support/core_ext/module/delegation'
|
6
|
+
require 'active_support/core_ext/object/blank'
|
6
7
|
require 'active_support/core_ext/string/inflections'
|
7
8
|
|
8
9
|
require 'gitlab/experiment/errors'
|
@@ -87,34 +88,15 @@ module Gitlab
|
|
87
88
|
variant(:control, &block)
|
88
89
|
end
|
89
90
|
|
90
|
-
def candidate(
|
91
|
-
|
92
|
-
Configuration.deprecated(<<~MESSAGE, version: '0.7.0')
|
93
|
-
passing name to `candidate` is deprecated and will be removed from {{release}} (instead use `variant(#{name.inspect})`)
|
94
|
-
MESSAGE
|
95
|
-
end
|
96
|
-
|
97
|
-
variant(name || :candidate, &block)
|
91
|
+
def candidate(&block)
|
92
|
+
variant(:candidate, &block)
|
98
93
|
end
|
99
94
|
|
100
|
-
def variant(name
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
return behaviors[name.to_s] = block
|
105
|
-
end
|
106
|
-
|
107
|
-
if name.present?
|
108
|
-
Configuration.deprecated(<<~MESSAGE, version: '0.7.0')
|
109
|
-
setting the variant using `variant` is deprecated and will be removed from {{release}} (instead use `assigned(#{name.inspect})`)
|
110
|
-
MESSAGE
|
111
|
-
else
|
112
|
-
Configuration.deprecated(<<~MESSAGE, version: '0.7.0')
|
113
|
-
getting the assigned variant using `variant` is deprecated and will be removed from {{release}} (instead use `assigned`)
|
114
|
-
MESSAGE
|
115
|
-
end
|
95
|
+
def variant(name, &block)
|
96
|
+
raise ArgumentError, 'name required' if name.blank?
|
97
|
+
raise ArgumentError, 'block required' unless block.present?
|
116
98
|
|
117
|
-
|
99
|
+
behaviors[name] = block
|
118
100
|
end
|
119
101
|
|
120
102
|
def context(value = nil)
|
@@ -126,9 +108,7 @@ module Gitlab
|
|
126
108
|
|
127
109
|
def assigned(value = nil)
|
128
110
|
@_assigned_variant_name = cache_variant(value) if value.present?
|
129
|
-
if @_assigned_variant_name || @_resolving_variant
|
130
|
-
return Variant.new(name: (@_assigned_variant_name || :unresolved).to_s)
|
131
|
-
end
|
111
|
+
return Variant.new(name: @_assigned_variant_name || :unresolved) if @_assigned_variant_name || @_resolving_variant
|
132
112
|
|
133
113
|
if enabled?
|
134
114
|
@_resolving_variant = true
|
@@ -137,7 +117,7 @@ module Gitlab
|
|
137
117
|
|
138
118
|
run_callbacks(segmentation_callback_chain) do
|
139
119
|
@_assigned_variant_name ||= :control
|
140
|
-
Variant.new(name: @_assigned_variant_name
|
120
|
+
Variant.new(name: @_assigned_variant_name)
|
141
121
|
end
|
142
122
|
ensure
|
143
123
|
@_resolving_variant = false
|
@@ -171,13 +151,6 @@ module Gitlab
|
|
171
151
|
instance_exec(action, tracking_context(event_args).try(:compact) || {}, &Configuration.tracking_behavior)
|
172
152
|
end
|
173
153
|
|
174
|
-
def process_redirect_url(url)
|
175
|
-
return unless Configuration.redirect_url_validator&.call(url)
|
176
|
-
|
177
|
-
track('visited', url: url)
|
178
|
-
url # return the url, which allows for mutation
|
179
|
-
end
|
180
|
-
|
181
154
|
def enabled?
|
182
155
|
rollout.enabled?
|
183
156
|
end
|
@@ -193,23 +166,11 @@ module Gitlab
|
|
193
166
|
end
|
194
167
|
|
195
168
|
def signature
|
196
|
-
{ variant: assigned.name, experiment: name }.merge(context.signature)
|
169
|
+
{ variant: assigned.name.to_s, experiment: name }.merge(context.signature)
|
197
170
|
end
|
198
171
|
|
199
|
-
def
|
200
|
-
|
201
|
-
if (block = Configuration.instance_variable_get(:@__context_hash_strategy))
|
202
|
-
return instance_exec(source, seed, &block)
|
203
|
-
end
|
204
|
-
|
205
|
-
return source if source.is_a?(String)
|
206
|
-
|
207
|
-
source = source.keys + source.values if source.is_a?(Hash)
|
208
|
-
|
209
|
-
ingredients = Array(source).map { |v| identify(v) }
|
210
|
-
ingredients.unshift(seed).unshift(Configuration.context_key_secret)
|
211
|
-
|
212
|
-
Digest::SHA2.new(Configuration.context_key_bit_length).hexdigest(ingredients.join('|'))
|
172
|
+
def behaviors
|
173
|
+
@_behaviors ||= registered_behavior_callbacks
|
213
174
|
end
|
214
175
|
|
215
176
|
protected
|
@@ -218,23 +179,15 @@ module Gitlab
|
|
218
179
|
(object.respond_to?(:to_global_id) ? object.to_global_id : object).to_s
|
219
180
|
end
|
220
181
|
|
182
|
+
def cached_variant_resolver(provided_variant)
|
183
|
+
return :control if excluded?
|
184
|
+
|
185
|
+
result = cache_variant(provided_variant) { resolve_variant_name }
|
186
|
+
result.to_sym if result.present?
|
187
|
+
end
|
188
|
+
|
221
189
|
def resolve_variant_name
|
222
|
-
|
223
|
-
# TODO: Remove - deprecated in release 0.7.0
|
224
|
-
Configuration.deprecated(:experiment_group?, <<~MESSAGE, version: '0.7.0')
|
225
|
-
instead put this logic into custom rollout strategies
|
226
|
-
MESSAGE
|
227
|
-
|
228
|
-
rollout.resolve if experiment_group?
|
229
|
-
elsif (block = Configuration.instance_variable_get(:@__inclusion_resolver))
|
230
|
-
# TODO: Remove - deprecated in release 0.7.0
|
231
|
-
rollout.resolve if instance_exec(@_assigned_variant_name, &block)
|
232
|
-
elsif (block = Configuration.instance_variable_get(:@__variant_resolver))
|
233
|
-
# TODO: Remove - deprecated in release 0.6.5
|
234
|
-
instance_exec(@_assigned_variant_name, &block)
|
235
|
-
else
|
236
|
-
rollout.resolve # this is the end result of all deprecations
|
237
|
-
end
|
190
|
+
rollout.resolve
|
238
191
|
end
|
239
192
|
|
240
193
|
def tracking_context(event_args)
|
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: 0.9.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- GitLab
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date:
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -38,7 +38,161 @@ dependencies:
|
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '1.0'
|
41
|
-
|
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: 10.1.0
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 10.1.0
|
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.20.2
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 2.20.2
|
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.22.0
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: 2.22.0
|
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: 2.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: 2.1.0
|
195
|
+
description:
|
42
196
|
email:
|
43
197
|
- gitlab_rubygems@gitlab.com
|
44
198
|
executables: []
|
@@ -47,59 +201,58 @@ extra_rdoc_files: []
|
|
47
201
|
files:
|
48
202
|
- lib/generators/gitlab
|
49
203
|
- lib/generators/gitlab/experiment
|
204
|
+
- lib/generators/gitlab/experiment/USAGE
|
205
|
+
- lib/generators/gitlab/experiment/experiment_generator.rb
|
50
206
|
- lib/generators/gitlab/experiment/install
|
51
207
|
- lib/generators/gitlab/experiment/install/install_generator.rb
|
52
208
|
- lib/generators/gitlab/experiment/install/templates
|
209
|
+
- lib/generators/gitlab/experiment/install/templates/POST_INSTALL
|
53
210
|
- lib/generators/gitlab/experiment/install/templates/application_experiment.rb.tt
|
54
211
|
- lib/generators/gitlab/experiment/install/templates/initializer.rb.tt
|
55
|
-
- lib/generators/gitlab/experiment/install/templates/POST_INSTALL
|
56
|
-
- lib/generators/gitlab/experiment/USAGE
|
57
|
-
- lib/generators/gitlab/experiment/experiment_generator.rb
|
58
212
|
- lib/generators/gitlab/experiment/templates
|
59
213
|
- lib/generators/gitlab/experiment/templates/experiment.rb.tt
|
60
|
-
- lib/generators/test_unit
|
61
|
-
- lib/generators/test_unit/experiment
|
62
|
-
- lib/generators/test_unit/experiment/experiment_generator.rb
|
63
|
-
- lib/generators/test_unit/experiment/templates
|
64
|
-
- lib/generators/test_unit/experiment/templates/experiment_test.rb.tt
|
65
214
|
- lib/generators/rspec
|
66
215
|
- lib/generators/rspec/experiment
|
67
216
|
- lib/generators/rspec/experiment/experiment_generator.rb
|
68
217
|
- lib/generators/rspec/experiment/templates
|
69
218
|
- lib/generators/rspec/experiment/templates/experiment_spec.rb.tt
|
70
|
-
- lib/
|
219
|
+
- lib/generators/test_unit
|
220
|
+
- lib/generators/test_unit/experiment
|
221
|
+
- lib/generators/test_unit/experiment/experiment_generator.rb
|
222
|
+
- lib/generators/test_unit/experiment/templates
|
223
|
+
- lib/generators/test_unit/experiment/templates/experiment_test.rb.tt
|
71
224
|
- lib/gitlab/experiment
|
72
|
-
- lib/gitlab/experiment/
|
73
|
-
- lib/gitlab/experiment/middleware.rb
|
74
|
-
- lib/gitlab/experiment/test_behaviors
|
75
|
-
- lib/gitlab/experiment/test_behaviors/trackable.rb
|
225
|
+
- lib/gitlab/experiment/base_interface.rb
|
76
226
|
- lib/gitlab/experiment/cache
|
77
227
|
- lib/gitlab/experiment/cache/redis_hash_store.rb
|
78
|
-
- lib/gitlab/experiment/
|
228
|
+
- lib/gitlab/experiment/cache.rb
|
79
229
|
- lib/gitlab/experiment/callbacks.rb
|
80
|
-
- lib/gitlab/experiment/
|
81
|
-
- lib/gitlab/experiment/base_interface.rb
|
82
|
-
- lib/gitlab/experiment/nestable.rb
|
230
|
+
- lib/gitlab/experiment/configuration.rb
|
83
231
|
- lib/gitlab/experiment/context.rb
|
232
|
+
- lib/gitlab/experiment/cookies.rb
|
233
|
+
- lib/gitlab/experiment/dsl.rb
|
84
234
|
- lib/gitlab/experiment/engine.rb
|
85
|
-
- lib/gitlab/experiment/
|
235
|
+
- lib/gitlab/experiment/errors.rb
|
236
|
+
- lib/gitlab/experiment/middleware.rb
|
237
|
+
- lib/gitlab/experiment/nestable.rb
|
86
238
|
- lib/gitlab/experiment/rollout
|
239
|
+
- lib/gitlab/experiment/rollout/percent.rb
|
87
240
|
- lib/gitlab/experiment/rollout/random.rb
|
88
241
|
- lib/gitlab/experiment/rollout/round_robin.rb
|
89
|
-
- lib/gitlab/experiment/rollout
|
90
|
-
- lib/gitlab/experiment/
|
91
|
-
- lib/gitlab/experiment/
|
242
|
+
- lib/gitlab/experiment/rollout.rb
|
243
|
+
- lib/gitlab/experiment/rspec.rb
|
244
|
+
- lib/gitlab/experiment/test_behaviors
|
245
|
+
- lib/gitlab/experiment/test_behaviors/trackable.rb
|
246
|
+
- lib/gitlab/experiment/variant.rb
|
92
247
|
- lib/gitlab/experiment/version.rb
|
93
|
-
- lib/gitlab/experiment
|
94
|
-
- lib/gitlab/experiment/configuration.rb
|
95
|
-
- lib/gitlab/experiment/dsl.rb
|
248
|
+
- lib/gitlab/experiment.rb
|
96
249
|
- LICENSE.txt
|
97
250
|
- README.md
|
98
251
|
homepage: https://gitlab.com/gitlab-org/ruby/gems/gitlab-experiment
|
99
252
|
licenses:
|
100
253
|
- MIT
|
101
254
|
metadata: {}
|
102
|
-
post_install_message:
|
255
|
+
post_install_message:
|
103
256
|
rdoc_options: []
|
104
257
|
require_paths:
|
105
258
|
- lib
|
@@ -114,8 +267,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
114
267
|
- !ruby/object:Gem::Version
|
115
268
|
version: '0'
|
116
269
|
requirements: []
|
117
|
-
rubygems_version: 3.
|
118
|
-
signing_key:
|
270
|
+
rubygems_version: 3.3.26
|
271
|
+
signing_key:
|
119
272
|
specification_version: 4
|
120
273
|
summary: GitLab experimentation library.
|
121
274
|
test_files: []
|