gitlab-experiment 0.7.0 → 0.8.0

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: b6d54c567ebebd09762fb8bbd93940efb449f99d546a62776889caebedda6387
4
- data.tar.gz: 87a37b577f0545885ce4c2d6db92f750781017c2235b4d89f0f5db977bec045c
3
+ metadata.gz: c92b7e2d77e534a23b8233f11539fa9c2767b21adab9c247e916933724dd7f5d
4
+ data.tar.gz: 7a1144676a16835f64eaefb9ea7bac2b7055c5cf8e329d55dba6c115be17eb50
5
5
  SHA512:
6
- metadata.gz: ed428cb380868eb7e94e9a5b4753c7c09dd73656fa66da4bb90fc728b2e2a460cb8d7f24170e783a63c6c28f86d61b00cb99ae9065a3c02999e7518ecc79e2e2
7
- data.tar.gz: 001edc2f533b5a5c210ada04c73a335b51e0e377be05449f0ae86ba9da078655d54e860294dd8a66ea8564270345cbbd145dc789aae1eb7f76f074db1fd79be3
6
+ metadata.gz: b7a645b7d2c3cf13d7031fc61dfdaef2626d0092661093a19ff5e21d346adda8cbe031b5c360de21fda3ecee828eaa7bda0a6afe839b6c5ce7ba57f9e10232eb
7
+ data.tar.gz: 1a8e4672a903746f5f31f2cf5dcf815bb6e2c3cb8c8fa73430c93462977310fcd6ed2f6f8676d5e2ccf856289c6befde6469e9f29715341c4bde7fd0b14fa51b
@@ -48,12 +48,10 @@ Gitlab::Experiment.configure do |config|
48
48
  # end
49
49
  #
50
50
  # Included rollout strategies:
51
- # Gitlab::Experiment::Rollout::Percent, (recommended)
52
- # Gitlab::Experiment::Rollout::RoundRobin, or
53
- # Gitlab::Experiment::Rollout::Random
54
- config.default_rollout = Gitlab::Experiment::Rollout::Percent.new(
51
+ # :percent (recommended), :round_robin, or :random
52
+ config.default_rollout = :percent, {
55
53
  include_control: true # include control in possible assignments
56
- )
54
+ }
57
55
 
58
56
  # Secret seed used in generating context keys.
59
57
  #
@@ -132,7 +130,7 @@ Gitlab::Experiment.configure do |config|
132
130
  # level1 initiated by file_name.rb:2
133
131
  # level2 initiated by file_name.rb:3
134
132
  config.nested_behavior = lambda do |nested_experiment|
135
- raise NestingError.new(experiment: self, nested_experiment: nested_experiment)
133
+ raise Gitlab::Experiment::NestingError.new(experiment: self, nested_experiment: nested_experiment)
136
134
  end
137
135
 
138
136
  # Called at the end of every experiment run, with the result.
@@ -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
@@ -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
 
@@ -66,10 +66,8 @@ module Gitlab
66
66
 
67
67
  store.write(cache_key, value)
68
68
  store.delete(old_key)
69
- return value
70
- end
71
-
72
- store.fetch(cache_key, &block)
69
+ break value
70
+ end || store.fetch(cache_key, &block)
73
71
  end
74
72
  end
75
73
  end
@@ -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?
@@ -82,7 +82,7 @@ module Gitlab
82
82
  end
83
83
 
84
84
  def build_run_callback(filters, **options)
85
- set_callback(:run, *filters, **options)
85
+ set_callback(:run, *filters.compact, **options)
86
86
  end
87
87
 
88
88
  def build_callback(chain, *filters, **options)
@@ -56,12 +56,8 @@ module Gitlab
56
56
  # end
57
57
  #
58
58
  # Included rollout strategies:
59
- # Gitlab::Experiment::Rollout::Percent, (recommended)
60
- # Gitlab::Experiment::Rollout::RoundRobin, or
61
- # Gitlab::Experiment::Rollout::Random
62
- @default_rollout = Gitlab::Experiment::Rollout::Percent.new(
63
- include_control: true # include control in possible assignments
64
- )
59
+ # :percent, (recommended), :round_robin, or :random
60
+ @default_rollout = Rollout.resolve(:percent)
65
61
 
