gitlab-experiment 0.5.4 → 0.6.4

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: 9d7bc7a1946d06b5188ccfe761342209779f75cf635d894bb8743519ff2830dc
4
- data.tar.gz: 7a5c347f8b58e5caa2fe814ce013dbf1ca71f6e017148528a0ff2ca74bd562de
3
+ metadata.gz: ed3c0028563936ce8a3fbdfd1de9d64e499102c4ec71a21946b63c56cc2bebd5
4
+ data.tar.gz: 43389ef9b7b2a4b3e60b82b5ffefbe3635739cb9fafec21dd5e9d59586f942c2
5
5
  SHA512:
6
- metadata.gz: '01927d9db97ad4b087d445db642aa39615faa68322e2bd0e0b82ae99441478bae3f1939b94956218121fe52d01f432836ba272d0b1ff052537c80f0d0ff107b3'
7
- data.tar.gz: f53702ea5bb97fe6c73427850534b9e156744336a37029dce2b96926b70b7a5c4959708ac626f6ac0435125ad4ffe62f2ba9fd528f756af811c6ff66abc47846
6
+ metadata.gz: 30d0988d199f34eb60e3d56d8e7aec3a00222a5de482410c0294e15ce37a2a9dccbb2fb35b372c90b4425879938bec0d7e5b0ffadf5d842ee31abb0ea673aaeb
7
+ data.tar.gz: 3665443e1d937b77e12debaf3195423b3fad171b651573e58a328dfb5c1f618eb9a282eedcfde68376ae0e7207837b74ae60e4fda616c046caa329b4cc681e68
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
@@ -9,6 +9,7 @@ require 'active_support/core_ext/object/blank'
9
9
  require 'active_support/core_ext/string/inflections'
10
10
  require 'active_support/core_ext/module/delegation'
11
11
 
12
+ require 'gitlab/experiment/errors'
12
13
  require 'gitlab/experiment/base_interface'
13
14
  require 'gitlab/experiment/cache'
14
15
  require 'gitlab/experiment/callbacks'
@@ -17,6 +18,7 @@ require 'gitlab/experiment/configuration'
17
18
  require 'gitlab/experiment/cookies'
18
19
  require 'gitlab/experiment/context'
19
20
  require 'gitlab/experiment/dsl'
21
+ require 'gitlab/experiment/middleware'
20
22
  require 'gitlab/experiment/variant'
21
23
  require 'gitlab/experiment/version'
22
24
  require 'gitlab/experiment/engine' if defined?(Rails::Engine)
@@ -102,6 +104,8 @@ module Gitlab
102
104
 
103
105
  def run(variant_name = nil)
104
106
  @result ||= super(variant(variant_name).name)
107
+ rescue Scientist::BehaviorMissing => e
108
+ raise Error, e
105
109
  end
106
110
 
107
111
  def publish(result)
@@ -116,6 +120,13 @@ module Gitlab
116
120
  instance_exec(action, event_args, &Configuration.tracking_behavior)
117
121
  end
118
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
+
119
130
  def enabled?
120
131
  true
121
132
  end
@@ -139,11 +150,25 @@ module Gitlab
139
150
  end
140
151
 
141
152
  def key_for(source, seed = name)
142
- instance_exec(source, seed, &Configuration.context_hash_strategy)
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('|'))
143
164
  end
144
165
 
145
166
  protected
146
167
 
168
+ def identify(object)
169
+ (object.respond_to?(:to_global_id) ? object.to_global_id : object).to_s
170
+ end
171
+
147
172
  def segmentation_callback_chain
148
173
  return :segmentation_check if @variant_name.nil? && enabled? && !excluded?
149
174
 
@@ -26,6 +26,12 @@ 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
+ name = CGI.unescape(name) if name
33
+ constantize(name).new(name).tap { |e| e.context.key(key) }
34
+ end
29
35
  end
30
36
 
31
37
  def initialize(name = nil, variant_name = nil, **context)
@@ -45,9 +51,10 @@ module Gitlab
45
51
  end
46
52
 
47
53
  def id
48
- "#{name}:#{key_for(context.value)}"
54
+ "#{name}:#{context.key}"
49
55
  end
50
56
  alias_method :session_id, :id
57
+ alias_method :to_param, :id
51
58
 
52
59
  def flipper_id
53
60
  "Experiment;#{id}"
