gitlab-experiment 0.5.2 → 0.6.2

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: d4b1d80362166d1e86dd434fffe2a8e31676440f48b578e4bfcca4624fc819a8
4
- data.tar.gz: 4535076f1c77f3faefc92f59be23689a5412f462bda088b67a4fbbdb58c74254
3
+ metadata.gz: cd00eb06f769b8268e0e02e7d48b38dbef6be029c8f4d02b127e9f2b44f9dbc3
4
+ data.tar.gz: 7a3b9c5357489afd4b14bd6411536b1a2484c50f44934cc646b20abd1607ab0b
5
5
  SHA512:
6
- metadata.gz: d9d3445f7ee60be9d445760f103162861b1983c35ed6a9347e8703bc44c4bef2d7b154a62b2b05aa02c84471b8cfa95f91aa5521ee5743b9afa8ea5b4a4088eb
7
- data.tar.gz: cb49261fe14971ff76799bac943e5826b04cad84982b8d061cbd7b2843a0817854772f2b3e8e55600023aff9b5266c0ba562df50c47bb90703cb686c4f973ebd
6
+ metadata.gz: '08ea7e0b62c92e7674a72271e388dd104878ad2b9328ad016fba758762e244b51da1a089cec0d74e1db11fba4da3e2bc59524ef2839424295fa3c7307c73cb87'
7
+ data.tar.gz: 7ecfc7bbaa0803b67ccfd642c7c06baec43cde54b41cc768a7c1a1537e5c6cae6e13a355bdf342d4cdaebef15f5a1b99e76676c87612d857affb865bb543e07d
data/README.md CHANGED
@@ -423,6 +423,30 @@ Gitlab::Experiment.configure do |config|
423
423
  end