66
62
  # Secret seed used in generating context keys.
67
63
  #
@@ -169,61 +165,6 @@ module Gitlab
169
165
  end
170
166
 
171
167
  class << self
172
- # @deprecated
173
- def context_hash_strategy=(block)
174
- deprecated(
175
- :context_hash_strategy,
176
- 'instead use `context_key_secret` and `context_key_bit_length`',
177
- version: '0.7.0'
178
- )
179
-
180
- @__context_hash_strategy = block
181
- end
182
-
183
- # @deprecated
184
- def variant_resolver
185
- deprecated(
186
- :variant_resolver,
187
- 'instead use `inclusion_resolver` with a block that returns a boolean',
188
- version: '0.6.5'
189
- )
190
-
191
- @__inclusion_resolver
192
- end
193
-
194
- # @deprecated
195
- def variant_resolver=(block)
196
- deprecated(
197
- :variant_resolver,
198
- 'instead use `inclusion_resolver` with a block that returns a boolean',
199
- version: '0.6.5'
200
- )
201
-
202
- @__inclusion_resolver = block
203
- end
204
-
205
- # @deprecated
206
- def inclusion_resolver=(block)
207
- deprecated(
208
- :inclusion_resolver,
209
- 'instead put this logic into custom rollout strategies',
210
- version: '0.7.0'
211
- )
212
-
213
- @__inclusion_resolver = block
214
- end
215
-
216
- # @deprecated
217
- def inclusion_resolver
218
- deprecated(
219
- :inclusion_resolver,
220
- 'instead put this logic into custom rollout strategies',
221
- version: '0.7.0'
222
- )
223
-
224
- @__inclusion_resolver
225
- end
226
-
227
168
  attr_accessor(
228
169
  :name_prefix,
229
170
  :logger,
@@ -241,7 +182,14 @@ module Gitlab
241
182
  :publishing_behavior
242
183
  )
243
184
 
244
- # Internal helpers warnings.
185
+ # Attribute method overrides.
186
+
187
+ def default_rollout=(args) # rubocop:disable Lint/DuplicateMethods
188
+ @default_rollout = Rollout.resolve(*args)
189
+ end
190
+
191
+ # Internal warning helpers.
192
+
245
193
  def deprecated(*args, version:, stack: 0)
246
194
  deprecator = deprecator(version)
247
195
  args << args.pop.to_s.gsub('{{release}}', "#{deprecator.gem_name} #{deprecator.deprecation_horizon}")
@@ -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
@@ -25,15 +25,25 @@ module Gitlab
25
25
  class Stack
26
26
  include Singleton
27
27
 
28
- @stack = []
28
+ delegate :pop, :length, :size, :[], to: :stack
29
29
 
30
30
  class << self
31
- delegate :pop, :length, :size, :[], to: :@stack
31
+ delegate :pop, :push, :length, :size, :[], to: :instance
32
+ end
33
+
34
+ def initialize
35
+ @thread_key = "#{self.class};#{object_id}".to_sym
36
+ end
37
+
38
+ def push(instance)
39
+ stack.last&.nest_experiment(instance)
40
+ stack.push(instance)
41
+ end
42
+
43
+ private
32
44
 
33
- def push(instance)
34
- @stack.last&.nest_experiment(instance)
35
- @stack.push(instance)
36
- end
45
+ def stack
46
+ Thread.current[@thread_key] ||= []
37
47
  end
38
48
  end
39
49
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "zlib"
4
+
3
5
  # The percent rollout strategy is the most comprehensive included with Gitlab::Experiment. It allows specifying the
4
6
  # percentages per variant using an array, a hash, or will default to even distribution when no rules are provided.
5
7
  #
@@ -34,14 +36,14 @@ module Gitlab
34
36
  when nil then nil
35
37
  when Array, Hash
36
38
  if distribution_rules.length != behavior_names.length
37
- raise InvalidRolloutRules, "the distribution rules don't match the number of variants defined"
39
+ raise InvalidRolloutRules, "the distribution rules don't match the number of behaviors defined"
38
40
  end
39
41
  else
40
42
  raise InvalidRolloutRules, 'unknown distribution options type'
41
43
  end
42
44
  end
43
45
 
