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 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: []