424
424
  ```
425
425
 
426
+ ### Middleware
427
+
428
+ There are times when you'll need to do link tracking in email templates, or markdown content -- or other places you won't be able to implement tracking. For these cases, gitlab-experiment comes with middleware that will redirect to a given URL while also tracking that the URL was visited.
429
+
430
+ In Rails this middleware is mounted automatically, with a base path of what's been configured for `mount_at`. If this path is empty the middleware won't be mounted at all.
431
+
432
+ Once mounted, the redirect URLs can be generated using the Rails route helpers. If not using Rails, mount the middleware and generate these URLs yourself.
433
+
434
+ ```ruby
435
+ Gitlab::Experiment.configure do |config|
436
+ config.mount_at = '/experiment'
437
+ end
438
+
439
+ ex = experiment(:example, foo: :bar)
440
+
441
+ # using rails path/url helpers
442
+ experiment_redirect_path(ex, 'https//docs.gitlab.com/') # => /experiment/example:[context_key]?https//docs.gitlab.com/
443
+
444
+ # manually
445
+ "#{Gitlab::Experiment.configure.mount_at}/#{ex.to_param}?https//docs.gitlab.com/"
446
+ ```
447
+
448
+ URLS that match the base path will be handled by the middleware and will redirect to the provided redirect path.
449
+
426
450
  ## Testing (rspec support)
427
451
 
428
452
  This gem comes with some rspec helpers and custom matchers. These are in flux at the time of writing.
@@ -18,8 +18,10 @@ Gitlab::Experiment.configure do |config|
18
18
  # The domain to use on cookies.
19
19
  #
20
20
  # When not set, it uses the current host. If you want to provide specific
21
- # hosts, you use `:all`, or provide an array like
22
- # `['www.gitlab.com', '.gitlab.com']`.
21
+ # hosts, you use `:all`, or provide an array.
22
+ #
23
+ # Examples:
24
+ # nil, :all, or ['www.gitlab.com', '.gitlab.com']
23
25
  config.cookie_domain = :all
24
26
 
25
27
  # The default rollout strategy that works for single and multi-variants.
@@ -28,8 +30,41 @@ Gitlab::Experiment.configure do |config|
28
30
  # experiment.
29
31
  #
30
32
  # Examples include:
31
- # Rollout::First, Rollout::Random, Rollout::RoundRobin
32
- config.default_rollout = Gitlab::Experiment::Rollout::First
33
+ # Rollout::Random, or Rollout::RoundRobin
34
+ config.default_rollout = Gitlab::Experiment::Rollout::Percent
35
+
36
+ # Secret seed used in generating context keys.
37
+ #
38
+ # Consider not using one that's shared with other systems, like Rails'
39
+ # SECRET_KEY_BASE. Generate a new secret and utilize that instead.
40
+ @context_key_secret = nil
41
+
42
+ # Bit length used by SHA2 in generating context keys.
43
+ #
44
+ # Using a higher bit length would require more computation time.
45
+ #
46
+ # Valid bit lengths:
47
+ # 256, 384, or 512.
48
+ @context_key_bit_length = 256
49
+
50
+ # The default base path that the middleware (or rails engine) will be
51
+ # mounted. Can be nil if you don't want anything to be mounted automatically.
52
+ #
53
+ # This enables a similar behavior to how links are instrumented in emails.
54
+ #
55
+ # Examples:
56
+ # '/-/experiment', '/redirect', nil
57
+ config.mount_at = '/experiment'
58
+
59
+ # When using the middleware, links can be instrumented and redirected
60
+ # elsewhere. This can be exploited to make a harmful url look innocuous or
61
+ # that it's a valid url on your domain. To avoid this, you can provide your
62
+ # own logic for what urls will be considered valid and redirected to.
63
+ #
64
+ # Expected to return a boolean value.
65
+ config.redirect_url_validator = lambda do |redirect_url|
66
+ true
67
+ end
33
68
 
34
69
  # Logic this project uses to determine inclusion in a given experiment.
35
70
  #
@@ -80,17 +115,4 @@ Gitlab::Experiment.configure do |config|
80
115
  #
81
116
  # Lograge::Event.log(experiment: name, result: result, signature: signature)
82
117
  end
83
-
84
- # Algorithm that consistently generates a hash key for a given hash map.
85
- #
86
- # Given a specific context hash map, we need to generate a consistent hash
87
- # key. The logic in here will be used for generating cache keys, and may also
88
- # be used when determining which variant may be presented.
89
- #
90
- # This block is executed within the scope of the experiment and so can access
91
- # experiment methods, like `name`, `context`, and `signature`.
92
- config.context_hash_strategy = lambda do |context|
93
- values = context.values.map { |v| (v.respond_to?(:to_global_id) ? v.to_global_id : v).to_s }
94
- Digest::MD5.hexdigest((context.keys + values).join('|'))
95
- end
96
118
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'scientist'
4
+ require 'request_store'
4
5
  require 'active_support/callbacks'
5
6
  require 'active_support/cache'
6
7
  require 'active_support/concern'
@@ -8,6 +9,7 @@ require 'active_support/core_ext/object/blank'
8
9
  require 'active_support/core_ext/string/inflections'
9
10
  require 'active_support/core_ext/module/delegation'
10
11
 
12
+ require 'gitlab/experiment/errors'
11
13
  require 'gitlab/experiment/base_interface'
12
14
  require 'gitlab/experiment/cache'
13
15
  require 'gitlab/experiment/callbacks'
@@ -16,6 +18,7 @@ require 'gitlab/experiment/configuration'
16
18
  require 'gitlab/experiment/cookies'
17
19
  require 'gitlab/experiment/context'
18
20
  require 'gitlab/experiment/dsl'
21
+ require 'gitlab/experiment/middleware'
19
22
  require 'gitlab/experiment/variant'
20
23
  require 'gitlab/experiment/version'
21
24
  require 'gitlab/experiment/engine' if defined?(Rails::Engine)
@@ -27,10 +30,10 @@ module Gitlab
27
30
  include Callbacks
28
31
 
29
32
  class << self
30
- def default_rollout(rollout = nil)
33
+ def default_rollout(rollout = nil, options = {})
31
34
  return @rollout ||= Configuration.default_rollout if rollout.blank?
32
35
 
33
- @rollout = Rollout.resolve(rollout)
36
+ @rollout = Rollout.resolve(rollout).new(options)
34
37
  end
35
38
 
36
39
  def exclude(*filter_list, **options, &block)
@@ -44,6 +47,10 @@ module Gitlab
44
47
  target.variant(variant) if target.instance_variable_get(:@variant_name).nil? && callback.call(target, nil)
45
48
  end
46
49
  end
50
+
51
+ def published_experiments
52
+ RequestStore.store[:published_gitlab_experiments] || {}
53
+ end
47
54
  end
48
55
 
49
56
  def name
@@ -74,9 +81,7 @@ module Gitlab
74
81
 
75
82
  if enabled?
76
83
  @resolving_variant = true
77
- @variant_name ||= :control if excluded?
78
- result = cache_variant(@variant_name) { resolve_variant_name }
79
- @variant_name = result.to_sym if result.present?
84
+ @variant_name = cached_variant_resolver(@variant_name)
80
85
  end
81
86
 
82
87
  run_callbacks(segmentation_callback_chain) do
@@ -87,10 +92,10 @@ module Gitlab
87
92
  @resolving_variant = false
88
93
  end
89
94
 
90
- def rollout(rollout = nil)
91
- return @rollout ||= self.class.default_rollout if rollout.blank?
95
+ def rollout(rollout = nil, options = {})
96
+ return @rollout ||= self.class.default_rollout(nil, options) if rollout.blank?
92
97
 
93
- @rollout = Rollout.resolve(rollout)
98
+ @rollout = Rollout.resolve(rollout).new(options)
94
99
  end
95
100
 
96
101
  def exclude!
@@ -99,10 +104,14 @@ module Gitlab
99
104
 
100
105
  def run(variant_name = nil)
101
106
  @result ||= super(variant(variant_name).name)
107
+ rescue Scientist::BehaviorMissing => e
108
+ raise Error, e
102
109
  end
103
110
 
104
111
  def publish(result)
105
112
  instance_exec(result, &Configuration.publishing_behavior)
113
+
114
+ (RequestStore.store[:published_gitlab_experiments] ||= {})[name] = signature.merge(excluded: excluded?)
106
115
  end
107
116
 
108
117
  def track(action, **event_args)
@@ -111,6 +120,13 @@ module Gitlab
111
120
  instance_exec(action, event_args, &Configuration.tracking_behavior)
112
121
  end
113
122
 
123
+ def process_redirect_url(url)
124
+ return unless Configuration.redirect_url_validator&.call(url)
125
+
126
+ track('visited', url: url)
127
+ url # return the url, which allows for mutation
128
+ end
129
+
114
130
  def enabled?
115
131
  true
116
132
  end
@@ -133,12 +149,26 @@ module Gitlab
133
149
  { variant: variant.name, experiment: name }.merge(context.signature)
134
150
  end
135
151
 
136
- def key_for(hash)
137
- instance_exec(hash, &Configuration.context_hash_strategy)
152
+ def key_for(source, seed = name)
153
+ # TODO: Added deprecation in release 0.6.0
154
+ if (block = Configuration.instance_variable_get(:@__context_hash_strategy))
155
+ return instance_exec(source, seed, &block)
156
+ end
157
+
158
+ source = source.keys + source.values if source.is_a?(Hash)
159
+
160
+ ingredients = Array(source).map { |v| identify(v) }
161
+ ingredients.unshift(seed).unshift(Configuration.context_key_secret)
162
+
163
+ Digest::SHA2.new(Configuration.context_key_bit_length).hexdigest(ingredients.join('|'))
138
164
  end
139
165
 
140
166
  protected
141
167
 
168
+ def identify(object)
169
+ (object.respond_to?(:to_global_id) ? object.to_global_id : object).to_s
170
+ end
171
+
142
172
  def segmentation_callback_chain
143
173
  return :segmentation_check if @variant_name.nil? && enabled? && !excluded?
144
174
 
@@ -146,7 +176,7 @@ module Gitlab
146
176
  end
147
177
 
148
178
  def resolve_variant_name
149
- rollout.new(self).execute if experiment_group?
179
+ rollout.rollout_for(self) if experiment_group?
150
180
  end
151
181
  end
152
182
  end
@@ -26,6 +26,11 @@ module Gitlab
26
26
 
27
27
  experiment_name(name).classify.safe_constantize || Configuration.base_class.constantize
28
28
  end
29
+
30
+ def from_param(id)
31
+ %r{/?(?<name>.*):(?<key>.*)$} =~ id
32
+ constantize(name).new(name).tap { |e| e.context.key(key) }
33
+ end
29
34
  end
30
35
 
31
36
  def initialize(name = nil, variant_name = nil, **context)
@@ -45,9 +50,10 @@ module Gitlab
45
50
  end
46
51
 
47
52
  def id
48
- "#{name}:#{key_for(context.value)}"
53
+ "#{name}:#{context.key}"
49
54
  end
50
55
  alias_method :session_id, :id
56
+ alias_method :to_param, :id
51
57
 
52
58
  def flipper_id
53
59
  "Experiment;#{id}"
@@ -72,6 +78,13 @@ module Gitlab
72
78
  false
73
79
  end
74
80
 
81
+ def cached_variant_resolver(provided_variant)
82
+ return :control if excluded?
83
+
84
+ result = cache_variant(provided_variant) { resolve_variant_name }
85
+ result.to_sym if result.present?
86
+ end
87
+
75
88
  def generate_result(variant_name)
76
89
  observation = Scientist::Observation.new(variant_name, self, &behaviors[variant_name])
77
90
  Scientist::Result.new(self, [observation], observation)
@@ -26,18 +26,32 @@ module Gitlab
26
26
  # The domain to use on cookies.
27
27
  @cookie_domain = :all
28
28
 
29
- # The default rollout strategy that works for single and multi-variants.
30
- @default_rollout = Rollout::First
29
+ # The default rollout strategy only works for single variant experiments.
30
+ # It's expected that you use a more advanced rollout for multiple variant
31
+ # experiments.
32
+ @default_rollout = Rollout::Base.new
33
+
34
+ # Secret seed used in generating context keys.
35
+ @context_key_secret = nil
36
+
37
+ # Bit length used by SHA2 in generating context keys - (256, 384 or 512.)
38
+ @context_key_bit_length = 256
39
+
40
+ # The default base path that the middleware (or rails engine) will be
41
+ # mounted.
42
+ @mount_at = nil
43
+
44
+ # The middleware won't redirect to urls that aren't considered valid.
45
+ # Expected to return a boolean value.
46
+ @redirect_url_validator = ->(_redirect_url) { true }
31
47
 
32
48
  # Logic this project uses to determine inclusion in a given experiment.
33
49
  # Expected to return a boolean value.
34
- @inclusion_resolver = lambda do |requested_variant|
35
- false
36
- end
50
+ @inclusion_resolver = ->(_requested_variant) { false }
37
51
 
38
52
  # Tracking behavior can be implemented to link an event to an experiment.
39
53
  @tracking_behavior = lambda do |event, args|
40
- Configuration.logger.info "Gitlab::Experiment[#{name}] #{event}: #{args.merge(signature: signature)}"
54
+ Configuration.logger.info("#{self.class.name}[#{name}] #{event}: #{args.merge(signature: signature)}")
41
55
  end
42
56
 
43
57
  # Called at the end of every experiment run, with the result.
@@ -45,23 +59,24 @@ module Gitlab
45
59
  track(:assignment)
46
60
  end
47
61
 
48
- # Algorithm that consistently generates a hash key for a given hash map.
49
- @context_hash_strategy = lambda do |hash_map|
50
- values = hash_map.values.map { |v| (v.respond_to?(:to_global_id) ? v.to_global_id : v).to_s }
51
- Digest::MD5.hexdigest(([name] + hash_map.keys + values).join('|'))
52
- end
53
-
54
62
  class << self
63
+ # TODO: Added deprecation in release 0.6.0
64
+ def context_hash_strategy=(block)
65
+ ActiveSupport::Deprecation.warn('context_hash_strategy has been deprecated, instead configure' \
66
+ ' `context_key_secret` and `context_key_bit_length`.')
67
+ @__context_hash_strategy = block
68
+ end
69
+
55
70
  # TODO: Added deprecation in release 0.5.0
56
71
  def variant_resolver
57
72
  ActiveSupport::Deprecation.warn('variant_resolver is deprecated, instead use `inclusion_resolver` with a' \
58
- 'block that returns a boolean.')
73
+ ' block that returns a boolean.')
59
74
  @inclusion_resolver
60
75
  end
61
76
 
62
77
  def variant_resolver=(block)
63
78
  ActiveSupport::Deprecation.warn('variant_resolver is deprecated, instead use `inclusion_resolver` with a' \
64
- 'block that returns a boolean.')
79
+ ' block that returns a boolean.')
65
80
  @inclusion_resolver = block
66
81
  end
67
82
 
@@ -71,11 +86,14 @@ module Gitlab
71
86
  :base_class,
72
87
  :cache,
73
88
  :cookie_domain,
89
+ :context_key_secret,
90
+ :context_key_bit_length,
91
+ :mount_at,
74
92
  :default_rollout,
93
+ :redirect_url_validator,
75
94
  :inclusion_resolver,
76
95
  :tracking_behavior,
77
- :publishing_behavior,
78
- :context_hash_strategy
96
+ :publishing_behavior
79
97
  )
80
98
  end
81
99
  end
@@ -31,6 +31,12 @@ module Gitlab
31
31
  @value.merge!(process_migrations(value))
32
32
  end
33
33
 
34
+ def key(key = nil)
35
+ return @key || @experiment.key_for(value) if key.nil?
36
+
37
+ @key = key
38
+ end
39
+
34
40
  def trackable?
35
41
  !(@request && @request.headers['DNT'].to_s.match?(DNT_REGEXP))
36
42
  end
@@ -41,7 +47,7 @@ module Gitlab
41
47
  end
42
48
 
43
49
  def signature
44
- @signature ||= { key: @experiment.key_for(@value), migration_keys: migration_keys }.compact
50
+ @signature ||= { key: key, migration_keys: migration_keys }.compact
45
51
  end
46
52
 
47
53
  def method_missing(method_name, *)
@@ -3,6 +3,10 @@
3
3
  module Gitlab
4
4
  class Experiment
5
5
  module Dsl
6
+ def self.include_in(klass, with_helper: false)
7
+ klass.include(self).tap { |base| base.helper_method(:experiment) if with_helper }
8
+ end
9
+
6
10
  def experiment(name, variant_name = nil, **context, &block)
7
11
  raise ArgumentError, 'name is required' if name.nil?
8
12
 
@@ -1,21 +1,43 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_model'
4
+
3
5
  module Gitlab
4
6
  class Experiment
7
+ include ActiveModel::Model
8
+
9
+ # used for generating routes
10
+ def self.model_name
11
+ ActiveModel::Name.new(self, Gitlab)
12
+ end
13
+
5
14
  class Engine < ::Rails::Engine
6
- def self.include_dsl
7
- if defined?(ActionController)
8
- ActionController::Base.include(Dsl)
9
- ActionController::Base.helper_method(:experiment)
10
- end
15
+ isolate_namespace Experiment
16
+
17
+ initializer('gitlab_experiment.include_dsl') { include_dsl }
18
+ initializer('gitlab_experiment.mount_engine') { |app| mount_engine(app, Configuration.mount_at) }
11
19
 
12
- return unless defined?(ActionMailer)
20
+ private
13
21
 
14
- ActionMailer::Base.include(Dsl)
15
- ActionMailer::Base.helper_method(:experiment)
22
+ def include_dsl
23
+ Dsl.include_in(ActionController::Base, with_helper: true) if defined?(ActionController)
24
+ Dsl.include_in(ActionMailer::Base, with_helper: true) if defined?(ActionMailer)
16
25
  end
17
26
 
18
- config.after_initialize { include_dsl }
27
+ def mount_engine(app, mount_at)
28
+ return if mount_at.blank?
29
+
30
+ engine = routes do
31
+ default_url_options app.routes.default_url_options
32
+ resources :experiments, path: '/', only: :show
33
+ end
34
+
35
+ app.config.middleware.use(Middleware, mount_at)
36
+ app.routes.append do
37
+ mount Engine, at: mount_at, as: :experiment_engine
38
+ direct(:experiment_redirect) { |ex, url:| "#{engine.url_helpers.experiment_url(ex)}?#{url}" }
39
+ end
40
+ end
19
41
  end
20
42
  end
21
43
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gitlab
4
+ class Experiment
5
+ Error = Class.new(StandardError)
6
+ InvalidRolloutRules = Class.new(Error)
7
+ end
8
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gitlab
4
+ class Experiment
5
+ class Middleware
6
+ def self.redirect(id, url)
7
+ raise Error, 'no url to redirect to' if url.blank?
8
+
9
+ experiment = Gitlab::Experiment.from_param(id)
10
+ [303, { 'Location' => experiment.process_redirect_url(url) || raise(Error, 'not redirecting') }, []]
11
+ end
12
+
13
+ def initialize(app, base_path)
14
+ @app = app
15
+ @matcher = %r{^#{base_path}/(?<id>.+)}
16
+ end
17
+
18
+ def call(env)
19
+ return @app.call(env) if env['REQUEST_METHOD'] != 'GET' || (match = @matcher.match(env['PATH_INFO'])).nil?
20
+
21
+ Middleware.redirect(match[:id], env['QUERY_STRING'])
22
+ rescue Error
23
+ @app.call(env)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -3,7 +3,7 @@
3
3
  module Gitlab
4
4
  class Experiment
5
5
  module Rollout
6
- autoload :First, 'gitlab/experiment/rollout/first.rb' # default strategy
6
+ autoload :Percent, 'gitlab/experiment/rollout/percent.rb'
7
7
  autoload :Random, 'gitlab/experiment/rollout/random.rb'
8
8
  autoload :RoundRobin, 'gitlab/experiment/rollout/round_robin.rb'
9
9
 
@@ -16,10 +16,25 @@ module Gitlab
16
16
  class Base
17
17
  attr_reader :experiment
18
18
 
19
- delegate :variant_names, :cache, to: :experiment
19
+ delegate :variant_names, :cache, :id, to: :experiment
20
20
 
21
- def initialize(experiment)
21
+ def initialize(options = {})
22
+ @options = options
23
+ # validate! # we want to validate here, but we can't yet
24
+ end
25
+
26
+ def rollout_for(experiment)
22
27
  @experiment = experiment
28
+ validate! # until we have variant registration we can only validate here
29
+ execute
30
+ end
31
+
32
+ def validate!
33
+ # base is always valid
34
+ end
35
+
36
+ def execute
37
+ variant_names.first
23
38
  end
24
39
  end
25
40
  end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zlib'
4
+
5
+ module Gitlab
6
+ class Experiment
7
+ module Rollout
8
+ class Percent < Base
9
+ def execute
10
+ crc = normalized_id
11
+ total = 0
12
+
13
+ case distribution_rules
14
+ # run through the rules until finding an acceptable one
15
+ when Array then variant_names[distribution_rules.find_index { |percent| crc % 100 <= total += percent }]
16
+ # run through the variant names until finding an acceptable one
17
+ when Hash then distribution_rules.find { |_, percent| crc % 100 <= total += percent }.first
18
+ # when there are no rules, assume even distribution
19
+ else variant_names[crc % variant_names.length]
20
+ end
21
+ end
22
+
23
+ def validate!
24
+ case distribution_rules
25
+ when nil then nil
26
+ when Array, Hash
27
+ if distribution_rules.length != variant_names.length
28
+ raise InvalidRolloutRules, "the distribution rules don't match the number of variants defined"
29
+ end
30
+ else
31
+ raise InvalidRolloutRules, 'unknown distribution options type'
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def normalized_id
38
+ Zlib.crc32(id, nil)
39
+ end
40
+
41
+ def distribution_rules
42
+ @options[:distribution]
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -82,7 +82,7 @@ module Gitlab
82
82
  extend RSpec::Matchers::DSL
83
83
 
84
84
  def require_experiment(experiment, matcher_name, classes: false)
85
- klass = experiment.class == Class ? experiment : experiment.class
85
+ klass = experiment.instance_of?(Class) ? experiment : experiment.class
86
86
  unless klass <= Gitlab::Experiment
87
87
  raise(
88
88
  ArgumentError,
@@ -232,6 +232,10 @@ RSpec.configure do |config|
232
232
  config.include Gitlab::Experiment::RSpecHelpers
233
233
  config.include Gitlab::Experiment::Dsl
234
234
 
235
+ config.before(:each, :experiment) do
236
+ RequestStore.clear!
237
+ end
238
+
235
239
  config.include Gitlab::Experiment::RSpecMatchers, :experiment
236
240
  config.define_derived_metadata(file_path: Regexp.new('/spec/experiments/')) do |metadata|
237
241
  metadata[:type] = :experiment
@@ -2,6 +2,10 @@
2
2
 
3
3
  module Gitlab
4
4
  class Experiment
5
- Variant = Struct.new(:name, :payload, keyword_init: true)
5
+ Variant = Struct.new(:name, :payload, keyword_init: true) do
6
+ def group
7
+ name == 'control' ? :control : :experiment
8
+ end
9
+ end
6
10
  end
7
11
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Gitlab
4
4
  class Experiment
5
- VERSION = '0.5.2'
5
+ VERSION = '0.6.2'
6
6
  end
7
7
  end
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.5.2
4
+ version: 0.6.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitLab
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-04-02 00:00:00.000000000 Z
11
+ date:
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '3.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: request_store
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '1.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '1.0'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: scientist
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -51,36 +65,52 @@ executables: []
51
65
  extensions: []
52
66
  extra_rdoc_files: []
53
67
  files:
54
- - LICENSE.txt
55
- - README.md
56
- - lib/generators/gitlab/experiment/USAGE
57
- - lib/generators/gitlab/experiment/experiment_generator.rb
68
+ - lib/generators/gitlab
69
+ - lib/generators/gitlab/experiment
70
+ - lib/generators/gitlab/experiment/install
58
71
  - lib/generators/gitlab/experiment/install/install_generator.rb
59
- - lib/generators/gitlab/experiment/install/templates/POST_INSTALL
72
+ - lib/generators/gitlab/experiment/install/templates
60
73
  - lib/generators/gitlab/experiment/install/templates/application_experiment.rb.tt
61
74
  - lib/generators/gitlab/experiment/install/templates/initializer.rb.tt
75
+ - lib/generators/gitlab/experiment/install/templates/POST_INSTALL
76
+ - lib/generators/gitlab/experiment/USAGE
77
+ - lib/generators/gitlab/experiment/experiment_generator.rb
78
+ - lib/generators/gitlab/experiment/templates
62
79
  - lib/generators/gitlab/experiment/templates/experiment.rb.tt
63
- - lib/generators/rspec/experiment/experiment_generator.rb
64
- - lib/generators/rspec/experiment/templates/experiment_spec.rb.tt
80
+ - lib/generators/test_unit
81
+ - lib/generators/test_unit/experiment
65
82
  - lib/generators/test_unit/experiment/experiment_generator.rb
83
+ - lib/generators/test_unit/experiment/templates
66
84
  - lib/generators/test_unit/experiment/templates/experiment_test.rb.tt
85
+ - lib/generators/rspec
86
+ - lib/generators/rspec/experiment
87
+ - lib/generators/rspec/experiment/experiment_generator.rb
88
+ - lib/generators/rspec/experiment/templates
89
+ - lib/generators/rspec/experiment/templates/experiment_spec.rb.tt
67
90
  - lib/gitlab/experiment.rb
68
- - lib/gitlab/experiment/base_interface.rb
69
- - lib/gitlab/experiment/cache.rb
91
+ - lib/gitlab/experiment
92
+ - lib/gitlab/experiment/variant.rb
93
+ - lib/gitlab/experiment/middleware.rb
94
+ - lib/gitlab/experiment/cache
70
95
  - lib/gitlab/experiment/cache/redis_hash_store.rb
96
+ - lib/gitlab/experiment/errors.rb
71
97
  - lib/gitlab/experiment/callbacks.rb
72
- - lib/gitlab/experiment/configuration.rb
98
+ - lib/gitlab/experiment/rollout.rb
99
+ - lib/gitlab/experiment/base_interface.rb
73
100
  - lib/gitlab/experiment/context.rb
74
- - lib/gitlab/experiment/cookies.rb
75
- - lib/gitlab/experiment/dsl.rb
76
101
  - lib/gitlab/experiment/engine.rb
77
- - lib/gitlab/experiment/rollout.rb
78
- - lib/gitlab/experiment/rollout/first.rb
102
+ - lib/gitlab/experiment/rspec.rb
103
+ - lib/gitlab/experiment/rollout
79
104
  - lib/gitlab/experiment/rollout/random.rb
80
105
  - lib/gitlab/experiment/rollout/round_robin.rb
81
- - lib/gitlab/experiment/rspec.rb
82
- - lib/gitlab/experiment/variant.rb
106
+ - lib/gitlab/experiment/rollout/percent.rb
107
+ - lib/gitlab/experiment/cache.rb
83
108
  - lib/gitlab/experiment/version.rb
109
+ - lib/gitlab/experiment/cookies.rb
110
+ - lib/gitlab/experiment/configuration.rb
111
+ - lib/gitlab/experiment/dsl.rb
112
+ - LICENSE.txt
113
+ - README.md
84
114
  homepage: https://gitlab.com/gitlab-org/gitlab-experiment
85
115
  licenses:
86
116
  - MIT
@@ -1,16 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Gitlab
4
- class Experiment
5
- module Rollout
6
- class First < Base
7
- # This rollout strategy just picks the first variant name. It's the
8
- # default resolver as it assumes a single variant. You should consider
9
- # using a more advanced rollout if you have multiple variants.
10
- def execute
11
- variant_names.first
12
- end
13
- end
14
- end
15
- end
16
- end