44
- def execute_assigment
46
+ def execute_assignment
45
47
  crc = normalized_id
46
48
  total = 0
47
49
 
@@ -27,7 +27,7 @@ module Gitlab
27
27
  class Random < Base
28
28
  protected
29
29
 
30
- def execute_assigment
30
+ def execute_assignment
31
31
  behavior_names.sample # pick a random variant
32
32
  end
33
33
  end
@@ -31,7 +31,7 @@ module Gitlab
31
31
 
32
32
  protected
33
33
 
34
- def execute_assigment
34
+ def execute_assignment
35
35
  behavior_names[(cache.attr_inc(KEY_NAME) - 1) % behavior_names.size]
36
36
  end
37
37
  end
@@ -7,70 +7,61 @@ module Gitlab
7
7
  autoload :Random, 'gitlab/experiment/rollout/random.rb'
8
8
  autoload :RoundRobin, 'gitlab/experiment/rollout/round_robin.rb'
9
9
 
10
- def self.resolve(klass)
11
- return "#{name}::#{klass.to_s.classify}".constantize if klass.is_a?(Symbol) || klass.is_a?(String)
12
-
13
- klass
10
+ def self.resolve(klass, options = {})
11
+ options ||= {}
12
+ case klass
13
+ when String
14
+ Strategy.new(klass.classify.constantize, options)
15
+ when Symbol
16
+ Strategy.new("#{name}::#{klass.to_s.classify}".constantize, options)
17
+ when Class
18
+ Strategy.new(klass, options)
19
+ else
20
+ raise ArgumentError, "unable to resolve rollout from #{klass.inspect}"
21
+ end
14
22
  end
15
23
 
16
24
  class Base
17
- DEFAULT_OPTIONS = {
18
- include_control: false
19
- }.freeze
20
-
21
25
  attr_reader :experiment, :options
22
26
 
23
- delegate :variant_names, :cache, :id, to: :experiment
24
-
25
- def initialize(options = {})
26
- @options = DEFAULT_OPTIONS.merge(options)
27
- end
27
+ delegate :cache, :id, to: :experiment
28
28
 
29
- def for(experiment)
29
+ def initialize(experiment, options = {})
30
30
  raise ArgumentError, 'you must provide an experiment instance' unless experiment.class <= Gitlab::Experiment
31
31
 
32
32
  @experiment = experiment
33
-
34
- self
33
+ @options = options
35
34
  end
36
35
 
37
36
  def enabled?
38
- require_experiment(__method__)
39
-
40
37
  true
41
38
  end
42
39
 
43
40
  def resolve
44
- require_experiment(__method__)
45
-
46
- return nil if @experiment.respond_to?(:experiment_group?) && !@experiment.experiment_group?
47
-
48
41
  validate! # allow the rollout strategy to validate itself
49
42
 
50
- assignment = execute_assigment
51
- assignment == :control ? nil : assignment # avoid caching control
43
+ assignment = execute_assignment
44
+ assignment == :control ? nil : assignment # avoid caching control by returning nil
52
45
  end
53
46
 
54
- protected
47
+ private
55
48
 
56
49
  def validate!
57
50
  # base is always valid
58
51
  end
59
52
 
60
- def execute_assigment
53
+ def execute_assignment
61
54
  behavior_names.first
62
55
  end
63
56
 
64
- private
65
-
66
- def require_experiment(method_name)
67
- return if @experiment.present?
68
-
69
- raise ArgumentError, "you need to call `for` with an experiment instance before chaining `#{method_name}`"
57
+ def behavior_names
58
+ experiment.behaviors.keys
70
59
  end
60
+ end
71
61
 
72
- def behavior_names
73
- options[:include_control] ? [:control] + variant_names : variant_names
62
+ Strategy = Struct.new(:klass, :options) do
63
+ def for(experiment)
64
+ klass.new(experiment, options)
74
65
  end
75
66
  end
76
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,21 @@ RSpec.configure do |config|
303
301
  config.include Gitlab::Experiment::RSpecHelpers
304
302
  config.include Gitlab::Experiment::Dsl
305
303
 
306
- config.before(:each, :experiment) 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
314
  config.include Gitlab::Experiment::RSpecMatchers, :experiment
