gitlab-experiment 0.7.1 → 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 83f13c3f8259c739163ada749acee80ecbfb48f1b94f66a4cd69e265c29df874
4
- data.tar.gz: e70fca76e1af2f6234d888473a31cdf088bf44d0bb5dc13dbf84f40e781076bd
3
+ metadata.gz: 2ff3ecacc0f83a605ecaad79cef7368802307b9bc1c106eca1c74967ade3a00c
4
+ data.tar.gz: a72ceadbbcd689a290bdf7f85b9dababa7f7ae252bc3886a6fcd42170e561ee7
5
5
  SHA512:
6
- metadata.gz: 9d0964f78bfea29586c176cf001a335df04618e62dfd1a76952d5d12d643de6970d8eeec063b1a28255fabfad8a3fd2b47df233cea6d071b46d98d7867b97185
7
- data.tar.gz: 172778e34c61ffe02f3d11e5538beef44ddd0f8c5e2f6029a449904264eb1365f2f013aad25648e3c1ee0fd19a015aa69a6e62de9867f87c339f8c36e4d14776
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 :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
  #
@@ -78,79 +78,33 @@ module Gitlab
78
78
 
79
79
  alias_method :to_param, :id
80
80
 
81
- def variant_names
82
- @_variant_names ||= behaviors.keys.map(&:to_sym) - [:control]
83
- end
81
+ def process_redirect_url(url)
82
+ return unless Configuration.redirect_url_validator&.call(url)
84
83
 
85
- def behaviors
86
- @_behaviors ||= public_behaviors_with_deprecations(registered_behavior_callbacks)
84
+ track('visited', url: url)
85
+ url # return the url, which allows for mutation
87
86
  end
88
87
 
89
- # @deprecated
90
- def public_behaviors_with_deprecations(behaviors)
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
- private
91
+ source = source.keys + source.values if source.is_a?(Hash)
106
92
 
107
- def #{name}
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
- # @deprecated
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 flipper_id
125
- Configuration.deprecated(:flipper_id, 'instead use `id` or use a custom rollout strategy', version: '0.7.0')
126
- "Experiment;#{id}"
127
- end
128
-
129
- # @deprecated
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
- result = cache_variant(provided_variant) { resolve_variant_name }
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, **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
@@ -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
- next unless (value = store.read(old_key))
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.to_s]
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.to_s] = callback_behavior
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, # :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:
59
63
  # :percent, (recommended), :round_robin, or :random
60
- @default_rollout = Rollout.resolve(:percent, include_control: true)
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
- rollout, options = Array(args)
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, "#{@experiment.name}_id")
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, Hash
38
- if distribution_rules.length != behavior_names.length
39
- raise InvalidRolloutRules, "the distribution rules don't match the number of variants 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
@@ -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 :variant_names, :cache, :id, to: :experiment
31
-
32
- def initialize(options = {})
33
- @options = DEFAULT_OPTIONS.merge(options)
34
- end
27
+ delegate :cache, :id, to: :experiment
35
28
 
36
- def for(experiment)
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
- protected
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
- options[:include_control] ? [:control] + variant_names : variant_names
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).for(experiment)
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.to_s] ||= []) << receiver if Gitlab::Experiment >= receiver
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.to_s].pop)
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.to_s]
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.to_s == @expected_variant.to_s : @actual_variant.present?
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.to_s
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.to_s
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
- clear_cache = proc do
307
- RequestStore.clear!
304
+ config.before(:each) do |example|
305
+ if example.metadata[:experiment] == true || example.metadata[:type] == :experiment
306
+ RequestStore.clear!
308
307
 
309
- if defined?(Gitlab::Experiment::TestBehaviors::TrackedStructure)
310
- Gitlab::Experiment::TestBehaviors::TrackedStructure.reset!
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
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Gitlab
4
4
  class Experiment
5
- VERSION = '0.7.1'
5
+ VERSION = '0.9.1'
6
6
  end
7
7
  end
@@ -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(name = nil, &block)
91
- if name.present?
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 = nil, &block)
101
- if block.present? # we know we're defining a variant block
102
- raise ArgumentError, 'missing variant name' if name.blank?
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
- assigned(name)
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.to_s)
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 key_for(source, seed = name)
200
- # TODO: Remove - deprecated in release 0.7.0
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
- if respond_to?(:experiment_group?, true)
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.7.1
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
- description:
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/gitlab/experiment.rb
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/variant.rb
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/errors.rb
228
+ - lib/gitlab/experiment/cache.rb
79
229
  - lib/gitlab/experiment/callbacks.rb
80
- - lib/gitlab/experiment/rollout.rb
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/rspec.rb
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/percent.rb
90
- - lib/gitlab/experiment/rollout/concerns
91
- - lib/gitlab/experiment/cache.rb
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/cookies.rb
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.1.6
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: []