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 +4 -4
- data/README.md +24 -0
- data/lib/generators/gitlab/experiment/install/templates/initializer.rb.tt +39 -17
- data/lib/gitlab/experiment.rb +40 -8
- data/lib/gitlab/experiment/base_interface.rb +7 -1
- data/lib/gitlab/experiment/configuration.rb +34 -16
- data/lib/gitlab/experiment/context.rb +7 -1
- data/lib/gitlab/experiment/dsl.rb +4 -0
- data/lib/gitlab/experiment/engine.rb +31 -9
- data/lib/gitlab/experiment/errors.rb +8 -0
- data/lib/gitlab/experiment/middleware.rb +27 -0
- data/lib/gitlab/experiment/rollout.rb +18 -3
- data/lib/gitlab/experiment/rollout/percent.rb +47 -0
- data/lib/gitlab/experiment/rspec.rb +5 -1
- data/lib/gitlab/experiment/variant.rb +5 -1
- data/lib/gitlab/experiment/version.rb +1 -1
- metadata +48 -18
- data/lib/gitlab/experiment/rollout/first.rb +0 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f89a48e8e15030981a25d71cfc86d9f92520e535b90381209861e698d47d646e
|
4
|
+
data.tar.gz: 80c92e4817efd0b9f9c273bd6392548ef03352140c9d8e2cc9ce13cee90fb5b2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
22
|
-
#
|
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::
|
32
|
-
config.default_rollout = Gitlab::Experiment::Rollout::
|
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
|
data/lib/gitlab/experiment.rb
CHANGED
@@ -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(
|
135
|
-
|
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.
|
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}:#{
|
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
|
30
|
-
|
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 =
|
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
|
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:
|
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
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
-
|
20
|
+
private
|
13
21
|
|
14
|
-
|
15
|
-
|
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
|
-
|
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,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 :
|
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(
|
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.
|
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
|
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.
|
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:
|
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
|
-
-
|
55
|
-
-
|
56
|
-
- lib/generators/gitlab/experiment/
|
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
|
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/
|
64
|
-
- lib/generators/
|
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
|
69
|
-
- lib/gitlab/experiment/
|
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/
|
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/
|
78
|
-
- lib/gitlab/experiment/rollout
|
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/
|
82
|
-
- lib/gitlab/experiment/
|
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
|