315
- config.define_derived_metadata(file_path: Regexp.new('/spec/experiments/')) do |metadata|
316
- metadata[:type] = :experiment
315
+ config.include Gitlab::Experiment::RSpecMatchers, type: :experiment
316
+
317
+ config.define_derived_metadata(file_path: Regexp.new('spec/experiments/')) do |metadata|
318
+ metadata[:type] ||= :experiment
317
319
  end
318
320
 
319
321
  # We need to monkeypatch rspec-mocks because there's an issue around stubbing class methods that impacts us here.
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Gitlab
4
4
  class Experiment
5
- VERSION = '0.7.0'
5
+ VERSION = '0.8.0'
6
6
  end
7
7
  end
@@ -3,6 +3,8 @@
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'
7
+ require 'active_support/core_ext/string/inflections'
6
8
 
7
9
  require 'gitlab/experiment/errors'
8
10
  require 'gitlab/experiment/base_interface'
@@ -68,7 +70,7 @@ module Gitlab
68
70
  def default_rollout(rollout = nil, options = {})
69
71
  return @_rollout ||= Configuration.default_rollout if rollout.blank?
70
72
 
71
- @_rollout = Rollout.resolve(rollout).new(options)
73
+ @_rollout = Rollout.resolve(rollout, options)
72
74
  end
73
75
 
74
76
  # Class level accessor methods.
@@ -86,34 +88,15 @@ module Gitlab
86
88
  variant(:control, &block)
87
89
  end
88
90
 
89
- def candidate(name = nil, &block)
90
- if name.present?
91
- Configuration.deprecated(<<~MESSAGE, version: '0.7.0')
92
- passing name to `candidate` is deprecated and will be removed from {{release}} (instead use `variant(#{name.inspect})`)
93
- MESSAGE
94
- end
95
-
96
- variant(name || :candidate, &block)
91
+ def candidate(&block)
92
+ variant(:candidate, &block)
97
93
  end
98
94
 
99
- def variant(name = nil, &block)
100
- if block.present? # we know we're defining a variant block
101
- raise ArgumentError, 'missing variant name' if name.blank?
102
-
103
- return behaviors[name.to_s] = block
104
- end
105
-
106
- if name.present?
107
- Configuration.deprecated(<<~MESSAGE, version: '0.7.0')
108
- setting the variant using `variant` is deprecated and will be removed from {{release}} (instead use `assigned(#{name.inspect})`)
109
- MESSAGE
110
- else
111
- Configuration.deprecated(<<~MESSAGE, version: '0.7.0')
112
- getting the assigned variant using `variant` is deprecated and will be removed from {{release}} (instead use `assigned`)
113
- MESSAGE
114
- end
95
+ def variant(name, &block)
96
+ raise ArgumentError, 'name required' if name.blank?
97
+ raise ArgumentError, 'block required' unless block.present?
115
98
 
116
- assigned(name)
99
+ behaviors[name] = block
117
100
  end
118
101
 
119
102
  def context(value = nil)
@@ -125,9 +108,7 @@ module Gitlab
125
108
 
126
109
  def assigned(value = nil)
127
110
  @_assigned_variant_name = cache_variant(value) if value.present?
128
- if @_assigned_variant_name || @_resolving_variant
129
- return Variant.new(name: (@_assigned_variant_name || :unresolved).to_s)
130
- end
111
+ return Variant.new(name: @_assigned_variant_name || :unresolved) if @_assigned_variant_name || @_resolving_variant
131
112
 
132
113
  if enabled?
133
114
  @_resolving_variant = true
@@ -136,7 +117,7 @@ module Gitlab
136
117
 
137
118
  run_callbacks(segmentation_callback_chain) do
138
119
  @_assigned_variant_name ||= :control
139
- Variant.new(name: @_assigned_variant_name.to_s)
120
+ Variant.new(name: @_assigned_variant_name)
140
121
  end
141
122
  ensure
142
123
  @_resolving_variant = false
@@ -145,7 +126,7 @@ module Gitlab
145
126
  def rollout(rollout = nil, options = {})
146
127
  return @_rollout ||= self.class.default_rollout(nil, options).for(self) if rollout.blank?
147
128
 
148
- @_rollout = Rollout.resolve(rollout).new(options).for(self)
129
+ @_rollout = Rollout.resolve(rollout, options).for(self)
149
130
  end