@@ -31,15 +31,27 @@ module Gitlab
31
31
  # experiments.
32
32
  @default_rollout = Rollout::Base.new
33
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 }
47
+
34
48
  # Logic this project uses to determine inclusion in a given experiment.
35
49
  # Expected to return a boolean value.
36
- @inclusion_resolver = lambda do |requested_variant|
37
- false
38
- end
50
+ @inclusion_resolver = ->(_requested_variant) { false }
39
51
 
40
52
  # Tracking behavior can be implemented to link an event to an experiment.
41
53
  @tracking_behavior = lambda do |event, args|
42
- Configuration.logger.info "Gitlab::Experiment[#{name}] #{event}: #{args.merge(signature: signature)}"
54
+ Configuration.logger.info("#{self.class.name}[#{name}] #{event}: #{args.merge(signature: signature)}")
43
55
  end
44
56
 
45
57
  # Called at the end of every experiment run, with the result.
@@ -47,24 +59,24 @@ module Gitlab
47
59
  track(:assignment)
48
60
  end
49
61
 
50
- # Algorithm that consistently generates a hash key for a given source.
51
- @context_hash_strategy = lambda do |source, seed|
52
- source = source.keys + source.values if source.is_a?(Hash)
53
- data = Array(source).map { |v| (v.respond_to?(:to_global_id) ? v.to_global_id : v).to_s }
54
- Digest::MD5.hexdigest(data.unshift(seed).join('|'))
55
- end
56
-
57
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
+
58
70
  # TODO: Added deprecation in release 0.5.0
59
71
  def variant_resolver
60
72
  ActiveSupport::Deprecation.warn('variant_resolver is deprecated, instead use `inclusion_resolver` with a' \
61
- 'block that returns a boolean.')
73
+ ' block that returns a boolean.')
62
74
  @inclusion_resolver
63
75
  end
64
76
 
65
77
  def variant_resolver=(block)
66
78
  ActiveSupport::Deprecation.warn('variant_resolver is deprecated, instead use `inclusion_resolver` with a' \
67
- 'block that returns a boolean.')
79
+ ' block that returns a boolean.')
68
80
  @inclusion_resolver = block
69
81
  end
70
82
 
@@ -74,11 +86,14 @@ module Gitlab
74
86
  :base_class,
75
87
  :cache,
76
88
  :cookie_domain,
89
+ :context_key_secret,
90
+ :context_key_bit_length,
91
+ :mount_at,
77
92
  :default_rollout,
93
+ :redirect_url_validator,
78
94
  :inclusion_resolver,
79
95
  :tracking_behavior,
80
- :publishing_behavior,
81
- :context_hash_strategy
96
+ :publishing_behavior
82
97
  )
83
98
  end
84
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, *)
@@ -55,16 +61,18 @@ module Gitlab
55
61
  private
56
62
 
57
63
  def process_migrations(value)
58
- add_migration(value.delete(:migrated_from))
59
- add_migration(value.delete(:migrated_with), merge: true)
64
+ add_unmerged_migration(value.delete(:migrated_from))
65
+ add_merged_migration(value.delete(:migrated_with))
60
66
 
61
67
  migrate_cookie(value, "#{@experiment.name}_id")
62
68
  end
63
69
 
64
- def add_migration(value, merge: false)
65
- return unless value.is_a?(Hash)
70
+ def add_unmerged_migration(value = {})
71
+ @migrations[:unmerged] << value if value.is_a?(Hash)
72
+ end
66
73
 
67
- @migrations[merge ? :merged : :unmerged] << value
74
+ def add_merged_migration(value = {})
75
+ @migrations[:merged] << value if value.is_a?(Hash)
68
76
  end
69
77
 
70
78
  def migration_keys
@@ -23,7 +23,7 @@ module Gitlab
23
23
  return hash if cookie.to_s.empty?
24
24
  return hash.merge(key => cookie) if hash[key].nil?
25
25
 
26
- add_migration(key => cookie)
26
+ add_unmerged_migration(key => cookie)
27
27
  cookie_jar.delete(cookie_name, domain: domain)
28
28
 
29
29
  hash
@@ -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,46 @@
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) do |ex, options|
39
+ url = options[:url]
40
+ "#{engine.url_helpers.experiment_url(ex)}?#{url}"
41
+ end
42
+ end
43
+ end
19
44
  end
20
45
  end
21
46
  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,6 +3,7 @@
