gitlab-experiment 0.5.3 → 0.6.3

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: 1c4e4a4b19b67700aab0629cd615617c4452d8b4b145ef8ff85d12443994362b
4
- data.tar.gz: 802c31143890f2431a80ed3dcb247cbf46247ce276053e8e27a4d190822650df
3
+ metadata.gz: f89a48e8e15030981a25d71cfc86d9f92520e535b90381209861e698d47d646e
4
+ data.tar.gz: 80c92e4817efd0b9f9c273bd6392548ef03352140c9d8e2cc9ce13cee90fb5b2
5
5
  SHA512:
6
- metadata.gz: 86f86cd9dd01006be9668a2ceb4d60351e943487576d236e12b40eb9405e4255fdbbffb0c5811b17a4f1c8d2784b011eb20bc783e622f4f4f05fc6fb166f0df3
7
- data.tar.gz: bca224adeaade21b59077effd1faa0d6ca95607fe02f6c7735b8c9b79b7d5a08f76675549ddbc874ce63adbe39895d7b3f6f1fb71e908ae22a76c817a1d0f88d
6
+ metadata.gz: 9ff2aa5d59df1dac0d5c6efd5a592e98c5363ad59c209a46b84a722c231b674cdfaa7c9ca53eade02b180d99bce2c13c336fedc92cebd2f0bec1bb59c7b0f82a
7
+ data.tar.gz: 3e94dd388343cddab5eef06dd3e3bfa756725a690f45f2143ef03d6c56e5b19f4b47d5757fcfbc9cde9ae415f74ec838c764ac59254d385368a2ab76d9d97a7b
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
@@ -85,10 +92,10 @@ module Gitlab
85
92
  @resolving_variant = false
86
93
  end
87
94
 
88
- def rollout(rollout = nil)
89
- 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?
90
97
 
91
- @rollout = Rollout.resolve(rollout)
98
+ @rollout = Rollout.resolve(rollout).new(options)
92
99
  end
93
100
 
94
101
  def exclude!
@@ -97,10 +104,14 @@ module Gitlab
97
104
 
98
105
  def run(variant_name = nil)
99
106
  @result ||= super(variant(variant_name).name)
107
+ rescue Scientist::BehaviorMissing => e
108
+ raise Error, e
100
109
  end
101
110
 
102
111
  def publish(result)
103
112
  instance_exec(result, &Configuration.publishing_behavior)
113
+
114
+ (RequestStore.store[:published_gitlab_experiments] ||= {})[name] = signature.merge(excluded: excluded?)
104
115
  end
105
116
 
106
117
  def track(action, **event_args)
@@ -109,6 +120,13 @@ module Gitlab
109
120
  instance_exec(action, event_args, &Configuration.tracking_behavior)
110
121
  end
111
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
+
112
130
  def enabled?
113
131
  true
114
132
  end
@@ -131,12 +149,26 @@ module Gitlab
131
149
  { variant: variant.name, experiment: name }.merge(context.signature)
132
150
  end
133
151
 
134
- def key_for(hash)
135
- 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('|'))
136
164
  end
137
165
 
138
166
  protected
139
167
 
168
+ def identify(object)
169
+ (object.respond_to?(:to_global_id) ? object.to_global_id : object).to_s
170
+ end
171
+
140
172
  def segmentation_callback_chain
141
173
  return :segmentation_check if @variant_name.nil? && enabled? && !excluded?
142
174
 
@@ -144,7 +176,7 @@ module Gitlab
144
176
  end
145
177
 
146
178
  def resolve_variant_name
147
- rollout.new(self).execute if experiment_group?
179
+ rollout.rollout_for(self) if experiment_group?
148
180
  end
149
181
  end
150
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}"
@@ -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.clone.without(:script_name)
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.3'
5
+ VERSION = '0.6.3'
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.3
4
+ version: 0.6.3
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-06 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