150
131
 
151
132
  def exclude!
@@ -170,13 +151,6 @@ module Gitlab
170
151
  instance_exec(action, tracking_context(event_args).try(:compact) || {}, &Configuration.tracking_behavior)
171
152
  end
172
153
 
173
- def process_redirect_url(url)
174
- return unless Configuration.redirect_url_validator&.call(url)
175
-
176
- track('visited', url: url)
177
- url # return the url, which allows for mutation
178
- end
179
-
180
154
  def enabled?
181
155
  rollout.enabled?
182
156
  end
@@ -192,23 +166,11 @@ module Gitlab
192
166
  end
193
167
 
194
168
  def signature
195
- { variant: assigned.name, experiment: name }.merge(context.signature)
169
+ { variant: assigned.name.to_s, experiment: name }.merge(context.signature)
196
170
  end
197
171
 
198
- def key_for(source, seed = name)
199
- # TODO: Remove - deprecated in release 0.7.0
200
- if (block = Configuration.instance_variable_get(:@__context_hash_strategy))
201
- return instance_exec(source, seed, &block)
202
- end
203
-
204
- return source if source.is_a?(String)
205
-
206
- source = source.keys + source.values if source.is_a?(Hash)
207
-
208
- ingredients = Array(source).map { |v| identify(v) }
209
- ingredients.unshift(seed).unshift(Configuration.context_key_secret)
210
-
211
- Digest::SHA2.new(Configuration.context_key_bit_length).hexdigest(ingredients.join('|'))
172
+ def behaviors
173
+ @_behaviors ||= registered_behavior_callbacks
212
174
  end
213
175
 
214
176
  protected
@@ -217,23 +179,15 @@ module Gitlab
217
179
  (object.respond_to?(:to_global_id) ? object.to_global_id : object).to_s
218
180
  end
219
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
+
220
189
  def resolve_variant_name
221
- if respond_to?(:experiment_group?, true)
222
- # TODO: Remove - deprecated in release 0.7.0
223
- Configuration.deprecated(:experiment_group?, <<~MESSAGE, version: '0.7.0')
224
- instead put this logic into custom rollout strategies
225
- MESSAGE
226
-
227
- rollout.resolve if experiment_group?
228
- elsif (block = Configuration.instance_variable_get(:@__inclusion_resolver))
229
- # TODO: Remove - deprecated in release 0.7.0
230
- rollout.resolve if instance_exec(@_assigned_variant_name, &block)
231
- elsif (block = Configuration.instance_variable_get(:@__variant_resolver))
232
- # TODO: Remove - deprecated in release 0.6.5
233
- instance_exec(@_assigned_variant_name, &block)
234
- else
235
- rollout.resolve # this is the end result of all deprecations
236
- end
190
+ rollout.resolve
237
191
  end
238
192
 
239
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.0
4
+ version: 0.8.0
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: 2023-03-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -38,67 +38,52 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1.0'
41
- description:
41
+ description:
42
42
  email:
43
43
  - gitlab_rubygems@gitlab.com
44
44
  executables: []
45
45
  extensions: []
46
46
  extra_rdoc_files: []
47
47
  files:
48
- - lib/generators/test_unit
49
- - lib/generators/test_unit/experiment
50
- - lib/generators/test_unit/experiment/templates
51
- - lib/generators/test_unit/experiment/templates/experiment_test.rb.tt
52
- - lib/generators/test_unit/experiment/experiment_generator.rb
53
- - lib/generators/rspec
54
- - lib/generators/rspec/experiment
55
- - lib/generators/rspec/experiment/templates
56
- - lib/generators/rspec/experiment/templates/experiment_spec.rb.tt
57
- - lib/generators/rspec/experiment/experiment_generator.rb
58
- - lib/generators/gitlab
59
- - lib/generators/gitlab/experiment
48
+ - LICENSE.txt
49
+ - README.md
60
50
  - lib/generators/gitlab/experiment/USAGE
