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 +4 -4
- data/README.md +24 -0
- data/lib/generators/gitlab/experiment/install/templates/initializer.rb.tt +39 -17
- data/lib/gitlab/experiment.rb +26 -1
- data/lib/gitlab/experiment/base_interface.rb +8 -1
- data/lib/gitlab/experiment/configuration.rb +30 -15
- data/lib/gitlab/experiment/context.rb +14 -6
- data/lib/gitlab/experiment/cookies.rb +1 -1
- data/lib/gitlab/experiment/dsl.rb +4 -0
- data/lib/gitlab/experiment/engine.rb +34 -9
- data/lib/gitlab/experiment/errors.rb +8 -0
- data/lib/gitlab/experiment/middleware.rb +27 -0
- data/lib/gitlab/experiment/rollout.rb +8 -1
- data/lib/gitlab/experiment/rollout/percent.rb +47 -0
- data/lib/gitlab/experiment/rspec.rb +1 -1
- data/lib/gitlab/experiment/variant.rb +5 -1
- data/lib/gitlab/experiment/version.rb +1 -1
- metadata +35 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ed3c0028563936ce8a3fbdfd1de9d64e499102c4ec71a21946b63c56cc2bebd5
|
4
|
+
data.tar.gz: 43389ef9b7b2a4b3e60b82b5ffefbe3635739cb9fafec21dd5e9d59586f942c2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
@@ -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
|
-
|
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}:#{
|
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 =
|
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
|
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:
|
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
|
-
|
59
|
-
|
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
|
65
|
-
|
70
|
+
def add_unmerged_migration(value = {})
|
71
|
+
@migrations[:unmerged] << value if value.is_a?(Hash)
|
72
|
+
end
|
66
73
|
|
67
|
-
|
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
|
@@ -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
|
-
|
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) 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,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.
|
85
|
+
klass = experiment.instance_of?(Class) ? experiment : experiment.class
|
86
86
|
unless klass <= Gitlab::Experiment
|
87
87
|
raise(
|
88
88
|
ArgumentError,
|
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.4
|
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
|
@@ -65,35 +65,52 @@ executables: []
|
|
65
65
|
extensions: []
|
66
66
|
extra_rdoc_files: []
|
67
67
|
files:
|
68
|
-
-
|
69
|
-
-
|
70
|
-
- lib/generators/gitlab/experiment/
|
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
|
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/
|
78
|
-
- lib/generators/
|
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
|
83
|
-
- lib/gitlab/experiment/
|
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/
|
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/
|
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/
|
95
|
-
- lib/gitlab/experiment/
|
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.
|
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.
|