gitlab-experiment 0.4.1 → 0.4.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +110 -48
- data/lib/generators/gitlab/experiment/install/templates/initializer.rb.tt +25 -28
- data/lib/gitlab/experiment.rb +19 -15
- data/lib/gitlab/experiment/caching.rb +2 -2
- data/lib/gitlab/experiment/configuration.rb +8 -11
- data/lib/gitlab/experiment/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 865d51c081670824fc7ffb48d4bc764fd5370606d3aa63b2196421b5570506e7
|
4
|
+
data.tar.gz: a007f872a3d56ee81c9925117f603e4930d1b7d689078edb074a2ba5567833f2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 03d401a71b952b74519a21fa851a87ee9e104ccd19011af37ac41c36bec5fd63db3e68c62c05beb34ab296d354e00e15f013dd34a547c3b4c5c94474b89096cc
|
7
|
+
data.tar.gz: 2bb0260e40d4689446c1317273d85140fae9f56394821fda96c7bbc5d1acbe441f3e4024bf00ec05812f64f5588897137fc8061e790d871c5719742f6cedf516
|
data/README.md
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
|
1
|
+
GitLab Experiment
|
2
|
+
=================
|
2
3
|
|
3
4
|
<img alt="experiment" src="/uploads/60990b2dbf4c0406bbf8b7f998de2dea/experiment.png" align="right" width="40%">
|
4
5
|
|
@@ -16,6 +17,8 @@ When we discuss the behavior of this gem, we'll use terms like experiment, conte
|
|
16
17
|
|
17
18
|
Candidate and variant are the same concept, but simplify how we speak about experimental paths.<br clear="all">
|
18
19
|
|
20
|
+
[[_TOC_]]
|
21
|
+
|
19
22
|
## Installation
|
20
23
|
|
21
24
|
Add the gem to your Gemfile and then `bundle install`.
|
@@ -24,7 +27,7 @@ Add the gem to your Gemfile and then `bundle install`.
|
|
24
27
|
gem 'gitlab-experiment'
|
25
28
|
```
|
26
29
|
|
27
|
-
If you're using Rails, you can install the initializer
|
30
|
+
If you're using Rails, you can install the initializer which provides basic configuration, documentation, and the base experiment class that all your experiments can inherit from.
|
28
31
|
|
29
32
|
```shell
|
30
33
|
$ rails generate gitlab:experiment:install
|
@@ -79,72 +82,84 @@ To this end, we track events that are important by calling the same experiment e
|
|
79
82
|
experiment(:notification_toggle, actor: user).track(:clicked_button)
|
80
83
|
```
|
81
84
|
|
82
|
-
|
83
|
-
<summary>You can also use the more low level class or instance interfaces...</summary>
|
85
|
+
### Custom experiments
|
84
86
|
|
85
|
-
|
87
|
+
You can craft more advanced behaviors by defining custom experiments at a higher level. To do this you can define a class that inherits from `ApplicationExperiment` (or `Gitlab::Experiment`).
|
86
88
|
|
87
|
-
|
88
|
-
exp = Gitlab::Experiment.run(:notification_toggle, actor: user) do |e|
|
89
|
-
# Context may be passed in the block, but must be finalized before calling
|
90
|
-
# run or track.
|
91
|
-
e.context(project: project) # add the project to the context
|
89
|
+
Let's say you want to do more advanced segmentation, or provide default behavior for the variants on the experiment we've already outlined above -- that way if the variants aren't defined in the block at the time the experiment is run, these methods will be used.
|
92
90
|
|
93
|
-
|
94
|
-
e.use { render_toggle } # control
|
95
|
-
e.try { render_button } # candidate
|
96
|
-
end
|
91
|
+
You can generate a custom experiment by running:
|
97
92
|
|
98
|
-
|
99
|
-
|
100
|
-
```
|
93
|
+
```shell
|
94
|
+
$ rails generate gitlab:experiment NotificationToggle control candidate
|
95
|
+
```
|
96
|
+
|
97
|
+
This will generate a file in `app/experiments/notification_toggle_experiment.rb`, as well as a test file for you to further expand on.
|
101
98
|
|
102
|
-
|
99
|
+
Here are some examples of what you can introduce once you have a custom experiment defined.
|
103
100
|
|
104
101
|
```ruby
|
105
|
-
|
106
|
-
#
|
107
|
-
#
|
108
|
-
|
102
|
+
class NotificationToggleExperiment < ApplicationExperiment
|
103
|
+
# Segment any account less than 2 weeks old into the candidate, without
|
104
|
+
# asking the variant resolver to decide which variant to provide.
|
105
|
+
segment :account_age, variant: :candidate
|
106
|
+
|
107
|
+
# Define the default control behavior, which can be overridden at
|
108
|
+
# experiment time.
|
109
|
+
def control_behavior
|
110
|
+
render_toggle
|
111
|
+
end
|
112
|
+
|
113
|
+
# Define the default candidate behavior, which can be overridden
|
114
|
+
# at experiment time.
|
115
|
+
def candidate_behavior
|
116
|
+
render_button
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
109
120
|
|
110
|
-
|
111
|
-
|
112
|
-
|
121
|
+
def account_age
|
122
|
+
context.actor && context.actor.created_at < 2.weeks.ago
|
123
|
+
end
|
124
|
+
end
|
113
125
|
|
114
|
-
#
|
126
|
+
# The class will be looked up based on the experiment name provided.
|
127
|
+
exp = experiment(:notification_toggle, actor: user)
|
128
|
+
exp # => instance of NotificationToggleExperiment
|
129
|
+
|
130
|
+
# Run the experiment -- returning the result.
|
115
131
|
exp.run
|
116
132
|
|
117
133
|
# Track an event on the experiment we've defined.
|
118
134
|
exp.track(:clicked_button)
|
119
135
|
```
|
120
136
|
|
121
|
-
|
137
|
+
You can now also do things very similar to the simple examples and override the default variant behaviors defined in the custom experiment -- keeping in mind that this should be carefully considered within the scope of your experiment.
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
experiment(:notification_toggle, actor: user) do |e|
|
141
|
+
e.use { render_special_toggle } # override default control behavior
|
142
|
+
end
|
143
|
+
```
|
122
144
|
|
123
145
|
<details>
|
124
|
-
<summary>You can
|
146
|
+
<summary>You can also use the lower level class interface...</summary>
|
125
147
|
|
126
|
-
###
|
148
|
+
### Using the `.run` approach
|
127
149
|
|
128
|
-
|
129
|
-
class NotificationExperiment < Gitlab::Experiment
|
130
|
-
def initialize(variant_name = nil, **context, &block)
|
131
|
-
super(:notification_toggle, variant_name, **context, &block)
|
150
|
+
This is useful if you haven't included the DSL and so don't have access to the `experiment` method, but still want to execute an experiment. This is ultimately what the `experiment` method calls through to, and the method signatures are the same.
|
132
151
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
152
|
+
```ruby
|
153
|
+
exp = Gitlab::Experiment.run(:notification_toggle, actor: user) do |e|
|
154
|
+
# Context may be passed in the block, but must be finalized before calling
|
155
|
+
# run or track.
|
156
|
+
e.context(project: project) # add the project to the context
|
138
157
|
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
e.context(project: project) # add the project id to the context
|
158
|
+
# Define the control and candidate variant.
|
159
|
+
e.use { render_toggle } # control
|
160
|
+
e.try { render_button } # candidate
|
143
161
|
end
|
144
162
|
|
145
|
-
# Run the experiment -- returning the result.
|
146
|
-
exp.run
|
147
|
-
|
148
163
|
# Track an event on the experiment we've defined.
|
149
164
|
exp.track(:clicked_button)
|
150
165
|
```
|
@@ -152,11 +167,11 @@ exp.track(:clicked_button)
|
|
152
167
|
</details>
|
153
168
|
|
154
169
|
<details>
|
155
|
-
<summary>You can also specify the variant to use...</summary>
|
170
|
+
<summary>You can also specify the variant to use for segmentation...</summary>
|
156
171
|
|
157
172
|
### Specifying variant
|
158
173
|
|
159
|
-
|
174
|
+
Generally, defining segmentation rules is a better way to approach routing into specific variants, but it's possible to explicitly specify the variant when running an experiment. It's important to know what this might do to your data during rollout, so use this with careful consideration.
|
160
175
|
|
161
176
|
```ruby
|
162
177
|
experiment(:notification_toggle, :no_interface, actor: user) do |e|
|
@@ -188,6 +203,25 @@ end
|
|
188
203
|
|
189
204
|
</details>
|
190
205
|
|
206
|
+
### Segmentation rules
|
207
|
+
|
208
|
+
This library comes with the capability to segment contexts into a specific variant, before asking the variant resolver which variant to provide.
|
209
|
+
|
210
|
+
Segmentation can be achieved by using a custom experiment class and specifying the segmentation rules at a class level.
|
211
|
+
|
212
|
+
```ruby
|
213
|
+
class NotificationToggleExperiment < ApplicationExperiment
|
214
|
+
segment(variant: :variant_one) { context.actor.username == 'jejacks0n' }
|
215
|
+
segment(variant: :variant_two) { context.actor.created_at < 2.weeks.ago }
|
216
|
+
end
|
217
|
+
```
|
218
|
+
|
219
|
+
In the previous examples, any user with the username `'jejacks0n'` would always receive the experience defined in "variant_one". As well, any account less than 2 weeks old would get the alternate experience defined in "variant_two".
|
220
|
+
|
221
|
+
When an experiment is run, the segmentation rules are executed in the order they're defined. The first segmentation rule to produce a truthy result is the one which gets used to assign the variant. The remaining segmentation rules are skipped.
|
222
|
+
|
223
|
+
This means that any user with the name `'jejacks0n'`, regardless of account age, will always be provided the experience as defined in "variant_one".
|
224
|
+
|
191
225
|
### Return value
|
192
226
|
|
193
227
|
By default the return value is a `Gitlab::Experiment` instance. In simple cases you may want only the results of the experiment though. You can call `run` within the block to get the return value of the assigned variant.
|
@@ -351,4 +385,32 @@ If you only include a user, that user would get the same experience across every
|
|
351
385
|
|
352
386
|
Each of these approaches could be desirable given the objectives of your experiment.
|
353
387
|
|
354
|
-
|
388
|
+
## Development
|
389
|
+
|
390
|
+
After checking out the repo, run `bundle install` to install dependencies.
|
391
|
+
Then, run `bundle exec rspec` to run the tests. You can also run `bundle exec pry` for an
|
392
|
+
interactive prompt that will allow you to experiment.
|
393
|
+
|
394
|
+
## Contributing
|
395
|
+
|
396
|
+
Bug reports and merge requests are welcome on GitLab at
|
397
|
+
https://gitlab.com/gitlab-org/gitlab-experiment. This project is intended to be a
|
398
|
+
safe, welcoming space for collaboration, and contributors are expected to adhere
|
399
|
+
to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
400
|
+
|
401
|
+
## Release Process
|
402
|
+
|
403
|
+
Please refer to the [Release Process](docs/release_process.md).
|
404
|
+
|
405
|
+
## License
|
406
|
+
|
407
|
+
The gem is available as open source under the terms of the
|
408
|
+
[MIT License](http://opensource.org/licenses/MIT).
|
409
|
+
|
410
|
+
## Code of Conduct
|
411
|
+
|
412
|
+
Everyone interacting in the `Gitlab::Experiment` project’s codebases, issue trackers,
|
413
|
+
chat rooms and mailing lists is expected to follow the
|
414
|
+
[code of conduct](CODE_OF_CONDUCT.md).
|
415
|
+
|
416
|
+
***Make code not war***
|
@@ -7,20 +7,28 @@ Gitlab::Experiment.configure do |config|
|
|
7
7
|
# The logger is used to log various details of the experiments.
|
8
8
|
config.logger = Logger.new($stdout)
|
9
9
|
|
10
|
-
# The base class that should be instantiated for basic experiments.
|
10
|
+
# The base class that should be instantiated for basic experiments. It should
|
11
|
+
# be a string, so we can constantize it later.
|
11
12
|
config.base_class = 'ApplicationExperiment'
|
12
13
|
|
13
|
-
# The caching layer is expected to respond to fetch, like Rails.cache
|
14
|
+
# The caching layer is expected to respond to fetch, like Rails.cache for
|
15
|
+
# instance -- or anything that adheres to ActiveSupport::Cache::Store.
|
14
16
|
config.cache = nil
|
15
17
|
|
18
|
+
# The domain to use on cookies.
|
19
|
+
#
|
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']`.
|
23
|
+
config.cookie_domain = :all
|
24
|
+
|
16
25
|
# Logic this project uses to resolve a variant for a given experiment.
|
17
26
|
#
|
18
|
-
#
|
19
|
-
#
|
20
|
-
# class will be used.
|
27
|
+
# Should return a symbol or string that represents the variant that should
|
28
|
+
# be assigned.
|
21
29
|
#
|
22
|
-
# This block
|
23
|
-
#
|
30
|
+
# This block is executed within the scope of the experiment and so can access
|
31
|
+
# experiment methods, like `name`, `context`, and `signature`.
|
24
32
|
config.variant_resolver = lambda do |requested_variant|
|
25
33
|
# Run the control, unless a variant was requested in code:
|
26
34
|
requested_variant || 'control'
|
@@ -28,23 +36,12 @@ Gitlab::Experiment.configure do |config|
|
|
28
36
|
# Run the candidate, unless a variant was requested, with a fallback:
|
29
37
|
#
|
30
38
|
# requested_variant || variant_names.first || 'control'
|
31
|
-
|
32
|
-
# Using Unleash to determine the variant:
|
33
|
-
#
|
34
|
-
# fallback = Unleash::Variant.new(name: requested_variant || 'control', enabled: true)
|
35
|
-
# Unleash.get_variant(name, context.value, fallback)
|
36
|
-
|
37
|
-
# Using Flipper to determine the variant:
|
38
|
-
#
|
39
|
-
# TODO: provide example.
|
40
|
-
# Variant.new(name: requested_variant || 'control')
|
41
39
|
end
|
42
40
|
|
43
41
|
# Tracking behavior can be implemented to link an event to an experiment.
|
44
42
|
#
|
45
|
-
#
|
46
|
-
# experiment
|
47
|
-
# such as name and signature.
|
43
|
+
# This block is executed within the scope of the experiment and so can access
|
44
|
+
# experiment methods, like `name`, `context`, and `signature`.
|
48
45
|
config.tracking_behavior = lambda do |event, args|
|
49
46
|
# An example of using a generic logger to track events:
|
50
47
|
config.logger.info "Gitlab::Experiment[#{name}] #{event}: #{args.merge(signature: signature)}"
|
@@ -61,8 +58,11 @@ Gitlab::Experiment.configure do |config|
|
|
61
58
|
# Called at the end of every experiment run, with the result.
|
62
59
|
#
|
63
60
|
# You may want to track that you've assigned a variant to a given context,
|
64
|
-
# or push the experiment into the client or publish results elsewhere
|
65
|
-
# into redis.
|
61
|
+
# or push the experiment into the client or publish results elsewhere like
|
62
|
+
# into redis.
|
63
|
+
#
|
64
|
+
# This block is executed within the scope of the experiment and so can access
|
65
|
+
# experiment methods, like `name`, `context`, and `signature`.
|
66
66
|
config.publishing_behavior = lambda do |result|
|
67
67
|
# Track the event using our own configured tracking logic.
|
68
68
|
track(:assignment)
|
@@ -83,14 +83,11 @@ Gitlab::Experiment.configure do |config|
|
|
83
83
|
# Given a specific context hash map, we need to generate a consistent hash
|
84
84
|
# key. The logic in here will be used for generating cache keys, and may also
|
85
85
|
# be used when determining which variant may be presented.
|
86
|
+
#
|
87
|
+
# This block is executed within the scope of the experiment and so can access
|
88
|
+
# experiment methods, like `name`, `context`, and `signature`.
|
86
89
|
config.context_hash_strategy = lambda do |context|
|
87
90
|
values = context.values.map { |v| (v.respond_to?(:to_global_id) ? v.to_global_id : v).to_s }
|
88
91
|
Digest::MD5.hexdigest((context.keys + values).join('|'))
|
89
92
|
end
|
90
|
-
|
91
|
-
# The domain for which this cookie applies so you can restrict to the domain level.
|
92
|
-
#
|
93
|
-
# When not set, it uses the current host. If you want to provide specific hosts, you can
|
94
|
-
# provide them either via an array like `['www.gitlab.com', .gitlab.com']`, or set it to `:all`.
|
95
|
-
config.cookie_domain = :all
|
96
93
|
end
|
data/lib/gitlab/experiment.rb
CHANGED
@@ -76,10 +76,11 @@ module Gitlab
|
|
76
76
|
end
|
77
77
|
|
78
78
|
def variant(value = nil)
|
79
|
-
|
79
|
+
@variant_name = value unless value.blank?
|
80
|
+
@variant_name ||= :control if excluded?
|
80
81
|
|
81
|
-
|
82
|
-
|
82
|
+
resolved = cache { resolve_variant_name }
|
83
|
+
Variant.new(name: (resolved.presence || :control).to_s)
|
83
84
|
end
|
84
85
|
|
85
86
|
def exclude(&block)
|
@@ -88,19 +89,13 @@ module Gitlab
|
|
88
89
|
|
89
90
|
def run(variant_name = nil)
|
90
91
|
@result ||= begin
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
run_callbacks(chain) do
|
96
|
-
variant_name = cache { variant.name }
|
97
|
-
|
98
|
-
method_name = "#{variant_name}_behavior"
|
99
|
-
if respond_to?(method_name)
|
100
|
-
behaviors[variant_name] ||= -> { send(method_name) } # rubocop:disable GitlabSecurity/PublicSend
|
92
|
+
variant_name = variant(variant_name).name
|
93
|
+
run_callbacks(variant_assigned? ? :unsegmented_run : :segmented_run) do
|
94
|
+
if respond_to?((behavior_name = "#{variant_name}_behavior"))
|
95
|
+
behaviors[variant_name] ||= -> { send(behavior_name) } # rubocop:disable GitlabSecurity/PublicSend
|
101
96
|
end
|
102
97
|
|
103
|
-
super(variant_name)
|
98
|
+
super(@variant_name = variant_name)
|
104
99
|
end
|
105
100
|
end
|
106
101
|
end
|
@@ -140,7 +135,7 @@ module Gitlab
|
|
140
135
|
end
|
141
136
|
|
142
137
|
def id
|
143
|
-
"#{name}:#{
|
138
|
+
"#{name}:#{key_for(context.value)}"
|
144
139
|
end
|
145
140
|
alias_method :session_id, :id
|
146
141
|
|
@@ -154,6 +149,15 @@ module Gitlab
|
|
154
149
|
|
155
150
|
protected
|
156
151
|
|
152
|
+
def resolve_variant_name
|
153
|
+
return :unresolved if @resolving
|
154
|
+
|
155
|
+
@resolving = true
|
156
|
+
result = instance_exec(@variant_name, &Configuration.variant_resolver)
|
157
|
+
@resolving = false
|
158
|
+
result
|
159
|
+
end
|
160
|
+
|
157
161
|
def generate_result(variant_name)
|
158
162
|
observation = Scientist::Observation.new(variant_name, self, &behaviors[variant_name])
|
159
163
|
Scientist::Result.new(self, [observation], observation)
|
@@ -14,8 +14,8 @@ module Gitlab
|
|
14
14
|
|
15
15
|
def cache_strategy
|
16
16
|
[
|
17
|
-
"#{name}:#{signature[:key]}",
|
18
|
-
signature[:migration_keys]&.map { |key| "#{name}:#{key}" }
|
17
|
+
"#{name}:#{context.signature[:key]}",
|
18
|
+
context.signature[:migration_keys]&.map { |key| "#{name}:#{key}" }
|
19
19
|
]
|
20
20
|
end
|
21
21
|
|
@@ -18,12 +18,15 @@ module Gitlab
|
|
18
18
|
# The base class that should be instantiated for basic experiments.
|
19
19
|
@base_class = 'Gitlab::Experiment'
|
20
20
|
|
21
|
-
#
|
21
|
+
# The caching layer is expected to respond to fetch, like Rails.cache.
|
22
22
|
@cache = nil
|
23
23
|
|
24
|
+
# The domain to use on cookies.
|
25
|
+
@cookie_domain = :all
|
26
|
+
|
24
27
|
# Logic this project uses to resolve a variant for a given experiment.
|
25
28
|
@variant_resolver = lambda do |requested_variant|
|
26
|
-
requested_variant ||
|
29
|
+
requested_variant || :control
|
27
30
|
end
|
28
31
|
|
29
32
|
# Tracking behavior can be implemented to link an event to an experiment.
|
@@ -31,8 +34,7 @@ module Gitlab
|
|
31
34
|
Configuration.logger.info "Gitlab::Experiment[#{name}] #{event}: #{args.merge(signature: signature)}"
|
32
35
|
end
|
33
36
|
|
34
|
-
# Called at the end of every experiment run, with the
|
35
|
-
# want to push the experiment into the client or push results elsewhere.
|
37
|
+
# Called at the end of every experiment run, with the result.
|
36
38
|
@publishing_behavior = lambda do |_result|
|
37
39
|
track(:assignment)
|
38
40
|
end
|
@@ -43,22 +45,17 @@ module Gitlab
|
|
43
45
|
Digest::MD5.hexdigest(([name] + hash_map.keys + values).join('|'))
|
44
46
|
end
|
45
47
|
|
46
|
-
# The domain for which this cookie applies so you can restrict to the domain level.
|
47
|
-
# When not set, it uses the current host. If you want to provide specific hosts, you can
|
48
|
-
# provide them either via an array like `['www.gitlab.com', .gitlab.com']`, or set it to `:all`.
|
49
|
-
@cookie_domain = :all
|
50
|
-
|
51
48
|
class << self
|
52
49
|
attr_accessor(
|
53
50
|
:name_prefix,
|
54
51
|
:logger,
|
55
52
|
:base_class,
|
56
53
|
:cache,
|
54
|
+
:cookie_domain,
|
57
55
|
:variant_resolver,
|
58
56
|
:tracking_behavior,
|
59
57
|
:publishing_behavior,
|
60
|
-
:context_hash_strategy
|
61
|
-
:cookie_domain
|
58
|
+
:context_hash_strategy
|
62
59
|
)
|
63
60
|
end
|
64
61
|
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.4.
|
4
|
+
version: 0.4.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- GitLab
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-11-
|
11
|
+
date: 2020-11-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|