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.
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
- 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 variant and promote it as the new default code path. Or revert back to the control if no variant outperformed it. You can read our [Experiment Guide](https://docs.gitlab.com/ee/development/experiment_guide/) docs if you're curious about how we use this gem internally.
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
- This library provides a clean and elegant DSL (domain specific language) to define, run, and track your GitLab experiment.
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
- Candidate and variant are the same concept, but simplify how we speak about experimental paths -- this concept is also widely referred to as the "experiment group".<br clear="all">
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 an example let's make one up. Let's run an experiment on what we render for disabling desktop notifications.
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 this will make the action more clear and will help in making a choice about if that's what the user really wants to do.
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
- We'll name our experiment `notification_toggle`. This name is prefixed based on configuration. If you've set `config.name_prefix = 'gitlab'`, the experiment name would be `gitlab_notification_toggle` elsewhere.
48
+ So let's name our experiment `pill_color`, and use the generator to get some files:
47
49
 
48
- When you implement an experiment you'll need to provide a name, and a context. The name can show up in tracking calls, and potentially other aspects. The context determines the variant assigned, and should be consistent between calls. We'll discuss migrating context in later examples.
50
+ ```shell
51
+ $ rails generate gitlab:experiment pill_color
52
+ ```
49
53
 
50
- A context "key" represents the unique id of a context. It allows us to give the same experience between different calls to the experiment and can be used in caching.
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
- Now in our experiment we're going to render one of two views: the control will be our current view, and the candidate will be the new toggle button with a confirmation flow.
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 SubscriptionsController < ApplicationController
56
- def show
57
- experiment(:notification_toggle, actor: user) do |e|
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
- You can define the experiment using simple control/candidate paths, or provide named variants.
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
- Handling [multivariate](https://en.wikipedia.org/wiki/Multivariate_statistics) experiments is up to the configuration you provide around resolving variants. But in our example we may want to try with and without the confirmation. We can run any number of variations in our experiments this way.
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
- ```ruby
70
- experiment(:notification_toggle, actor: user) do |e|
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
- Understanding how an experiment can change behavior is important in evaluating its performance.
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
- To this end, we track events that are important by calling the same experiment elsewhere in code. By using the same context, you'll have consistent behavior and the ability to track events to it.
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
- experiment(:notification_toggle, actor: user).track(:clicked_button)
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
- ### Custom experiments
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
- 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`).
87
+ ```haml
88
+ %button{ class: pill_color_experiment.run } Click Me!
89
+ ```
88
90
 
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.
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
- You can generate a custom experiment by running:
93
+ ```ruby
94
+ experiment(:pill_color, actor: current_user).track(:clicked)
95
+ ```
92
96
 
93
- ```shell
94
- $ rails generate gitlab:experiment NotificationToggle control candidate
95
- ```
97
+ ## Advanced experimentation
96
98
 
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.
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 you can introduce once you have a custom experiment defined.
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 NotificationToggleExperiment < ApplicationExperiment
103
- # Exclude any users that aren't me.
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 candidate, without
107
- # asking the variant resolver to decide which variant to provide.
108
- segment :old_account?, variant: :candidate
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
- # Define the default control behavior, which can be overridden at
111
- # experiment time.
120
+ private
121
+
122
+ # Define the default control behavior, which can be overridden on runs.
112
123
  def control_behavior
113
- # render_toggle
124
+ 'grey'
114
125
  end
115
126
 
116
- # Define the default candidate behavior, which can be overridden
117
- # at experiment time.
118
- def candidate_behavior
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
- private
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.first_name == 'Richard'
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
- exp = experiment(:notification_toggle, actor: user)
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
- exp.run
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
- 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.
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
- ```ruby
147
- experiment(:notification_toggle, actor: user) do |e|
148
- e.use { render_special_toggle } # override default control behavior
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 to use for segmentation...</summary>
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. It's important to know what this might do to your data during rollout, so use this with careful consideration.
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 provided (including `:control`) it will be cached for that context, if caching is enabled.
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
- experiment(:notification_toggle, :no_interface, actor: user) do |e|
161
- e.use { render_toggle } # control
162
- e.try { render_button } # candidate
163
- e.try(:no_interface) { no_interface! } # no_interface variant
164
- end
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
- Or you can set the variant within the block. This allows using unique segmentation logic or variant resolution if you need it.
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
- experiment(:notification_toggle, actor: user) do |e|
171
- # Variant selection must be done before calling run or track.
172
- e.variant(:no_interface) # set the variant
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
- Or it can be specified in the call to run if you call it from within the block.
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
- experiment(:notification_toggle, actor: user) do |e|
181
- # ...
182
- # Variant selection can be specified when calling run.
183
- e.run(:no_interface)
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
- </details>
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
- ### Segmentation rules
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
- This library comes with the capability to segment contexts into a specific variant, before asking the variant resolver which variant to provide.
235
+ ### Run callbacks
192
236
 
193
- Segmentation can be achieved by using a custom experiment class and specifying the segmentation rules at a class level.
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 ExampleExperiment < ApplicationExperiment
197
- segment(variant: :variant_one) { context.actor.first_name == 'Richard' }
198
- segment :old_account?, variant: :variant_two
240
+ class PillColorExperiment < Gitlab::Experiment # OR ApplicationExperiment
241
+ # ...registered behaviors
199
242
 
200
- private
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 examples, any user named `'Richard'` would always receive the experience defined in "variant_one". As well, any account older than 2 weeks old would get the alternate experience defined in "variant_two".
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
- 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.
249
+ ```ruby
250
+ class PillColorExperiment < Gitlab::Experiment # OR ApplicationExperiment
251
+ # ...registered behaviors
211
252
 
212
- This means that any user named `'Richard'`, regardless of account age, will always be provided the experience as defined in "variant_one". If you wanted the opposite logic, you can flip the order.
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
- ### Exclusion rules
261
+ ### Rollout strategies
215
262
 
216
- Exclusion rules are similar to segmentation rules, but are intended to determine if a context should even be considered as something we should track events towards. Exclusion means we don't care about the events in relation to the given context.
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 ExampleExperiment < ApplicationExperiment
220
- exclude :old_account?, ->{ context.actor.first_name == 'Richard' }
221
-
222
- private
266
+ class PillColorExperiment < Gitlab::Experiment # OR ApplicationExperiment
267
+ # ...registered behaviors
223
268
 
224
- def old_account?
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
- The previous examples will exclude all users named `'Richard'` as well as any account older than 2 weeks old. Not only will they be given the control behavior, but no events will be tracked in these cases as well.
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
- You may need to check exclusion in custom tracking logic by calling `should_track?`:
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
- def expensive_tracking_logic
236
- return unless should_track?
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
- 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.
319
+ ### Experiment stickiness
243
320
 
244
- ### Return value
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
- 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.
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(:notification_toggle) do |e|
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
- ### Including the DSL
331
+ ### Experiment signature
257
332
 
258
- By default, `Gitlab::Experiment` injects itself into the controller and view layers. This exposes the `experiment` method application wide in those layers.
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
- Some experiments may extend outside of those layers, so you may want to include it elsewhere. For instance in a mailer, service object, background job, or similar.
335
+ ```ruby
336
+ experiment(:example).signature # => {:variant=>"control", :experiment=>"example", :key=>"4d7aee..."}
337
+ ```
261
338
 
262
- Note: In a lot of these contexts you may not have a reference to the request (unless you pass it in, or provide access to it) which may be needed if you want to enable cookie behaviors and track that through to user conversion.
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
- ```ruby
265
- class WelcomeMailer < ApplicationMailer
266
- include Gitlab::Experiment::Dsl # include the `experiment` method
341
+ ### Return value
267
342
 
268
- def welcome
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
- ex = experiment(:project_suggestions, actor: @user) do |e|
272
- e.use { 'welcome' }
273
- e.try { 'welcome_with_project_suggestions' }
274
- end
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
- mail(to: @user.email, subject: 'Welcome!', template: ex.run)
277
- end
278
- end
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
- # Migrate just the `:version` portion of the previous context, `{ actor: project, version: 1 }`:
291
- experiment(:example, actor: project, version: 2, migrated_with: { version: 1 })
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
- # Migrate the full context from `{ actor: project }` to `{ actor: project, version: 1 }`:
300
- experiment(:example, actor: project, version: 1, migrated_from: { actor: project })
301
- ```
381
+ # First implementation.
382
+ experiment(:example, actor: current_user)
302
383
 
303
- This can impact an experience if you:
304
-
305
- 1. haven't implemented the concept of migrations in your variant resolver
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
- ### When there isn't an actor (cookie fallback)
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
- When there isn't an identifying key in the context (this is `actor` by default), we fall back to cookies to provide a consistent experience for the client viewing them.
390
+ An example of our experiment signature when we migrate would include the `migration_keys` property:
311
391
 
312
- Once we assign a certain variant to a context, we need to always provide the same experience. We achieve this by setting a cookie for the experiment in question, but only when needed.
392
+ ```ruby
393
+ ex = experiment(:example, version: 1)
394
+ ex.signature # => {:key=>"20d69a...", ...}
313
395
 
314
- 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.
396
+ ex = experiment(:example, version: 2, migrated_from: { version: 1 })
397
+ ex.signature # => {:key=>"9e9d93...", :migration_keys=>["20d69a..."], ...}
398
+ ```
315
399
 
316
- 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.
400
+ ### Cookies and the actor keyword
317
401
 
318
- You'll need to provide the `request` as an option to the experiment if it's outside of the controller and views.
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
- ```ruby
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
- The cookie isn't set if the `actor` key isn't present at all in the context. Meaning that when no `actor` key is provided, the cookie will not be set.
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 and is nil, so the cookie is set and used
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 set to a value, so no cookie is set
334
- experiment(:example, actor: user, project: project)
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
- For edge cases, you can pass the cookie through by assigning it yourself -- e.g. `actor: request.cookie_jar.signed['example_actor']`. The cookie name is the full experiment name (including any configured prefix) with `_actor` appended -- e.g. `gitlab_notification_toggle_actor` for the `:notification_toggle` experiment key with a configured prefix of `gitlab`.
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
- ## How it works
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
- The way the gem works is best described using the following decision tree illustration. When an experiment is run, the following logic is executed to resolve what experience should be provided, given how the experiment is defined, and the context provided.
342
-
343
- ```mermaid
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
- classDef included fill:#380d75,color:#ffffff,stroke:none
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
- class VariantA,VariantB,VariantC included
366
- class Control,Excluded excluded
367
- class Cached cached
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
- This gem needs to be configured before being used in a meaningful way.
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
- The default configuration will always render the control, so it's important to configure your own logic for resolving variants.
445
+ Simple documentation can be found in the provided [initializer](lib/generators/gitlab/experiment/install/templates/initializer.rb.tt).
375
446
 
376
- Yes, the most important aspect of the gem -- that of determining which variant to render and when -- is up to you. Consider using [Unleash](https://github.com/Unleash/unleash-client-ruby) or [Flipper](https://github.com/jnunemaker/flipper) for this.
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
- # The block here is evaluated within the scope of the experiment instance,
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
- More examples for configuration are available in the provided [rails initializer](lib/generators/gitlab/experiment/install/templates/initializer.rb.tt).
461
+ The gem includes the following cache stores, which are documented in the implementation:
395
462
 
396
- ### Client layer / JavaScript
463
+ - [`RedisHashStore`](lib/gitlab/experiment/cache/redis_hash_store.rb): Useful if using redis
397
464
 
398
- This library doesn't attempt to provide any logic for the client layer.
465
+ ### Rollout strategies
399
466
 
400
- Instead it allows you to do this yourself in configuration. Using [Gon](https://github.com/gazay/gon) to publish your experiment information to the client layer is pretty simple.
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
- Gitlab::Experiment.configure do |config|
404
- config.publishing_behavior = lambda do |_result|
405
- # Push the experiment knowledge into the front end. The signature contains
406
- # the context key, and the variant that has been determined.
407
- Gon.push({ experiment: { name => signature } }, true)
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
- In the client you can now access `window.gon.experiment.notificationToggle`.
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
- ### Caching
498
+ Anyway, now you can use your custom `Flipper` rollout strategy by instantiating it in configuration:
415
499
 
416
- 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. Migrating the cache through context migrations is handled automatically, and this helps ensure an experiment experience remains consistent.
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
- It's important to understand that using caching can drastically change or override your rollout strategy logic.
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.configure do |config|
422
- config.cache = Rails.cache
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
- ### Middleware
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
- 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.
522
+ ```ruby
523
+ experiment(:pill_color).enabled? # => false
524
+ experiment(:pill_color).assigned.name # => "control"
429
525
 
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.
526
+ # Now we can enable the feature flag to enable the experiment.
527
+ Flipper.enable(:pill_color) # => true
431
528
 
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.
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
- ex = experiment(:example, foo: :bar)
548
+ Once configured to be mounted, the experiment tracking redirect URLs can be generated using the Rails route helpers.
440
549
 
441
- # using rails path/url helpers
442
- experiment_redirect_path(ex, 'https//docs.gitlab.com/') # => /experiment/example:[context_key]?https//docs.gitlab.com/
550
+ ```ruby
551
+ ex = experiment(:example)
443
552
 
444
- # manually
445
- "#{Gitlab::Experiment.configure.mount_at}/#{ex.to_param}?https//docs.gitlab.com/"
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
- URLS that match the base path will be handled by the middleware and will redirect to the provided redirect path.
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. These are in flux at the time of writing.
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
- This mixes in some of the basics, but the matchers and other aspects need to be included. This happens automatically for files in `spec/experiments`, but for other files and specs you want to include it in, you can specify the `:experiment` type:
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
- it "tests", :experiment do
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 experiments using `stub_experiments`. Pass it a hash using experiment names as the keys and the variants you want each to resolve to as the values:
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
- # Ensures the experiments named `:example` & `:example2` are both
473
- # "enabled" and that each will resolve to the given variant
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(:example2) do |e|
483
- e.enabled? # => true
484
- e.variant.name # => 'control'
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
- ### Exclusion and segmentation matchers
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
- class ExampleExperiment < ApplicationExperiment
494
- exclude { context.actor.first_name == 'Richard' }
495
- segment(variant: :candidate) { context.actor.username == 'jejacks0n' }
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
- excluded = double(username: 'rdiggitty', first_name: 'Richard')
499
- segmented = double(username: 'jejacks0n', first_name: 'Jeremy')
607
+ ### Registered behaviors matcher
500
608
 
501
- # exclude matcher
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
- # segment matcher
506
- expect(experiment(:example)).to segment(actor: segmented).into(:candidate)
507
- expect(experiment(:example)).not_to segment(actor: excluded)
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
- ### Tracking matcher
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 do this on the instance level or at an "any instance" level. At an instance level this is pretty straight forward:
622
+ You can also easily test your experiment classes using the `exclude`, `segment` metchers.
515
623
 
516
624
  ```ruby
517
- subject = experiment(:example)
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
- expect(subject).to track(:my_event)
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
- subject.track(:my_event)
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
- You can use the `on_any_instance` chain method to specify that it could happen on any instance of the experiment. This can be useful if you're calling `experiment(:example).track` downstream:
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
- expect(experiment(:example)).to track(:my_event).on_any_instance
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
- experiment(:example).track(:my_event)
652
+ subject.track(:clicked)
653
+ end
530
654
  ```
531
655
 
532
- And here's a full example of the methods that can be chained onto the `track` matcher:
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
- expect(experiment(:example)).to track(:my_event, value: 1, property: '_property_')
536
- .on_any_instance
537
- .with_context(foo: :bar)
538
- .for(:variant_name)
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(:example, :variant_name, foo: :bar).track(:my_event, value: 1, property: '_property_')
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 checking out the repo, run `bundle install` to install dependencies.
558
- Then, run `bundle exec rake` to run the tests. You can also run `bundle exec pry` for an
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***