gitlab-experiment 0.6.4 → 0.7.1
Sign up to get free protection for your applications and to get access to all the features.
- 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/README.md
CHANGED
@@ -3,9 +3,11 @@ GitLab Experiment
|
|
3
3
|
|
4
4
|
<img alt="experiment" src="/uploads/60990b2dbf4c0406bbf8b7f998de2dea/experiment.png" align="right" width="40%">
|
5
5
|
|
6
|
-
|
6
|
+
This README represents the current main branch and may not be applicable to the release you're using in your project. Please refer to the correct release branch if you'd like to review documentation relevant for that release.
|
7
7
|
|
8
|
-
|
8
|
+
Here at GitLab we run experiments as A/B/n tests and review the data the experiment generates. From that data, we determine the best performing code path and promote it as the new default code path, or revert back to the original code path. You can read our [Experiment Guide](https://docs.gitlab.com/ee/development/experiment_guide/) documentation if you're curious about how we use this gem internally at GitLab.
|
9
|
+
|
10
|
+
This library provides a clean and elegant DSL (domain specific language) to define, run, and track your experiments.
|
9
11
|
|
10
12
|
When we discuss the behavior of this gem, we'll use terms like experiment, context, control, candidate, and variant. It's worth defining these terms so they're more understood.
|
11
13
|
|
@@ -14,8 +16,12 @@ When we discuss the behavior of this gem, we'll use terms like experiment, conte
|
|
14
16
|
- `control` is the default, or "original" code path.
|
15
17
|
- `candidate` defines that there's one experimental code path.
|
16
18
|
- `variant(s)` is used when more than one experimental code path exists.
|
19
|
+
- `behaviors` is used to reference all possible code paths of an experiment.
|
20
|
+
|
21
|
+
Candidate and variant are the same concept, but simplify how we speak about experimental paths -- both are widely referred to as the "experiment group". If you use "control and candidate," the assumption is an A/B test, and if you say "variants" the assumption is a multiple variant experiment.
|
17
22
|
|
18
|
-
|
23
|
+
Behaviors is a general term for all code paths -- if that's control and candidate, or control and variants. It's all of them.
|
24
|
+
<br clear="all">
|
19
25
|
|
20
26
|
[[_TOC_]]
|
21
27
|
|
@@ -35,247 +41,320 @@ $ rails generate gitlab:experiment:install
|
|
35
41
|
|
36
42
|
## Implementing an experiment
|
37
43
|
|
38
|
-
For the sake of
|
39
|
-
|
40
|
-
In our control (current world) we show a simple toggle interface labeled, "Notifications." In our experiment we want a "Turn on/off desktop notifications" button with a confirmation.
|
41
|
-
|
42
|
-
The behavior will be the same, but the interface will be different and may involve more or fewer steps.
|
44
|
+
For the sake of having a simple example let's define an experiment around a button color, even if it's not how we'd likely implement a real experiment it makes for a clean example.
|
43
45
|
|
44
|
-
Our hypothesis is that
|
46
|
+
Currently our button is blue, but we want to test out a red variant. Our hypothesis is that a red button will make it more visible but also might appear like a warning button -- this is why we're testing our hypothesis.
|
45
47
|
|
46
|
-
|
48
|
+
So let's name our experiment `pill_color`, and use the generator to get some files:
|
47
49
|
|
48
|
-
|
50
|
+
```shell
|
51
|
+
$ rails generate gitlab:experiment pill_color
|
52
|
+
```
|
49
53
|
|
50
|
-
|
54
|
+
This generator will give us an `app/experiments/pill_color_experiment.rb` file, which we can use to define our experiment class and register our default variant behaviors. The generator also provides a bunch of useful comments in this file unless you `--skip-comments`, so feel free to play around with that.
|
51
55
|
|
52
|
-
|
56
|
+
Let's fill out our control and candidate behaviors with some default values to start. For our example it'll just be some made up class names where we've defined our colors.
|
53
57
|
|
54
58
|
```ruby
|
55
|
-
class
|
56
|
-
|
57
|
-
|
58
|
-
e.use { render_toggle } # control
|
59
|
-
e.try { render_button } # candidate
|
60
|
-
end
|
61
|
-
end
|
59
|
+
class PillColorExperiment < ApplicationExperiment
|
60
|
+
control { 'blue' } # register and define our default control value
|
61
|
+
candidate { 'red' } # register and define our experimental candidate value
|
62
62
|
end
|
63
63
|
```
|
64
64
|
|
65
|
-
|
65
|
+
Now that we've defined our default behaviors, we can utilize our experiment elsewhere by calling the `experiment` method. When you run (or publish) an experiment you'll need to provide a name and a context. The name is how our `PillColorExperiment` class is resolved and will also show up in event data downstream. The context is a hash that's used to determine the variant assigned and should be consistent between calls.
|
66
66
|
|
67
|
-
|
67
|
+
For our experiment we're going to make it "sticky" to the current user -- and if there isn't a current user, we want to assign and use a cookie value instead. This happens automatically if you use the [`actor` keyword](#cookies-and-the-actor-keyword) in your context. We use that in a lot of our examples, but it's by no means how everything should be done.
|
68
68
|
|
69
|
-
```
|
70
|
-
experiment(:
|
71
|
-
e.use { render_toggle } # control
|
72
|
-
e.try(:variant_one) { render_button(confirmation: true) }
|
73
|
-
e.try(:variant_two) { render_button(confirmation: false) }
|
74
|
-
end
|
69
|
+
```haml
|
70
|
+
%button{ class: experiment(:pill_color, actor: current_user).run } Click Me!
|
75
71
|
```
|
76
72
|
|
77
|
-
|
73
|
+
Now when our view is rendered, the class attribute of the button will be pulled from the experiment and will be sticky to the user that's viewing it.
|
78
74
|
|
79
|
-
|
75
|
+
You can also provide behavior overrides when you run the experiment. Let's use a view helper for this example because it'll be cleaner than putting our override blocks into the view.
|
80
76
|
|
81
77
|
```ruby
|
82
|
-
|
78
|
+
def pill_color_experiment
|
79
|
+
experiment(:pill_color, actor: current_user) do |e|
|
80
|
+
e.candidate { 'purple' } # override the candidate default of 'red'
|
81
|
+
end
|
82
|
+
end
|
83
83
|
```
|
84
84
|
|
85
|
-
|
85
|
+
Now we can run the experiment using that helper method. Instead of a red button, users who are assigned the candidate will be given a purple button. Users in the control group will still see the blue button. Using experiments in this way permits a default experiment to be defined while also allowing the experiment to be customized in the places where its run, using the scope, variables and helper methods that are available to us where we want to run the experiment.
|
86
86
|
|
87
|
-
|
87
|
+
```haml
|
88
|
+
%button{ class: pill_color_experiment.run } Click Me!
|
89
|
+
```
|
88
90
|
|
89
|
-
|
91
|
+
Understanding how an experiment can change user behavior or engagement is important in evaluating its performance. To this end, you'll probably want to track events that are important elsewhere in code. By using the same context you can provide a consistent experience and have the ability to anonymously track events to that experience.
|
90
92
|
|
91
|
-
|
93
|
+
```ruby
|
94
|
+
experiment(:pill_color, actor: current_user).track(:clicked)
|
95
|
+
```
|
92
96
|
|
93
|
-
|
94
|
-
$ rails generate gitlab:experiment NotificationToggle control candidate
|
95
|
-
```
|
97
|
+
## Advanced experimentation
|
96
98
|
|
97
|
-
|
99
|
+
Now let's create a more complex experiment with multiple variants. This time we'll start with a grey button, and have red and blue variants.
|
98
100
|
|
99
|
-
Here are some examples of what
|
101
|
+
We'll also do more advanced segmentation when the experiment is run, exclude certain cases, and register callbacks that will be executed after our experiment is run. Here are some examples of what we can start to do in our experiment classes:
|
100
102
|
|
101
103
|
```ruby
|
102
|
-
class
|
103
|
-
#
|
104
|
+
class PillColorExperiment < Gitlab::Experiment # OR ApplicationExperiment
|
105
|
+
# Register our behaviors.
|
106
|
+
control # register our control, which will by default call #control_behavior
|
107
|
+
variant(:red) # register the red variant that will call #red_behavior
|
108
|
+
variant(:blue) # register the blue variant that will call #blue_behavior
|
109
|
+
|
110
|
+
# Exclude any users that are named "Richard".
|
104
111
|
exclude :users_named_richard
|
105
112
|
|
106
|
-
# Segment any account older than 2 weeks into the
|
107
|
-
#
|
108
|
-
segment :old_account?, variant: :
|
113
|
+
# Segment any account older than 2 weeks into the red variant without asking
|
114
|
+
# the rollout strategy to assign a variant.
|
115
|
+
segment :old_account?, variant: :red
|
116
|
+
|
117
|
+
# After the experiment has been run, we want to log some performance metrics.
|
118
|
+
after_run { log_performance_metrics }
|
109
119
|
|
110
|
-
|
111
|
-
|
120
|
+
private
|
121
|
+
|
122
|
+
# Define the default control behavior, which can be overridden on runs.
|
112
123
|
def control_behavior
|
113
|
-
|
124
|
+
'grey'
|
114
125
|
end
|
115
126
|
|
116
|
-
# Define the default
|
117
|
-
|
118
|
-
|
119
|
-
# render_button
|
127
|
+
# Define the default red behavior, which can be overridden on runs.
|
128
|
+
def red_behavior
|
129
|
+
'red'
|
120
130
|
end
|
121
131
|
|
122
|
-
|
132
|
+
# Define the default blue behavior, which can be overridden on runs.
|
133
|
+
def blue_behavior
|
134
|
+
'blue'
|
135
|
+
end
|
123
136
|
|
137
|
+
# Define our special exclusion logic.
|
124
138
|
def users_named_richard
|
125
|
-
context.actor
|
139
|
+
context.try(:actor)&.first_name == 'Richard' # use try for nil actors
|
126
140
|
end
|
127
141
|
|
142
|
+
# Define our segmentation logic.
|
128
143
|
def old_account?
|
129
|
-
context.actor.created_at < 2.weeks.ago
|
144
|
+
context.try(:actor) && context.actor.created_at < 2.weeks.ago
|
145
|
+
end
|
146
|
+
|
147
|
+
# Let's assume that we've tracked this and want to push it into some system.
|
148
|
+
def log_performance_metrics
|
149
|
+
# ...hypothetical implementation
|
130
150
|
end
|
131
151
|
end
|
152
|
+
```
|
153
|
+
|
154
|
+
You can play around with our new `PillColorExperiment` using a console or irb session. In our example we've used an actual `User` model with a first name and timestamps. Feel free to use something more appropriate for your project in your exploration, or just not pass in `actor` at all.
|
155
|
+
|
156
|
+
```ruby
|
157
|
+
include Gitlab::Experiment::Dsl
|
132
158
|
|
133
159
|
# The class will be looked up based on the experiment name provided.
|
134
|
-
|
135
|
-
exp # => instance of NotificationToggleExperiment
|
160
|
+
ex = experiment(:pill_color, actor: User.first) # => #<PillColorExperiment:0x...>
|
136
161
|
|
137
162
|
# Run the experiment -- returning the result.
|
138
|
-
|
139
|
-
|
140
|
-
# Track an event on the experiment we've defined.
|
141
|
-
exp.track(:clicked_button)
|
142
|
-
```
|
163
|
+
ex.run # => "grey" (the value defined in our control)
|
143
164
|
|
144
|
-
|
165
|
+
# Track an event on the experiment we've defined, using the logic we've defined
|
166
|
+
# in configuration.
|
167
|
+
ex.track(:clicked) # => true
|
145
168
|
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
end
|
169
|
+
# Publish the experiment without running it, using the logic we've defined in
|
170
|
+
# configuration.
|
171
|
+
ex.publish # => {:variant=>"control", :experiment=>"pill_color", :key=>"45f595...", :excluded=>false}
|
150
172
|
```
|
151
173
|
|
152
174
|
<details>
|
153
|
-
<summary>You can also specify the variant
|
175
|
+
<summary>You can also specify the variant manually...</summary>
|
154
176
|
|
155
|
-
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.
|
177
|
+
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.
|
156
178
|
|
157
|
-
Any time a specific variant is
|
179
|
+
Caching: It's important to understand what this might do to your data during rollout, so use this with careful consideration. Any time a specific variant is assigned manually, or through segmentation (including `:control`) it will be cached for that context. That means that if you manually assign `:control`, that context will never be moved out of the control unless you do it programmatically elsewhere.
|
158
180
|
|
159
181
|
```ruby
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
182
|
+
include Gitlab::Experiment::Dsl
|
183
|
+
|
184
|
+
# Assign the candidate manually.
|
185
|
+
ex = experiment(:pill_color, :red, actor: User.first) # => #<PillColorExperiment:0x..>
|
186
|
+
|
187
|
+
# Run the experiment -- returning the result.
|
188
|
+
ex.run # => "red"
|
189
|
+
|
190
|
+
# If caching is enabled this will remain sticky between calls.
|
191
|
+
experiment(:pill_color, actor: User.first).run # => "red"
|
165
192
|
```
|
166
193
|
|
167
|
-
|
194
|
+
</details>
|
195
|
+
|
196
|
+
### Exclusion rules
|
197
|
+
|
198
|
+
Exclusion rules let us determine if a context should even be considered as something to include in an experiment. If we're excluding something, it means that we don't want to run the experiment in that case. This can be useful if you only want to run experiments on new users for instance.
|
168
199
|
|
169
200
|
```ruby
|
170
|
-
|
171
|
-
#
|
172
|
-
|
173
|
-
|
201
|
+
class PillColorExperiment < Gitlab::Experiment # OR ApplicationExperiment
|
202
|
+
# ...registered behaviors
|
203
|
+
|
204
|
+
exclude :old_account?, ->{ context.actor.first_name == 'Richard' }
|
174
205
|
end
|
175
206
|
```
|
176
207
|
|
177
|
-
|
208
|
+
In the previous example, we'll exclude all users named `'Richard'` as well as any account older than 2 weeks old. Not only will they be immediately given the control behavior, but no events will be tracked in these cases either.
|
209
|
+
|
210
|
+
Exclusion rules are executed in the order they're defined. The first exclusion rule to produce a truthy result will halt execution of further exclusion checks.
|
211
|
+
|
212
|
+
Note: Although tracking calls will be ignored on all exclusions, you may want to check exclusion yourself in expensive custom logic by calling the `should_track?` or `excluded?` methods.
|
213
|
+
|
214
|
+
Note: When using exclusion rules it's important to understand that the control assignment is cached, which improves future experiment run performance but can be a gotcha around caching.
|
215
|
+
|
216
|
+
Note: Exclusion rules aren't the best way to determine if an experiment is enabled. There's an `enabled?` method that can be overridden to have a high-level way of determining if an experiment should be running and tracking at all. This `enabled?` check should be as efficient as possible because it's the first early opt out path an experiment can implement. This can be seen in [How it works](#how-it-works).
|
217
|
+
|
218
|
+
### Segmentation rules
|
219
|
+
|
220
|
+
Segmentation, or assigning certain variants in certain cases, is important to running experiments. This can be useful if you want to push a given population into a specific variant because you've already determined that variant is successful for that population.
|
178
221
|
|
179
222
|
```ruby
|
180
|
-
|
181
|
-
# ...
|
182
|
-
|
183
|
-
|
223
|
+
class PillColorExperiment < Gitlab::Experiment # OR ApplicationExperiment
|
224
|
+
# ...registered behaviors
|
225
|
+
|
226
|
+
segment(variant: :red) { context.actor.first_name == 'Richard' }
|
227
|
+
segment :old_account?, variant: :blue
|
184
228
|
end
|
185
229
|
```
|
186
230
|
|
187
|
-
|
231
|
+
In the previous example, any user named `'Richard'` would always receive the experience defined in the red variant. As well, any account older than 2 weeks would get the alternate experience defined in the blue variant.
|
188
232
|
|
189
|
-
|
233
|
+
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. This means that for our example, any user named `'Richard'` regardless of account age, will always be provided the experience as defined in the red variant.
|
190
234
|
|
191
|
-
|
235
|
+
### Run callbacks
|
192
236
|
|
193
|
-
|
237
|
+
Callbacks can be registered for when you want to execute logic before, after, or around when an experiment is run. These callbacks won't be called unless the experiment is actually run, meaning that exclusion rules take precedence.
|
194
238
|
|
195
239
|
```ruby
|
196
|
-
class
|
197
|
-
|
198
|
-
segment :old_account?, variant: :variant_two
|
240
|
+
class PillColorExperiment < Gitlab::Experiment # OR ApplicationExperiment
|
241
|
+
# ...registered behaviors
|
199
242
|
|
200
|
-
|
201
|
-
|
202
|
-
def old_account?
|
203
|
-
context.actor.created_at < 2.weeks.ago
|
204
|
-
end
|
243
|
+
after_run :log_performance_metrics, -> { publish_to_database }
|
205
244
|
end
|
206
245
|
```
|
207
246
|
|
208
|
-
In the previous
|
247
|
+
In the previous example, we're going to call the `log_performance_method`, and do a hypothetical publish to the database. If you want to do an `around_run`, you just need to call the block:
|
209
248
|
|
210
|
-
|
249
|
+
```ruby
|
250
|
+
class PillColorExperiment < Gitlab::Experiment # OR ApplicationExperiment
|
251
|
+
# ...registered behaviors
|
211
252
|
|
212
|
-
|
253
|
+
around_run do |experiment, block|
|
254
|
+
puts "- before #{experiment.name} run"
|
255
|
+
block.call
|
256
|
+
puts "- after #{experiment.name} run"
|
257
|
+
end
|
258
|
+
end
|
259
|
+
```
|
213
260
|
|
214
|
-
###
|
261
|
+
### Rollout strategies
|
215
262
|
|
216
|
-
|
263
|
+
While a default rollout strategy can be defined in configuration, it's useful to be able to override this per experiment if needed. You can do this by specifying a specific `default_rollout` override in your experiment class.
|
217
264
|
|
218
265
|
```ruby
|
219
|
-
class
|
220
|
-
|
221
|
-
|
222
|
-
private
|
266
|
+
class PillColorExperiment < Gitlab::Experiment # OR ApplicationExperiment
|
267
|
+
# ...registered behaviors
|
223
268
|
|
224
|
-
|
225
|
-
context.actor.created_at < 2.weeks.ago
|
226
|
-
end
|
269
|
+
default_rollout :random # randomly assign one of the registered behaviors
|
227
270
|
end
|
228
271
|
```
|
229
272
|
|
230
|
-
|
273
|
+
Obviously random assignment might not be the best rollout strategy for you, but you can define your own rollout strategies, or use one of the ones provided in the gem. You can read more about configuring the default rollout and how to write your own rollout strategies in the [configuration documentation](#rollout-strategies-1) for it.
|
274
|
+
|
275
|
+
## How it works
|
276
|
+
|
277
|
+
The way experiments work is best described using the following decision tree diagram. When an experiment is run, the following logic is executed to resolve what experience should be provided, given how the experiment is defined, and using the context passed to the experiment call.
|
278
|
+
|
279
|
+
```mermaid
|
280
|
+
graph TD
|
281
|
+
GP[General Pool/Population] --> Running?[Rollout Enabled?]
|
282
|
+
Running? -->|Yes| Cached?[Cached? / Pre-segmented?]
|
283
|
+
Running? -->|No| Excluded[Control / No Tracking]
|
284
|
+
Cached? -->|No| Excluded?
|
285
|
+
Cached? -->|Yes| Cached[Cached Value]
|
286
|
+
Excluded? -->|Yes / Cached| Excluded
|
287
|
+
Excluded? -->|No| Segmented?
|
288
|
+
Segmented? -->|Yes / Cached| VariantA
|
289
|
+
Segmented? -->|No| Rollout[Rollout Resolve]
|
290
|
+
Rollout --> Control
|
291
|
+
Rollout -->|Cached| VariantA
|
292
|
+
Rollout -->|Cached| VariantB
|
293
|
+
Rollout -->|Cached| VariantN
|
294
|
+
|
295
|
+
classDef included fill:#380d75,color:#ffffff,stroke:none
|
296
|
+
classDef excluded fill:#fca121,stroke:none
|
297
|
+
classDef cached fill:#2e2e2e,color:#ffffff,stroke:none
|
298
|
+
classDef default fill:#fff,stroke:#6e49cb
|
299
|
+
|
300
|
+
class VariantA,VariantB,VariantN included
|
301
|
+
class Control,Excluded excluded
|
302
|
+
class Cached cached
|
303
|
+
```
|
304
|
+
|
305
|
+
## Technical details
|
231
306
|
|
232
|
-
|
307
|
+
This library is intended to be powerful and easy to use, which can lead to some complex underpinnings in the implementation. Some of those implementation details are important to understand at a technical level when considering how you want to design your experiments.
|
308
|
+
|
309
|
+
### Including the DSL
|
310
|
+
|
311
|
+
By default, `Gitlab::Experiment` injects itself into the controller, view, and mailer layers. This exposes the `experiment` method application wide in those layers. Some experiments may extend outside of those layers however, so you may want to include it elsewhere. For instance in an irb session or the rails console, or in all your service objects, background jobs, or similar.
|
233
312
|
|
234
313
|
```ruby
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
track(:my_event, value: expensive_method_call)
|
314
|
+
class ApplicationJob < ActiveJob::Base
|
315
|
+
include Gitlab::Experiment::Dsl # include the `experiment` method for all jobs
|
239
316
|
end
|
240
317
|
```
|
241
318
|
|
242
|
-
|
319
|
+
### Experiment stickiness
|
243
320
|
|
244
|
-
|
321
|
+
Internally, experiments have what's referred to as the context "key" that represents the unique and anonymous id of a given context. This allows us to assign the same variant between different calls to the experiment, is used in caching and can be used in event data downstream. This context "key" is how an experiment remains "sticky" to a given context, and is an important aspect to understand.
|
322
|
+
|
323
|
+
You can specify what the experiment should be sticky to by providing the `:sticky_to` option. By default this will be the entire context, but this can be overridden manually if needed.
|
245
324
|
|
246
|
-
|
325
|
+
In a fabricated example, we might want to provide the user and a project to an experiment, but we want it to remain sticky to the project that the user is viewing and not the user viewing that project. This is a very powerful concept that you're encouraged to play around with to understand what it means, and how you can run complex experiments that aren't user centric.
|
247
326
|
|
248
327
|
```ruby
|
249
|
-
experiment(:
|
250
|
-
e.use { 'A' }
|
251
|
-
e.try { 'B' }
|
252
|
-
e.run
|
253
|
-
end # => 'A'
|
328
|
+
experiment(:example, actor: current_user, project: project, sticky_to: project)
|
254
329
|
```
|
255
330
|
|
256
|
-
###
|
331
|
+
### Experiment signature
|
257
332
|
|
258
|
-
|
333
|
+
The best way to understand the details of an experiment is through its signature. An example signature can be retrieved by calling the `signature` method, and looks like the following:
|
259
334
|
|
260
|
-
|
335
|
+
```ruby
|
336
|
+
experiment(:example).signature # => {:variant=>"control", :experiment=>"example", :key=>"4d7aee..."}
|
337
|
+
```
|
261
338
|
|
262
|
-
|
339
|
+
An experiment signature is useful when tracking events and when using experiments on the client layer. The signature can also contain the optional `migration_keys`, and `excluded` properties.
|
263
340
|
|
264
|
-
|
265
|
-
class WelcomeMailer < ApplicationMailer
|
266
|
-
include Gitlab::Experiment::Dsl # include the `experiment` method
|
341
|
+
### Return value
|
267
342
|
|
268
|
-
|
269
|
-
@user = params[:user]
|
343
|
+
By default the return value of calling `experiment` is a `Gitlab::Experiment` instance, or whatever class the experiment is resolved to, which likely inherits from `Gitlab::Experiment`. In simple cases you may want only the results of running the experiment though. You can call `run` within the block to get the return value of the assigned variant.
|
270
344
|
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
345
|
+
```ruby
|
346
|
+
# Normally an experiment instance.
|
347
|
+
experiment(:example) do |e|
|
348
|
+
e.control { 'A' }
|
349
|
+
e.candidate { 'B' }
|
350
|
+
end # => #<Gitlab::Experiment:0x...>
|
275
351
|
|
276
|
-
|
277
|
-
|
278
|
-
|
352
|
+
# But calling `run` causes the return value to be the result.
|
353
|
+
experiment(:example) do |e|
|
354
|
+
e.control { 'A' }
|
355
|
+
e.candidate { 'B' }
|
356
|
+
e.run
|
357
|
+
end # => 'A'
|
279
358
|
```
|
280
359
|
|
281
360
|
### Context migrations
|
@@ -287,8 +366,11 @@ Take for instance, that you might be using `version: 1` in your context currentl
|
|
287
366
|
In providing the context migration data, we can resolve an experience and its events all the way back. This can also help in keeping our cache relevant.
|
288
367
|
|
289
368
|
```ruby
|
290
|
-
#
|
291
|
-
experiment(:example, actor:
|
369
|
+
# First implementation.
|
370
|
+
experiment(:example, actor: current_user, version: 1)
|
371
|
+
|
372
|
+
# Migrate just the `:version` portion.
|
373
|
+
experiment(:example, actor: current_user, version: 2, migrated_with: { version: 1 })
|
292
374
|
```
|
293
375
|
|
294
376
|
You can add or remove context by providing a `migrated_from` option. This approach expects a full context replacement -- i.e. what it was before you added or removed the new context key.
|
@@ -296,248 +378,292 @@ You can add or remove context by providing a `migrated_from` option. This approa
|
|
296
378
|
If you wanted to introduce a `version` to your context, provide the full previous context.
|
297
379
|
|
298
380
|
```ruby
|
299
|
-
#
|
300
|
-
experiment(:example, actor:
|
301
|
-
```
|
381
|
+
# First implementation.
|
382
|
+
experiment(:example, actor: current_user)
|
302
383
|
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
1. haven't enabled a reasonable caching mechanism
|
384
|
+
# Migrate the full context of `{ actor: current_user }` to `{ actor: current_user, version: 1 }`.
|
385
|
+
experiment(:example, actor: current_user, version: 1, migrated_from: { actor: current_user })
|
386
|
+
```
|
307
387
|
|
308
|
-
|
388
|
+
When you migrate context, this information is included in the signature of the experiment. This can be used downstream in event handling and reporting to resolve a series of events back to a single experience, while also keeping everything anonymous.
|
309
389
|
|
310
|
-
|
390
|
+
An example of our experiment signature when we migrate would include the `migration_keys` property:
|
311
391
|
|
312
|
-
|
392
|
+
```ruby
|
393
|
+
ex = experiment(:example, version: 1)
|
394
|
+
ex.signature # => {:key=>"20d69a...", ...}
|
313
395
|
|
314
|
-
|
396
|
+
ex = experiment(:example, version: 2, migrated_from: { version: 1 })
|
397
|
+
ex.signature # => {:key=>"9e9d93...", :migration_keys=>["20d69a..."], ...}
|
398
|
+
```
|
315
399
|
|
316
|
-
|
400
|
+
### Cookies and the actor keyword
|
317
401
|
|
318
|
-
|
402
|
+
We use cookies to auto migrate an unknown value into a known value, often in the case of the current user. The implementation of this uses the same concept outlined above with context migrations, but will happen automatically for you if you use the `actor` keyword.
|
319
403
|
|
320
|
-
|
321
|
-
experiment(:example, actor: user, request: request)
|
322
|
-
```
|
404
|
+
When you use the `actor: current_user` pattern in your context, the nil case is handled by setting a special cookie for the experiment and then deleting the cookie, and migrating the context key to the one generated from the user when they've signed in.
|
323
405
|
|
324
|
-
|
406
|
+
This cookie is a temporary, randomized uuid and isn't associated with a user. When we can finally provide an actor, the context is auto migrated from the cookie to that actor.
|
325
407
|
|
326
408
|
```ruby
|
327
|
-
# actor is not present, so no cookie is set
|
409
|
+
# The actor key is not present, so no cookie is set.
|
328
410
|
experiment(:example, project: project)
|
329
411
|
|
330
|
-
# actor is present
|
412
|
+
# The actor key is present but nil, so the cookie is set and used.
|
331
413
|
experiment(:example, actor: nil, project: project)
|
332
414
|
|
333
|
-
# actor is present and
|
334
|
-
|
415
|
+
# The actor key is present and isn't nil, so the cookie value (if found) is
|
416
|
+
# migrated forward and the cookie is deleted.
|
417
|
+
experiment(:example, actor: current_user, project: project)
|
335
418
|
```
|
336
419
|
|
337
|
-
|
420
|
+
Note: The cookie is deleted when resolved, but can be assigned again if the `actor` is ever nil again. A good example of this scenario would be on a sign in page. When a potential user arrives, they would never be known, so a cookie would be set for them, and then resolved/removed as soon as they signed in. This process would repeat each time they arrived while not being signed in and can complicate reporting unless it's handled well in the data layers.
|
338
421
|
|
339
|
-
|
422
|
+
Note: To read and write cookies, we provide the `request` from within the controller and views. The cookie migration will happen automatically if the experiment is within those layers. You'll need to provide the `request` as an option to the experiment if it's outside of the controller and views.
|
340
423
|
|
341
|
-
|
342
|
-
|
343
|
-
```
|
344
|
-
graph TD
|
345
|
-
GP[General Pool/Population] --> Enabled?
|
346
|
-
Enabled? -->|Yes| Cached?[Cached? / Pre-segmented?]
|
347
|
-
Enabled? -->|No| Excluded[Control / No Tracking]
|
348
|
-
Cached? -->|No| Excluded?
|
349
|
-
Cached? -->|Yes| Cached[Cached Value]
|
350
|
-
Excluded? -->|Yes / Cached| Excluded
|
351
|
-
Excluded? -->|No| Segmented?
|
352
|
-
Segmented? -->|Yes / Cached| VariantA
|
353
|
-
Segmented? -->|No| Included?[Experiment Group?]
|
354
|
-
Included? -->|Yes| Rollout
|
355
|
-
Included? -->|No| Control
|
356
|
-
Rollout -->|Cached| VariantA
|
357
|
-
Rollout -->|Cached| VariantB
|
358
|
-
Rollout -->|Cached| VariantC
|
424
|
+
```ruby
|
425
|
+
experiment(:example, actor: current_user, request: request)
|
426
|
+
```
|
359
427
|
|
360
|
-
|
361
|
-
classDef excluded fill:#fca121,stroke:none
|
362
|
-
classDef cached fill:#2e2e2e,color:#ffffff,stroke:none
|
363
|
-
classDef default fill:#fff,stroke:#6e49cb
|
428
|
+
Note: For edge cases, you can pass the cookie through by assigning it yourself -- e.g. `actor: request.cookie_jar.signed['example_id']`. The cookie name is the full experiment name (including any configured prefix) with `_id` appended -- e.g. `pill_color_id` for the `PillColorExperiment`.
|
364
429
|
|
365
|
-
|
366
|
-
|
367
|
-
|
430
|
+
### Client layer
|
431
|
+
|
432
|
+
Experiments that have been run (or published) during the request lifecycle can be pushed into to the client layer by injecting the published experiments into javascript in a layout or view using something like:
|
433
|
+
|
434
|
+
```haml
|
435
|
+
= javascript_tag(nonce: content_security_policy_nonce) do
|
436
|
+
window.experiments = #{raw ApplicationExperiment.published_experiments.to_json};
|
368
437
|
```
|
369
438
|
|
439
|
+
The `window.experiments` object can then be used in your client implementation to determine experimental behavior at that layer as well. For instance, we can now access the `window.experiments.pill_color` object to get the variant that was assigned, if the context was excluded, and to use the context key in our client side events.
|
440
|
+
|
370
441
|
## Configuration
|
371
442
|
|
372
|
-
|
443
|
+
The gem is meant to be configured when installed and is ambiguous about how it should track events, and what to do when publishing experiments. Some of the more important aspects are left up to you to implement the logic that's right for your project.
|
373
444
|
|
374
|
-
|
445
|
+
Simple documentation can be found in the provided [initializer](lib/generators/gitlab/experiment/install/templates/initializer.rb.tt).
|
375
446
|
|
376
|
-
|
447
|
+
Read on for comprehensive documentation on some of the more complex configuration options.
|
448
|
+
|
449
|
+
### Caching
|
450
|
+
|
451
|
+
Caching can be enabled in configuration and is implemented towards the `Rails.cache` / `ActiveSupport::Cache::Store` interface. When you enable caching, any variant resolution will be cached when something other than nil. Migrating the cache through context migrations is handled automatically, and this helps ensure an experiment experience remains consistent.
|
452
|
+
|
453
|
+
It's important to understand that using caching can drastically change or negate your specific rollout strategy logic.
|
377
454
|
|
378
455
|
```ruby
|
379
456
|
Gitlab::Experiment.configure do |config|
|
380
|
-
|
381
|
-
# which is why we are able to access things like name and context.
|
382
|
-
config.variant_resolver = lambda do |requested_variant|
|
383
|
-
# Return the requested variant if a specific one has been provided in code.
|
384
|
-
return requested_variant unless requested_variant.nil?
|
385
|
-
|
386
|
-
# Ask Unleash to determine the variant, given the context we've built,
|
387
|
-
# using the control as the fallback.
|
388
|
-
fallback = Unleash::Variant.new(name: 'control', enabled: true)
|
389
|
-
UNLEASH.get_variant(name, context.value, fallback)
|
390
|
-
end
|
457
|
+
config.cache = Rails.cache
|
391
458
|
end
|
392
459
|
```
|
393
460
|
|
394
|
-
|
461
|
+
The gem includes the following cache stores, which are documented in the implementation:
|
395
462
|
|
396
|
-
|
463
|
+
- [`RedisHashStore`](lib/gitlab/experiment/cache/redis_hash_store.rb): Useful if using redis
|
397
464
|
|
398
|
-
|
465
|
+
### Rollout strategies
|
399
466
|
|
400
|
-
|
467
|
+
There are some basic rollout strategies that come with the gem, and you can use these directly, or you can use them to help build your own custom rollout strategies. Each is documented more thoroughly in its implementation file.
|
468
|
+
|
469
|
+
- [`Base`](lib/gitlab/experiment/rollout.rb): Useful for building custom rollout strategies, not super useful by itself
|
470
|
+
- [`Percent`](lib/gitlab/experiment/rollout/percent.rb): A comprehensive percent based strategy, it's configured as the default
|
471
|
+
- [`Random`](lib/gitlab/experiment/rollout/random.rb): Random assignment can be useful on some experimentation
|
472
|
+
- [`RoundRobin`](lib/gitlab/experiment/rollout/round_robin.rb): Cycles through assignment using the cache to keep track of what was last assigned
|
473
|
+
|
474
|
+
The included rollout strategies are great, but you might want to write your own for your own project, which might already have nice tooling for toggling feature flags.
|
475
|
+
|
476
|
+
It's an important aspect of this library to be flexible with the approach you choose when determining if an experiment is enabled, and what variant should be assigned.
|
477
|
+
|
478
|
+
To that end, let's go ahead through an example of how to write up a custom rollout strategy that uses the [Flipper](https://github.com/jnunemaker/flipper) gem to manage rollout. For simpliticy in our example, we're going to start by inheriting from `Gitlab::Experiment::Rollout::Percent` because it's already useful.
|
401
479
|
|
402
480
|
```ruby
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
481
|
+
# We put it in this module namespace so we can get easy resolution when
|
482
|
+
# using `default_rollout :flipper` in our usage later.
|
483
|
+
module Gitlab::Experiment::Rollout
|
484
|
+
class Flipper < Percent
|
485
|
+
def enabled?
|
486
|
+
::Flipper.enabled?(experiment.name, self)
|
487
|
+
end
|
488
|
+
|
489
|
+
def flipper_id
|
490
|
+
"Experiment;#{id}"
|
491
|
+
end
|
408
492
|
end
|
409
493
|
end
|
410
494
|
```
|
411
495
|
|
412
|
-
|
496
|
+
So, Flipper needs something that responds to `flipper_id`, and since our experiment "id" (which is also our context key) is unique and consistent, we're going to give that to Flipper to manage things like percentage of actors etc. You might want to consider something more complex here if you're using things that can be flipper actors in your experiment context.
|
413
497
|
|
414
|
-
|
498
|
+
Anyway, now you can use your custom `Flipper` rollout strategy by instantiating it in configuration:
|
415
499
|
|
416
|
-
|
500
|
+
```ruby
|
501
|
+
Gitlab::Experiment.configure do |config|
|
502
|
+
config.default_rollout = Gitlab::Experiment::Rollout::Flipper.new(
|
503
|
+
include_control: true # specify to include control, which we want to do
|
504
|
+
)
|
505
|
+
end
|
506
|
+
```
|
417
507
|
|
418
|
-
|
508
|
+
Or if you don't want to make that change globally, you can use it in specific experiment classes:
|
419
509
|
|
420
510
|
```ruby
|
421
|
-
Gitlab::Experiment
|
422
|
-
|
511
|
+
class PillColorExperiment < Gitlab::Experiment # OR ApplicationExperiment
|
512
|
+
# ...registered behaviors
|
513
|
+
|
514
|
+
default_rollout :flipper,
|
515
|
+
include_control: true, # optionally specify to include control
|
516
|
+
distribution: { control: 26, red: 37, blue: 37 } # optionally specify distribution
|
423
517
|
end
|
424
518
|
```
|
425
519
|
|
426
|
-
|
520
|
+
Now, enabling or disabling the Flipper feature flag will control if the experiment is enabled or not. If the experiment is enabled, as determined by our custom rollout strategy, the standard resolutuon logic will be executed, and a variant (or control) will be assigned.
|
427
521
|
|
428
|
-
|
522
|
+
```ruby
|
523
|
+
experiment(:pill_color).enabled? # => false
|
524
|
+
experiment(:pill_color).assigned.name # => "control"
|
429
525
|
|
430
|
-
|
526
|
+
# Now we can enable the feature flag to enable the experiment.
|
527
|
+
Flipper.enable(:pill_color) # => true
|
431
528
|
|
432
|
-
|
529
|
+
experiment(:pill_color).enabled? # => true
|
530
|
+
experiment(:pill_color).assigned.name # => "red"
|
531
|
+
```
|
532
|
+
|
533
|
+
### Middleware
|
534
|
+
|
535
|
+
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 a middleware layer that can redirect to a given URL while also tracking that the URL was visited has been provided.
|
536
|
+
|
537
|
+
In Rails this middleware is mounted automatically, with a base path of what's been configured for `mount_at`. If this path is nil, the middleware won't be mounted at all.
|
433
538
|
|
434
539
|
```ruby
|
435
540
|
Gitlab::Experiment.configure do |config|
|
436
541
|
config.mount_at = '/experiment'
|
542
|
+
|
543
|
+
# Only redirect on permitted domains.
|
544
|
+
config.redirect_url_validator = ->(url) { (url = URI.parse(url)) && url.host == 'gitlab.com' }
|
437
545
|
end
|
546
|
+
```
|
438
547
|
|
439
|
-
|
548
|
+
Once configured to be mounted, the experiment tracking redirect URLs can be generated using the Rails route helpers.
|
440
549
|
|
441
|
-
|
442
|
-
|
550
|
+
```ruby
|
551
|
+
ex = experiment(:example)
|
443
552
|
|
444
|
-
#
|
445
|
-
|
446
|
-
|
553
|
+
# Generating the path/url using the path and url helper.
|
554
|
+
experiment_redirect_path(ex, url: 'https//gitlab.com/docs') # => "/experiment/example:20d69a...?https//gitlab.com/docs"
|
555
|
+
experiment_redirect_url(ex, url: 'https//gitlab.com/docs') # => "https://gitlab.com/experiment/example:20d69a...?https//gitlab.com/docs"
|
447
556
|
|
448
|
-
|
557
|
+
# Manually generating a url is a bit less clean, but is possible.
|
558
|
+
"#{Gitlab::Experiment::Configuration.mount_at}/#{ex.to_param}?https//docs.gitlab.com/"
|
559
|
+
```
|
449
560
|
|
450
561
|
## Testing (rspec support)
|
451
562
|
|
452
|
-
This gem comes with some rspec helpers and custom matchers.
|
453
|
-
|
454
|
-
First, require the rspec support file:
|
563
|
+
This gem comes with some rspec helpers and custom matchers. To get the experiment specific rspec support, require the rspec support file:
|
455
564
|
|
456
565
|
```ruby
|
457
566
|
require 'gitlab/experiment/rspec'
|
458
567
|
```
|
459
568
|
|
460
|
-
|
569
|
+
Any file in `spec/experiments` path will automatically get the experiment specific support, but it can also be included in other specs by adding the `:experiment` label:
|
461
570
|
|
462
571
|
```ruby
|
463
|
-
|
572
|
+
describe MyExampleController do
|
573
|
+
context "with my experiment", :experiment do
|
574
|
+
# experiment helpers and matchers will be available here.
|
575
|
+
end
|
464
576
|
end
|
465
577
|
```
|
466
578
|
|
467
579
|
### Stub helpers
|
468
580
|
|
469
|
-
You can stub
|
581
|
+
You can stub experiment variant resolution using the `stub_experiments` helper. Pass a hash of experiment names and the variant each should resolve to.
|
470
582
|
|
471
583
|
```ruby
|
472
|
-
|
473
|
-
|
474
|
-
# (`:my_variant` & `:control` respectively).
|
475
|
-
stub_experiments(example: :my_variant, example2: :control)
|
476
|
-
|
477
|
-
experiment(:example) do |e|
|
478
|
-
e.enabled? # => true
|
479
|
-
e.variant.name # => 'my_variant'
|
480
|
-
end
|
584
|
+
it "stubs experiments to resolve to a specific variant" do
|
585
|
+
stub_experiments(pill_color: :red)
|
481
586
|
|
482
|
-
experiment(:
|
483
|
-
|
484
|
-
|
485
|
-
end
|
587
|
+
experiment(:pill_color) do |e|
|
588
|
+
expect(e).to be_enabled
|
589
|
+
expect(e.assigned.name).to eq('red')
|
590
|
+
end
|
591
|
+
end
|
486
592
|
```
|
487
593
|
|
488
|
-
|
489
|
-
|
490
|
-
You can also easily test the exclusion and segmentation matchers.
|
594
|
+
In special cases you can use a boolean `true` instead of a variant name. This allows the rollout strategy to resolve the variant however it wants to, but is otherwise just making sure the experiment is considered enabled.
|
491
595
|
|
492
596
|
```ruby
|
493
|
-
|
494
|
-
|
495
|
-
|
597
|
+
it "stubs experiments while allowing the rollout strategy to assign the variant" do
|
598
|
+
stub_experiments(pill_color: true) # only stubs enabled?
|
599
|
+
|
600
|
+
experiment(:pill_color) do |e|
|
601
|
+
expect(e).to be_enabled
|
602
|
+
# expect(e.assigned.name).to eq([whatever the rollout strategy assigns])
|
603
|
+
end
|
496
604
|
end
|
605
|
+
```
|
497
606
|
|
498
|
-
|
499
|
-
segmented = double(username: 'jejacks0n', first_name: 'Jeremy')
|
607
|
+
### Registered behaviors matcher
|
500
608
|
|
501
|
-
|
502
|
-
expect(experiment(:example)).to exclude(actor: excluded)
|
503
|
-
expect(experiment(:example)).not_to exclude(actor: segmented)
|
609
|
+
It's useful to test our registered behaviors, as well as their return values when we implement anything complex in them. The `register_behavior` matcher is useful for this.
|
504
610
|
|
505
|
-
|
506
|
-
|
507
|
-
expect(experiment(:
|
611
|
+
```ruby
|
612
|
+
it "tests our registered behaviors" do
|
613
|
+
expect(experiment(:pill_color)).to register_behavior(:control)
|
614
|
+
.with('grey') # with a default return value of "grey"
|
615
|
+
expect(experiment(:pill_color)).to register_behavior(:red)
|
616
|
+
expect(experiment(:pill_color)).to register_behavior(:blue)
|
617
|
+
end
|
508
618
|
```
|
509
619
|
|
510
|
-
###
|
511
|
-
|
512
|
-
Tracking events is a major aspect of experimentation, and because of this we try to provide a flexible way to ensure your tracking calls are covered.
|
620
|
+
### Exclusion and segmentation matchers
|
513
621
|
|
514
|
-
You can
|
622
|
+
You can also easily test your experiment classes using the `exclude`, `segment` metchers.
|
515
623
|
|
516
624
|
```ruby
|
517
|
-
|
625
|
+
let(:excluded) { double(first_name: 'Richard', created_at: Time.current) }
|
626
|
+
let(:segmented) { double(first_name: 'Jeremy', created_at: 3.weeks.ago) }
|
518
627
|
|
519
|
-
|
628
|
+
it "tests the exclusion rules" do
|
629
|
+
expect(experiment(:pill_color)).to exclude(actor: excluded)
|
630
|
+
expect(experiment(:pill_color)).not_to exclude(actor: segmented)
|
631
|
+
end
|
520
632
|
|
521
|
-
|
633
|
+
it "tests the segmentation rules" do
|
634
|
+
expect(experiment(:pill_color)).to segment(actor: segmented)
|
635
|
+
.into(:red) # into a specific variant
|
636
|
+
expect(experiment(:pill_color)).not_to segment(actor: excluded)
|
637
|
+
end
|
522
638
|
```
|
523
639
|
|
524
|
-
|
640
|
+
### Tracking matcher
|
641
|
+
|
642
|
+
Tracking events is a major aspect of experimentation, and because of this we try to provide a flexible way to ensure your tracking calls are covered.
|
525
643
|
|
526
644
|
```ruby
|
527
|
-
|
645
|
+
before do
|
646
|
+
stub_experiments(pill_color: true) # stub the experiment so tracking is permitted
|
647
|
+
end
|
648
|
+
|
649
|
+
it "tests that we track an event on a specific instance" do
|
650
|
+
expect(subject = experiment(:pill_color)).to track(:clicked)
|
528
651
|
|
529
|
-
|
652
|
+
subject.track(:clicked)
|
653
|
+
end
|
530
654
|
```
|
531
655
|
|
532
|
-
|
656
|
+
You can use the `on_next_instance` chain method to specify that the tracking call could happen on the next instance of the experiment. This can be useful if you're calling `experiment(:example).track` downstream and don't have access to that instance. Here's a full example of the methods that can be chained onto the `track` matcher:
|
533
657
|
|
534
658
|
```ruby
|
535
|
-
|
536
|
-
.
|
537
|
-
|
538
|
-
|
659
|
+
it "tests that we track an event with specific details" do
|
660
|
+
expect(experiment(:pill_color)).to track(:clicked, value: 1, property: '_property_')
|
661
|
+
.on_next_instance # any time in the future
|
662
|
+
.with_context(foo: :bar) # with the expected context
|
663
|
+
.for(:red) # and assigned the correct variant
|
539
664
|
|
540
|
-
experiment(:
|
665
|
+
experiment(:pill_color, :red, foo: :bar).track(:clicked, value: 1, property: '_property_')
|
666
|
+
end
|
541
667
|
```
|
542
668
|
|
543
669
|
## Tracking, anonymity and GDPR
|
@@ -554,16 +680,13 @@ Each of these approaches could be desirable given the objectives of your experim
|
|
554
680
|
|
555
681
|
## Development
|
556
682
|
|
557
|
-
After
|
558
|
-
|
559
|
-
interactive prompt that will allow you to experiment.
|
683
|
+
After cloning the repo, run `bundle install` to install dependencies.
|
684
|
+
|
685
|
+
Run `bundle exec rake` to run the tests. You can also run `bundle exec pry` for an interactive prompt that will allow you to experiment.
|
560
686
|
|
561
687
|
## Contributing
|
562
688
|
|
563
|
-
Bug reports and merge requests are welcome on GitLab at
|
564
|
-
https://gitlab.com/gitlab-org/gitlab-experiment. This project is intended to be a
|
565
|
-
safe, welcoming space for collaboration, and contributors are expected to adhere
|
566
|
-
to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
689
|
+
Bug reports and merge requests are welcome on GitLab at https://gitlab.com/gitlab-org/ruby/gems/gitlab-experiment. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
567
690
|
|
568
691
|
## Release process
|
569
692
|
|
@@ -571,13 +694,10 @@ Please refer to the [Release Process](docs/release_process.md).
|
|
571
694
|
|
572
695
|
## License
|
573
696
|
|
574
|
-
The gem is available as open source under the terms of the
|
575
|
-
[MIT License](http://opensource.org/licenses/MIT).
|
697
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
576
698
|
|
577
699
|
## Code of conduct
|
578
700
|
|
579
|
-
Everyone interacting in the `Gitlab::Experiment` project’s codebases, issue trackers,
|
580
|
-
chat rooms and mailing lists is expected to follow the
|
581
|
-
[code of conduct](CODE_OF_CONDUCT.md).
|
701
|
+
Everyone interacting in the `Gitlab::Experiment` project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](CODE_OF_CONDUCT.md).
|
582
702
|
|
583
703
|
***Make code not war***
|