3
3
  module Gitlab
4
4
  class Experiment
5
5
  module Rollout
6
+ autoload :Percent, 'gitlab/experiment/rollout/percent.rb'
6
7
  autoload :Random, 'gitlab/experiment/rollout/random.rb'
7
8
  autoload :RoundRobin, 'gitlab/experiment/rollout/round_robin.rb'
8
9
 
@@ -15,17 +16,23 @@ module Gitlab
15
16
  class Base
16
17
  attr_reader :experiment
17
18
 
18
- delegate :variant_names, :cache, to: :experiment
19
+ delegate :variant_names, :cache, :id, to: :experiment
19
20
 
20
21
  def initialize(options = {})
21
22
  @options = options
23
+ # validate! # we want to validate here, but we can't yet
22
24
  end
23
25
 
24
26
  def rollout_for(experiment)
25
27
  @experiment = experiment
28
+ validate! # until we have variant registration we can only validate here
26
29
  execute
27
30
  end
28
31
 
32
+ def validate!
33
+ # base is always valid
34
+ end
35
+
29
36
  def execute
30
37
  variant_names.first
31
38
  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,
@@ -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.4'
5
+ VERSION = '0.6.4'
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.4
4
+ version: 0.6.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitLab
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-05-05 00:00:00.000000000 Z
11
+ date:
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -65,35 +65,52 @@ executables: []
65
65
  extensions: []
66
66
  extra_rdoc_files: []
67
67
  files:
68
- - LICENSE.txt
69
- - README.md
70
- - lib/generators/gitlab/experiment/USAGE
71
- - lib/generators/gitlab/experiment/experiment_generator.rb
68
+ - lib/generators/gitlab
69
+ - lib/generators/gitlab/experiment
70
+ - lib/generators/gitlab/experiment/install
72
71
  - lib/generators/gitlab/experiment/install/install_generator.rb
73
- - lib/generators/gitlab/experiment/install/templates/POST_INSTALL
72
+ - lib/generators/gitlab/experiment/install/templates
74
73
  - lib/generators/gitlab/experiment/install/templates/application_experiment.rb.tt
75
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
76
79
  - lib/generators/gitlab/experiment/templates/experiment.rb.tt
77
- - lib/generators/rspec/experiment/experiment_generator.rb
78
- - lib/generators/rspec/experiment/templates/experiment_spec.rb.tt
80
+ - lib/generators/test_unit
81
+ - lib/generators/test_unit/experiment
79
82
  - lib/generators/test_unit/experiment/experiment_generator.rb
83
+ - lib/generators/test_unit/experiment/templates
80
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
81
90
  - lib/gitlab/experiment.rb
82
- - lib/gitlab/experiment/base_interface.rb
83
- - 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
84
95
  - lib/gitlab/experiment/cache/redis_hash_store.rb
96
+ - lib/gitlab/experiment/errors.rb
85
97
  - lib/gitlab/experiment/callbacks.rb
86
- - lib/gitlab/experiment/configuration.rb
98
+ - lib/gitlab/experiment/rollout.rb
99
+ - lib/gitlab/experiment/base_interface.rb
87
100
  - lib/gitlab/experiment/context.rb
88
- - lib/gitlab/experiment/cookies.rb
89
- - lib/gitlab/experiment/dsl.rb
90
101
  - lib/gitlab/experiment/engine.rb
91
- - lib/gitlab/experiment/rollout.rb
102
+ - lib/gitlab/experiment/rspec.rb
103
+ - lib/gitlab/experiment/rollout
92
104
  - lib/gitlab/experiment/rollout/random.rb
93
105
  - lib/gitlab/experiment/rollout/round_robin.rb
94
- - lib/gitlab/experiment/rspec.rb
95
- - lib/gitlab/experiment/variant.rb
106
+ - lib/gitlab/experiment/rollout/percent.rb
107
+ - lib/gitlab/experiment/cache.rb
96
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
97
114
  homepage: https://gitlab.com/gitlab-org/gitlab-experiment
98
115
  licenses:
99
116
  - MIT
@@ -113,7 +130,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
113
130
  - !ruby/object:Gem::Version
114
131
  version: '0'
115
132
  requirements: []
116
- rubygems_version: 3.2.17
133
+ rubygems_version: 3.1.4
117
134
  signing_key:
118
135
  specification_version: 4
119
136
  summary: GitLab experiment library built on top of scientist.