gitlab-experiment 0.6.4 → 0.7.1
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 +410 -290
- data/lib/generators/gitlab/experiment/experiment_generator.rb +9 -4
- data/lib/generators/gitlab/experiment/install/templates/initializer.rb.tt +87 -45
- data/lib/generators/gitlab/experiment/templates/experiment.rb.tt +69 -3
- data/lib/gitlab/experiment/base_interface.rb +86 -24
- data/lib/gitlab/experiment/cache/redis_hash_store.rb +10 -10
- data/lib/gitlab/experiment/cache.rb +3 -7
- data/lib/gitlab/experiment/callbacks.rb +97 -5
- data/lib/gitlab/experiment/configuration.rb +209 -28
- data/lib/gitlab/experiment/context.rb +2 -3
- data/lib/gitlab/experiment/cookies.rb +0 -2
- data/lib/gitlab/experiment/engine.rb +2 -1
- data/lib/gitlab/experiment/errors.rb +21 -0
- data/lib/gitlab/experiment/nestable.rb +51 -0
- data/lib/gitlab/experiment/rollout/percent.rb +41 -16
- data/lib/gitlab/experiment/rollout/random.rb +25 -4
- data/lib/gitlab/experiment/rollout/round_robin.rb +27 -10
- data/lib/gitlab/experiment/rollout.rb +61 -12
- data/lib/gitlab/experiment/rspec.rb +224 -130
- data/lib/gitlab/experiment/test_behaviors/trackable.rb +69 -0
- data/lib/gitlab/experiment/version.rb +1 -1
- data/lib/gitlab/experiment.rb +118 -56
- metadata +8 -24
data/lib/gitlab/experiment.rb
CHANGED
@@ -1,13 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'scientist'
|
4
3
|
require 'request_store'
|
5
|
-
require 'active_support
|
6
|
-
require 'active_support/cache'
|
7
|
-
require 'active_support/concern'
|
8
|
-
require 'active_support/core_ext/object/blank'
|
9
|
-
require 'active_support/core_ext/string/inflections'
|
4
|
+
require 'active_support'
|
10
5
|
require 'active_support/core_ext/module/delegation'
|
6
|
+
require 'active_support/core_ext/string/inflections'
|
11
7
|
|
12
8
|
require 'gitlab/experiment/errors'
|
13
9
|
require 'gitlab/experiment/base_interface'
|
@@ -19,6 +15,7 @@ require 'gitlab/experiment/cookies'
|
|
19
15
|
require 'gitlab/experiment/context'
|
20
16
|
require 'gitlab/experiment/dsl'
|
21
17
|
require 'gitlab/experiment/middleware'
|
18
|
+
require 'gitlab/experiment/nestable'
|
22
19
|
require 'gitlab/experiment/variant'
|
23
20
|
require 'gitlab/experiment/version'
|
24
21
|
require 'gitlab/experiment/engine' if defined?(Rails::Engine)
|
@@ -28,87 +25,141 @@ module Gitlab
|
|
28
25
|
include BaseInterface
|
29
26
|
include Cache
|
30
27
|
include Callbacks
|
28
|
+
include Nestable
|
31
29
|
|
32
30
|
class << self
|
33
|
-
|
34
|
-
|
31
|
+
# Class level behavior registration methods.
|
32
|
+
|
33
|
+
def control(*filter_list, **options, &block)
|
34
|
+
variant(:control, *filter_list, **options, &block)
|
35
|
+
end
|
35
36
|
|
36
|
-
|
37
|
+
def candidate(*filter_list, **options, &block)
|
38
|
+
variant(:candidate, *filter_list, **options, &block)
|
37
39
|
end
|
38
40
|
|
41
|
+
def variant(variant, *filter_list, **options, &block)
|
42
|
+
build_behavior_callback(filter_list, variant, **options, &block)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Class level callback registration methods.
|
46
|
+
|
39
47
|
def exclude(*filter_list, **options, &block)
|
40
|
-
|
41
|
-
throw(:abort) if target.instance_variable_get(:@excluded) || callback.call(target, nil) == true
|
42
|
-
end
|
48
|
+
build_exclude_callback(filter_list.unshift(block), **options)
|
43
49
|
end
|
44
50
|
|
45
51
|
def segment(*filter_list, variant:, **options, &block)
|
46
|
-
|
47
|
-
|
48
|
-
|
52
|
+
build_segment_callback(filter_list.unshift(block), variant, **options)
|
53
|
+
end
|
54
|
+
|
55
|
+
def before_run(*filter_list, **options, &block)
|
56
|
+
build_run_callback(filter_list.unshift(:before, block), **options)
|
57
|
+
end
|
58
|
+
|
59
|
+
def around_run(*filter_list, **options, &block)
|
60
|
+
build_run_callback(filter_list.unshift(:around, block), **options)
|
61
|
+
end
|
62
|
+
|
63
|
+
def after_run(*filter_list, **options, &block)
|
64
|
+
build_run_callback(filter_list.unshift(:after, block), **options)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Class level definition methods.
|
68
|
+
|
69
|
+
def default_rollout(rollout = nil, options = {})
|
70
|
+
return @_rollout ||= Configuration.default_rollout if rollout.blank?
|
71
|
+
|
72
|
+
@_rollout = Rollout.resolve(rollout, options)
|
49
73
|
end
|
50
74
|
|
75
|
+
# Class level accessor methods.
|
76
|
+
|
51
77
|
def published_experiments
|
52
78
|
RequestStore.store[:published_gitlab_experiments] || {}
|
53
79
|
end
|
54
80
|
end
|
55
81
|
|
56
82
|
def name
|
57
|
-
[Configuration.name_prefix, @
|
83
|
+
[Configuration.name_prefix, @_name].compact.join('_')
|
58
84
|
end
|
59
85
|
|
60
86
|
def control(&block)
|
61
|
-
|
87
|
+
variant(:control, &block)
|
62
88
|
end
|
63
|
-
alias_method :use, :control
|
64
89
|
|
65
90
|
def candidate(name = nil, &block)
|
66
|
-
|
67
|
-
|
91
|
+
if name.present?
|
92
|
+
Configuration.deprecated(<<~MESSAGE, version: '0.7.0')
|
93
|
+
passing name to `candidate` is deprecated and will be removed from {{release}} (instead use `variant(#{name.inspect})`)
|
94
|
+
MESSAGE
|
95
|
+
end
|
96
|
+
|
97
|
+
variant(name || :candidate, &block)
|
98
|
+
end
|
99
|
+
|
100
|
+
def variant(name = nil, &block)
|
101
|
+
if block.present? # we know we're defining a variant block
|
102
|
+
raise ArgumentError, 'missing variant name' if name.blank?
|
103
|
+
|
104
|
+
return behaviors[name.to_s] = block
|
105
|
+
end
|
106
|
+
|
107
|
+
if name.present?
|
108
|
+
Configuration.deprecated(<<~MESSAGE, version: '0.7.0')
|
109
|
+
setting the variant using `variant` is deprecated and will be removed from {{release}} (instead use `assigned(#{name.inspect})`)
|
110
|
+
MESSAGE
|
111
|
+
else
|
112
|
+
Configuration.deprecated(<<~MESSAGE, version: '0.7.0')
|
113
|
+
getting the assigned variant using `variant` is deprecated and will be removed from {{release}} (instead use `assigned`)
|
114
|
+
MESSAGE
|
115
|
+
end
|
116
|
+
|
117
|
+
assigned(name)
|
68
118
|
end
|
69
|
-
alias_method :try, :candidate
|
70
119
|
|
71
120
|
def context(value = nil)
|
72
|
-
return @
|
121
|
+
return @_context if value.blank?
|
73
122
|
|
74
|
-
@
|
75
|
-
@
|
123
|
+
@_context.value(value)
|
124
|
+
@_context
|
76
125
|
end
|
77
126
|
|
78
|
-
def
|
79
|
-
@
|
80
|
-
|
127
|
+
def assigned(value = nil)
|
128
|
+
@_assigned_variant_name = cache_variant(value) if value.present?
|
129
|
+
if @_assigned_variant_name || @_resolving_variant
|
130
|
+
return Variant.new(name: (@_assigned_variant_name || :unresolved).to_s)
|
131
|
+
end
|
81
132
|
|
82
133
|
if enabled?
|
83
|
-
@
|
84
|
-
@
|
134
|
+
@_resolving_variant = true
|
135
|
+
@_assigned_variant_name = cached_variant_resolver(@_assigned_variant_name)
|
85
136
|
end
|
86
137
|
|
87
138
|
run_callbacks(segmentation_callback_chain) do
|
88
|
-
@
|
89
|
-
Variant.new(name: @
|
139
|
+
@_assigned_variant_name ||= :control
|
140
|
+
Variant.new(name: @_assigned_variant_name.to_s)
|
90
141
|
end
|
91
142
|
ensure
|
92
|
-
@
|
143
|
+
@_resolving_variant = false
|
93
144
|
end
|
94
145
|
|
95
146
|
def rollout(rollout = nil, options = {})
|
96
|
-
return @
|
147
|
+
return @_rollout ||= self.class.default_rollout(nil, options).for(self) if rollout.blank?
|
97
148
|
|
98
|
-
@
|
149
|
+
@_rollout = Rollout.resolve(rollout, options).for(self)
|
99
150
|
end
|
100
151
|
|
101
152
|
def exclude!
|
102
|
-
@
|
153
|
+
@_excluded = true
|
103
154
|
end
|
104
155
|
|
105
156
|
def run(variant_name = nil)
|
106
|
-
@
|
107
|
-
|
108
|
-
|
157
|
+
return @_result if context.frozen?
|
158
|
+
|
159
|
+
@_result = run_callbacks(run_callback_chain) { super(assigned(variant_name).name) }
|
109
160
|
end
|
110
161
|
|
111
|
-
def publish(result)
|
162
|
+
def publish(result = nil)
|
112
163
|
instance_exec(result, &Configuration.publishing_behavior)
|
113
164
|
|
114
165
|
(RequestStore.store[:published_gitlab_experiments] ||= {})[name] = signature.merge(excluded: excluded?)
|
@@ -117,7 +168,7 @@ module Gitlab
|
|
117
168
|
def track(action, **event_args)
|
118
169
|
return unless should_track?
|
119
170
|
|
120
|
-
instance_exec(action, event_args, &Configuration.tracking_behavior)
|
171
|
+
instance_exec(action, tracking_context(event_args).try(:compact) || {}, &Configuration.tracking_behavior)
|
121
172
|
end
|
122
173
|
|
123
174
|
def process_redirect_url(url)
|
@@ -128,33 +179,31 @@ module Gitlab
|
|
128
179
|
end
|
129
180
|
|
130
181
|
def enabled?
|
131
|
-
|
182
|
+
rollout.enabled?
|
132
183
|
end
|
133
184
|
|
134
185
|
def excluded?
|
135
|
-
return @
|
186
|
+
return @_excluded if defined?(@_excluded)
|
136
187
|
|
137
|
-
@
|
138
|
-
end
|
139
|
-
|
140
|
-
def experiment_group?
|
141
|
-
instance_exec(@variant_name, &Configuration.inclusion_resolver)
|
188
|
+
@_excluded = !run_callbacks(exclusion_callback_chain) { :not_excluded }
|
142
189
|
end
|
143
190
|
|
144
191
|
def should_track?
|
145
|
-
enabled? &&
|
192
|
+
enabled? && context.trackable? && !excluded?
|
146
193
|
end
|
147
194
|
|
148
195
|
def signature
|
149
|
-
{ variant:
|
196
|
+
{ variant: assigned.name, experiment: name }.merge(context.signature)
|
150
197
|
end
|
151
198
|
|
152
199
|
def key_for(source, seed = name)
|
153
|
-
# TODO:
|
200
|
+
# TODO: Remove - deprecated in release 0.7.0
|
154
201
|
if (block = Configuration.instance_variable_get(:@__context_hash_strategy))
|
155
202
|
return instance_exec(source, seed, &block)
|
156
203
|
end
|
157
204
|
|
205
|
+
return source if source.is_a?(String)
|
206
|
+
|
158
207
|
source = source.keys + source.values if source.is_a?(Hash)
|
159
208
|
|
160
209
|
ingredients = Array(source).map { |v| identify(v) }
|
@@ -169,14 +218,27 @@ module Gitlab
|
|
169
218
|
(object.respond_to?(:to_global_id) ? object.to_global_id : object).to_s
|
170
219
|
end
|
171
220
|
|
172
|
-
def
|
173
|
-
|
174
|
-
|
175
|
-
|
221
|
+
def resolve_variant_name
|
222
|
+
if respond_to?(:experiment_group?, true)
|
223
|
+
# TODO: Remove - deprecated in release 0.7.0
|
224
|
+
Configuration.deprecated(:experiment_group?, <<~MESSAGE, version: '0.7.0')
|
225
|
+
instead put this logic into custom rollout strategies
|
226
|
+
MESSAGE
|
227
|
+
|
228
|
+
rollout.resolve if experiment_group?
|
229
|
+
elsif (block = Configuration.instance_variable_get(:@__inclusion_resolver))
|
230
|
+
# TODO: Remove - deprecated in release 0.7.0
|
231
|
+
rollout.resolve if instance_exec(@_assigned_variant_name, &block)
|
232
|
+
elsif (block = Configuration.instance_variable_get(:@__variant_resolver))
|
233
|
+
# TODO: Remove - deprecated in release 0.6.5
|
234
|
+
instance_exec(@_assigned_variant_name, &block)
|
235
|
+
else
|
236
|
+
rollout.resolve # this is the end result of all deprecations
|
237
|
+
end
|
176
238
|
end
|
177
239
|
|
178
|
-
def
|
179
|
-
|
240
|
+
def tracking_context(event_args)
|
241
|
+
{}.merge(event_args)
|
180
242
|
end
|
181
243
|
end
|
182
244
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: gitlab-experiment
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- GitLab
|
@@ -38,26 +38,6 @@ dependencies:
|
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '1.0'
|
41
|
-
- !ruby/object:Gem::Dependency
|
42
|
-
name: scientist
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
44
|
-
requirements:
|
45
|
-
- - "~>"
|
46
|
-
- !ruby/object:Gem::Version
|
47
|
-
version: '1.6'
|
48
|
-
- - ">="
|
49
|
-
- !ruby/object:Gem::Version
|
50
|
-
version: 1.6.0
|
51
|
-
type: :runtime
|
52
|
-
prerelease: false
|
53
|
-
version_requirements: !ruby/object:Gem::Requirement
|
54
|
-
requirements:
|
55
|
-
- - "~>"
|
56
|
-
- !ruby/object:Gem::Version
|
57
|
-
version: '1.6'
|
58
|
-
- - ">="
|
59
|
-
- !ruby/object:Gem::Version
|
60
|
-
version: 1.6.0
|
61
41
|
description:
|
62
42
|
email:
|
63
43
|
- gitlab_rubygems@gitlab.com
|
@@ -91,12 +71,15 @@ files:
|
|
91
71
|
- lib/gitlab/experiment
|
92
72
|
- lib/gitlab/experiment/variant.rb
|
93
73
|
- lib/gitlab/experiment/middleware.rb
|
74
|
+
- lib/gitlab/experiment/test_behaviors
|
75
|
+
- lib/gitlab/experiment/test_behaviors/trackable.rb
|
94
76
|
- lib/gitlab/experiment/cache
|
95
77
|
- lib/gitlab/experiment/cache/redis_hash_store.rb
|
96
78
|
- lib/gitlab/experiment/errors.rb
|
97
79
|
- lib/gitlab/experiment/callbacks.rb
|
98
80
|
- lib/gitlab/experiment/rollout.rb
|
99
81
|
- lib/gitlab/experiment/base_interface.rb
|
82
|
+
- lib/gitlab/experiment/nestable.rb
|
100
83
|
- lib/gitlab/experiment/context.rb
|
101
84
|
- lib/gitlab/experiment/engine.rb
|
102
85
|
- lib/gitlab/experiment/rspec.rb
|
@@ -104,6 +87,7 @@ files:
|
|
104
87
|
- lib/gitlab/experiment/rollout/random.rb
|
105
88
|
- lib/gitlab/experiment/rollout/round_robin.rb
|
106
89
|
- lib/gitlab/experiment/rollout/percent.rb
|
90
|
+
- lib/gitlab/experiment/rollout/concerns
|
107
91
|
- lib/gitlab/experiment/cache.rb
|
108
92
|
- lib/gitlab/experiment/version.rb
|
109
93
|
- lib/gitlab/experiment/cookies.rb
|
@@ -111,7 +95,7 @@ files:
|
|
111
95
|
- lib/gitlab/experiment/dsl.rb
|
112
96
|
- LICENSE.txt
|
113
97
|
- README.md
|
114
|
-
homepage: https://gitlab.com/gitlab-org/gitlab-experiment
|
98
|
+
homepage: https://gitlab.com/gitlab-org/ruby/gems/gitlab-experiment
|
115
99
|
licenses:
|
116
100
|
- MIT
|
117
101
|
metadata: {}
|
@@ -130,8 +114,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
130
114
|
- !ruby/object:Gem::Version
|
131
115
|
version: '0'
|
132
116
|
requirements: []
|
133
|
-
rubygems_version: 3.1.
|
117
|
+
rubygems_version: 3.1.6
|
134
118
|
signing_key:
|
135
119
|
specification_version: 4
|
136
|
-
summary: GitLab
|
120
|
+
summary: GitLab experimentation library.
|
137
121
|
test_files: []
|