61
- - lib/generators/gitlab/experiment/templates
62
- - lib/generators/gitlab/experiment/templates/experiment.rb.tt
63
- - lib/generators/gitlab/experiment/install
64
- - lib/generators/gitlab/experiment/install/templates
51
+ - lib/generators/gitlab/experiment/experiment_generator.rb
52
+ - lib/generators/gitlab/experiment/install/install_generator.rb
53
+ - lib/generators/gitlab/experiment/install/templates/POST_INSTALL
65
54
  - lib/generators/gitlab/experiment/install/templates/application_experiment.rb.tt
66
55
  - lib/generators/gitlab/experiment/install/templates/initializer.rb.tt
67
- - lib/generators/gitlab/experiment/install/templates/POST_INSTALL
68
- - lib/generators/gitlab/experiment/install/install_generator.rb
69
- - lib/generators/gitlab/experiment/experiment_generator.rb
70
- - lib/gitlab/experiment
71
- - lib/gitlab/experiment/dsl.rb
72
- - lib/gitlab/experiment/rspec.rb
73
- - lib/gitlab/experiment/context.rb
74
- - lib/gitlab/experiment/nestable.rb
75
- - lib/gitlab/experiment/configuration.rb
76
- - lib/gitlab/experiment/rollout.rb
77
- - lib/gitlab/experiment/cache
78
- - lib/gitlab/experiment/cache/redis_hash_store.rb
79
- - lib/gitlab/experiment/engine.rb
56
+ - lib/generators/gitlab/experiment/templates/experiment.rb.tt
57
+ - lib/generators/rspec/experiment/experiment_generator.rb
58
+ - lib/generators/rspec/experiment/templates/experiment_spec.rb.tt
59
+ - lib/generators/test_unit/experiment/experiment_generator.rb
60
+ - lib/generators/test_unit/experiment/templates/experiment_test.rb.tt
61
+ - lib/gitlab/experiment.rb
80
62
  - lib/gitlab/experiment/base_interface.rb
81
- - lib/gitlab/experiment/middleware.rb
82
- - lib/gitlab/experiment/version.rb
63
+ - lib/gitlab/experiment/cache.rb
64
+ - lib/gitlab/experiment/cache/redis_hash_store.rb
65
+ - lib/gitlab/experiment/callbacks.rb
66
+ - lib/gitlab/experiment/configuration.rb
67
+ - lib/gitlab/experiment/context.rb
83
68
  - lib/gitlab/experiment/cookies.rb
69
+ - lib/gitlab/experiment/dsl.rb
70
+ - lib/gitlab/experiment/engine.rb
84
71
  - lib/gitlab/experiment/errors.rb
85
- - lib/gitlab/experiment/cache.rb
86
- - lib/gitlab/experiment/variant.rb
87
- - lib/gitlab/experiment/rollout
88
- - lib/gitlab/experiment/rollout/random.rb
72
+ - lib/gitlab/experiment/middleware.rb
73
+ - lib/gitlab/experiment/nestable.rb
74
+ - lib/gitlab/experiment/rollout.rb
89
75
  - lib/gitlab/experiment/rollout/percent.rb
76
+ - lib/gitlab/experiment/rollout/random.rb
90
77
  - lib/gitlab/experiment/rollout/round_robin.rb
91
- - lib/gitlab/experiment/callbacks.rb
92
- - lib/gitlab/experiment/test_behaviors
78
+ - lib/gitlab/experiment/rspec.rb
93
79
  - lib/gitlab/experiment/test_behaviors/trackable.rb
94
- - lib/gitlab/experiment.rb
95
- - LICENSE.txt
96
- - README.md
80
+ - lib/gitlab/experiment/variant.rb
81
+ - lib/gitlab/experiment/version.rb
97
82
  homepage: https://gitlab.com/gitlab-org/ruby/gems/gitlab-experiment
98
83
  licenses:
99
84
  - MIT
100
85
  metadata: {}
101
- post_install_message:
86
+ post_install_message:
102
87
  rdoc_options: []
103
88
  require_paths:
104
89
  - lib
@@ -113,8 +98,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
113
98
  - !ruby/object:Gem::Version
114
99
  version: '0'
115
100
  requirements: []
116
- rubygems_version: 3.1.6
117
- signing_key:
101
+ rubygems_version: 3.4.7
102
+ signing_key:
118
103
  specification_version: 4
119
104
  summary: GitLab experimentation library.
120
105
  test_files: []