gitlab-experiment 0.5.4 → 0.6.4

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: 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.