gitlab-experiment 0.2.3 → 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1a35b23202fa542fa548093c65ec4580e764fdbda2549291a54bfa3ed98cec96
4
- data.tar.gz: 05f4c1ed8c5761ab03ed1e9d763d6e612f661cf9ee8736e46fb0e48b947ed52d
3
+ metadata.gz: a6284349854da93da979695a6493df95c0247616e10d5adc047d56d5dbdbf324
4
+ data.tar.gz: 1e4456945313fce9fbf03250299e3a0eb1b406035e4cce0d0559eee1fb3cfe53
5
5
  SHA512:
6
- metadata.gz: 9505898d8870749dbccab8fa98974948895a9898838aecc622e9f3243624a889076b248f6c0698ad8d44c63726447cc1b46638158614bb008015a82ce42a8d19
7
- data.tar.gz: 3484817693dbaf9254d16083c6fbb745fbe47adc7868bf924bd418d8f67735fd26ac00f7f486aecc4f9ec84647f58e7bfcb0ff15199370b2e782d68cf2a0b2a2
6
+ metadata.gz: '08d5c04e817c2679d115a845df588a4b81358e1b7ac7ddc8282a4cdaa2a878f6a4111b318fa3155671fdd05c0cb950ab076210518142e5f8a04db89e7b164104'
7
+ data.tar.gz: 118de1ae6b9995edee4058a1b756030ee6659199bf19a29cf2c1446ddb87a1a8ff29f479cf9ff20f7af47ff7706bbc97256929ed800f803fc381450bcedbf82c
data/README.md CHANGED
@@ -1,5 +1,6 @@
1
- GitLab Experiment
2
- =================
1
+ # GitLab Experiment
2
+
3
+ <img alt="experiment" src="/uploads/60990b2dbf4c0406bbf8b7f998de2dea/experiment.png" align="right" width="40%">
3
4
 
4
5
  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.
5
6
 
@@ -13,25 +14,7 @@ When we discuss the behavior of this gem, we'll use terms like experiment, conte
13
14
  - `candidate` defines that there's one experimental code path.
14
15
  - `variant(s)` is used when more than one experimental code path exists.
15
16
 
16
- Candidate and variant are the same concept, but simplify how we speak about experimental paths.
17
-
18
- ## Opinionated elevator pitch
19
-
20
- The last time I travelled I went through a process at the airport when I arrived. My passport was checked, I was asked a series of questions about my visit, and was ushered to a digital kiosk where I was prompted with a few more questions on a touch screen.
21
-
22
- It was Iceland, and for the record, it's a beautiful place and you should visit if you haven't yet.
23
-
24
- Anyway, at various stages of this process travellers are presented with a physical emoji button interface, and are encouraged to rate their satisfaction within the various stage.
25
-
26
- Running an experiment could be to change some aspect of the process for only some travelers, let's say by routing them down a different hallway. At various stages of both hallways we still have the emoji interfaces that can be happily tapped or angrily jabbed in their passing.
27
-
28
- After a while we can compare the results and can evaluate which hallway had the better overall experience, based on the ratings provided at the various stages.
29
-
30
- This library is about keeping track of which passports we send down which hallway, so we can consistently route them down the same hallway, and to know which hallway they're rating when they do.
31
-
32
- In this model we don’t need to know anything about the passport holder unless we decide to "ask", as we determine which hallway to send them down initially.
33
-
34
- This library doesn't provide a system of linking passports back to their passport holders, but it doesn't explicitly make doing so impossible. Doing so is often not a relevant detail on well defined and well executed experiments.
17
+ Candidate and variant are the same concept, but simplify how we speak about experimental paths.<br clear="all">
35
18
 
36
19
  ## Installation
37
20
 
@@ -51,11 +34,11 @@ $ rails generate gitlab-experiment:install
51
34
 
52
35
  For the sake of an example let's make one up. Let's run an experiment on what we render for disabling desktop notifications.
53
36
 
54
- In our control (current world) we show a simple toggle interface that reads "Notifications". In our experiment we want a "Turn on/off desktop notifications" button with a confirmation.
37
+ 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.
55
38
 
56
- The behavior will be the same, but the interface will be different and may involve more or less steps.
57
-
58
- This makes the action more clear and will help the user in making a choice about if that's what they want to do. Or that's what we're going to try to find out.
39
+ The behavior will be the same, but the interface will be different and may involve more or fewer steps.
40
+
41
+ Our hypothesis is that this will make the action more clear and will help the user in making a choice about if that's what they really want to do.
59
42
 
60
43
  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.
61
44
 
@@ -63,8 +46,8 @@ When you implement an experiment you'll need to provide a name, and a context. T
63
46
 
64
47
  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.
65
48
 
66
- Now in our experiment we're going to render one of two views. Control will be our current view, and candidate will be the new toggle button with a confirmation flow.
67
-
49
+ 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.
50
+
68
51
  ```ruby
69
52
  class SubscriptionsController < ApplicationController
70
53
  def show
@@ -120,15 +103,15 @@ exp.track(:clicked_button)
120
103
 
121
104
  ```ruby
122
105
  exp = Gitlab::Experiment.new(:notification_toggle, user_id: user.id)
123
- # Context may be passed in the block, but must be finalized before calling
124
- # run or track.
106
+ # Additional context may be provided to the instance (exp) but must be
107
+ # finalized before calling run or track.
125
108
  exp.context(project_id: project.id) # add the project id to the context
126
109
 
127
110
  # Define the control and candidate variant.
128
111
  exp.use { render_toggle } # control
129
112
  exp.try { render_button } # candidate
130
113
 
131
- # Run the experiment -- returning the result.
114
+ # Run the experiment, returning the result.
132
115
  exp.run
133
116
 
134
117
  # Track an event on the experiment we've defined.
@@ -154,8 +137,8 @@ class NotificationExperiment < Gitlab::Experiment
154
137
  end
155
138
 
156
139
  exp = NotificationExperiment.new(user_id: user.id) do |e|
157
- # Context may be passed in the block, but must be finalized before calling
158
- # run or track.
140
+ # Context may be provided within the block or to the instance (exp) but must
141
+ # be finalized before calling run or track.
159
142
  e.context(project_id: project.id) # add the project id to the context
160
143
  end
161
144
 
@@ -187,11 +170,22 @@ Or you can set the variant within the block. This allows using unique segmentati
187
170
 
188
171
  ```ruby
189
172
  experiment(:notification_toggle, user_id: user.id) do |e|
173
+ # Variant selection must be done before calling run or track.
190
174
  e.variant(:no_interface) # set the variant
191
175
  # ...
192
176
  end
193
177
  ```
194
178
 
179
+ Or it can be specified in the call to run if you call it from within the block.
180
+
181
+ ```ruby
182
+ experiment(:notification_toggle, user_id: user.id) do |e|
183
+ # ...
184
+ # Variant selection can be specified when calling run.
185
+ e.run(:no_interface)
186
+ end
187
+ ```
188
+
195
189
  </details>
196
190
 
197
191
  ### Return value
@@ -235,25 +229,27 @@ end
235
229
 
236
230
  There are times when we need to change context while an experiment is running. We make this possible by passing the migration data to the experiment.
237
231
 
238
- Take for instance, that you might be using `version: 1` in your context currently. To migrate this `version: 2`, provide the context to change using a `migrated_with` option.
232
+ Take for instance, that you might be using `version: 1` in your context currently. To migrate this to `version: 2`, provide the portion of the context you wish to change using a `migrated_with` option.
239
233
 
240
234
  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.
241
235
 
242
236
  ```ruby
237
+ # Migrate just the `:version` portion of the previous context, `{ user_id: 42, version: 1 }`:
243
238
  experiment(:my_experiment, user_id: 42, version: 2, migrated_with: { version: 1 })
244
239
  ```
245
240
 
246
- You can add or remove context by providing a `migrated_from` option. This approach expects a full context replacement -- e.g. what it was before you added or removed the new context key.
241
+ 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.
247
242
 
248
243
  If you wanted to introduce a `version` to your context, provide the full previous context.
249
-
244
+
250
245
  ```ruby
246
+ # Migrate the full context from `{ user_id: 42 }` to `{ user_id: 42, version: 1 }`:
251
247
  experiment(:my_experiment, user_id: 42, version: 1, migrated_from: { user_id: 42 })
252
248
  ```
253
249
 
254
- This can impact an experience if you haven't:
250
+ This can impact an experience if you:
255
251
 
256
- 1. implemented the concept of migrations in your variant resolver
252
+ 1. haven't implemented the concept of migrations in your variant resolver
257
253
  1. haven't enabled a reasonable caching mechanism
258
254
 
259
255
  ### When there isn't a user (cookies)
@@ -272,20 +268,33 @@ You'll need to provide the `request` as an option to the experiment if it's outs
272
268
  experiment(:my_experiment, user_id: user&.id, request: request)
273
269
  ```
274
270
 
275
- The cookie isn't set if the identifying key isn't present in the context. In this case, if there was no `user_id` key provided, the cookie wouldn't be set.
271
+ The cookie isn't set if the identifying key isn't present at all in the context. Using the default identifying key, when there is no `user_id` key provided, the cookie will not be set.
272
+
273
+ ```ruby
274
+ # no user_id context key is present, so no cookie is set
275
+ experiment(:my_experiment, project_id: @project.id)
276
276
 
277
- For edge cases, you can pass the cookie through by assigning it yourself -- e.g. `user_id: request.cookie_jar.signed['my_experiment_id']`. The cookie name is the experiment name (prefixed if configured) with `_id` appended.
277
+ # user_id context key is present, but is set to nil, so the cookie is set & used instead
278
+ experiment(:my_experiment, user_id: nil, project_id: @project.id)
279
+
280
+ # user_id context key is present and set to a value, so no cookie is set
281
+ experiment(:my_experiment, user_id: @user.id, project_id: @project.id)
282
+ ```
283
+
284
+ For edge cases, you can pass the cookie through by assigning it yourself -- e.g. `user_id: request.cookie_jar.signed['my_experiment_id']`. The cookie name is the full experiment name (including any configured prefix) with `_id` appended -- e.g. `gitlab_notification_toggle_id` for the `:notification_toggle` experiment key with a configured prefix of `gitlab`.
278
285
 
279
286
  ## Configuration
280
287
 
281
288
  This gem needs to be configured before being used in a meaningful way.
282
289
 
283
- The default configuration will always render the control. So it's important to configure your own logic for resolving variants.
290
+ The default configuration will always render the control, so it's important to configure your own logic for resolving variants.
284
291
 
285
- Yes, the most important aspect of the gem, 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.
292
+ 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.
286
293
 
287
294
  ```ruby
288
295
  Gitlab::Experiment.configure do |config|
296
+ # The block here is evaluated within the scope of the experiment instance,
297
+ # which is why we are able to access things like name and context.
289
298
  config.variant_resolver = lambda do |requested_variant|
290
299
  # Return the requested variant if a specific one has been provided in code.
291
300
  return requested_variant unless requested_variant.nil?
@@ -304,14 +313,14 @@ More examples for configuration are available in the provided [rails initializer
304
313
 
305
314
  This library doesn't attempt to provide any logic for the client layer.
306
315
 
307
- Instead it allows you to do this yourself in configuration. Using Gon to publish your experiment information to the client layer is pretty simple.
316
+ 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.
308
317
 
309
318
  ```ruby
310
319
  Gitlab::Experiment.configure do |config|
311
320
  config.publishing_behavior = lambda do |_result|
312
321
  # Push the experiment knowledge into the front end. The signature contains
313
322
  # the context key, and the variant that has been determined.
314
- Gon.push(experiment: { name => signature })
323
+ Gon.push({ experiment: { name => signature } }, true)
315
324
  end
316
325
  end
317
326
  ```
@@ -322,6 +331,8 @@ In the client you can now access `window.gon.experiment.notificationToggle`.
322
331
 
323
332
  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.
324
333
 
334
+ It's important to understand that using caching can drastically change or override your rollout strategy logic.
335
+
325
336
  ```ruby
326
337
  Gitlab::Experiment.configure do |config|
327
338
  config.cache = Rails.cache
@@ -330,11 +341,11 @@ end
330
341
 
331
342
  ## Tracking, anonymity and GDPR
332
343
 
333
- We generally try not to track things like user identifying values in our experimentation. What we can and do track is the "experiment experience" (a.k.a the context key).
344
+ We generally try not to track things like user identifying values in our experimentation. What we can and do track is the "experiment experience" (a.k.a. the context key).
334
345
 
335
346
  We generate this key from the context passed to the experiment. This allows creating funnels without exposing any user information.
336
347
 
337
- This library attempts to be non user centric, in that a context can contain things like a user, or a project.
348
+ This library attempts to be non-user-centric, in that a context can contain things like a user or a project.
338
349
 
339
350
  If you only include a user, that user would get the same experience across every project they view. If you only include the project, every user who views that project would get the same experience.
340
351
 
@@ -26,7 +26,7 @@ Gitlab::Experiment.configure do |config|
26
26
  #
27
27
  # requested_variant || variant_names.first || 'control'
28
28
 
29
- # Using unleash to determine the variant:
29
+ # Using Unleash to determine the variant:
30
30
  #
31
31
  # fallback = Unleash::Variant.new(name: requested_variant || 'control', enabled: true)
32
32
  # Unleash.get_variant(name, context.value, fallback)
@@ -40,19 +40,17 @@ Gitlab::Experiment.configure do |config|
40
40
  # Tracking behavior can be implemented to link an event to an experiment.
41
41
  #
42
42
  # Similar to the variant_resolver, this is called within the scope of the
43
- # experiment instance and so can access any methods on the experiment.
44
- #
45
- #
43
+ # experiment instance and so can access any methods on the experiment,
44
+ # such as name and signature.
46
45
  config.tracking_behavior = lambda do |event, args|
47
46
  # An example of using a generic logger to track events:
48
47
  config.logger.info "Gitlab::Experiment[#{name}] #{event}: #{args.merge(signature: signature)}"
49
48
 
50
- # Using something like snowplow to track events:
49
+ # Using something like snowplow to track events (in gitlab):
51
50
  #
52
51
  # Gitlab::Tracking.event(name, event, **args.merge(
53
52
  # context: (args[:context] || []) << SnowplowTracker::SelfDescribingJson.new(
54
- # 'iglu:com.gitlab/gitlab_experiment/jsonschema/0-2-0',
55
- # signature: signature
53
+ # 'iglu:com.gitlab/gitlab_experiment/jsonschema/0-2-0', signature
56
54
  # )
57
55
  # ))
58
56
  end
@@ -69,7 +67,7 @@ Gitlab::Experiment.configure do |config|
69
67
  # Push the experiment knowledge into the front end. The signature contains
70
68
  # the context key, and the variant that has been determined.
71
69
  #
72
- # Gon.push(experiment: { name => signature })
70
+ # Gon.push({ experiment: { name => signature } }, true)
73
71
 
74
72
  # Log using our logging system, so the result (which can be large) can be
75
73
  # reviewed later if we want to.
@@ -57,7 +57,9 @@ module Gitlab
57
57
  result.respond_to?(:name) ? result : Variant.new(name: result.to_s)
58
58
  end
59
59
 
60
- def run
60
+ def run(variant_name = nil)
61
+ @variant_name = variant_name unless variant_name.nil?
62
+
61
63
  @result ||= super(cache { variant.name })
62
64
  end
63
65
 
@@ -65,6 +65,7 @@ module Gitlab
65
65
 
66
66
  def resolve_cookie(jar, hash, key, cookie = nil)
67
67
  return if cookie.blank? && hash[key].blank?
68
+ return hash if cookie.blank?
68
69
  return hash.merge(key => cookie) if hash[key].blank?
69
70
 
70
71
  @migrations_with << { key => cookie }
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Gitlab
4
4
  class Experiment
5
- VERSION = '0.2.3'
5
+ VERSION = '0.2.4'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,35 +1,35 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gitlab-experiment
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitLab
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-09-27 00:00:00.000000000 Z
11
+ date: 2020-10-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: scientist
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: 1.5.0
20
17
  - - "~>"
21
18
  - !ruby/object:Gem::Version
22
19
  version: '1.5'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.5.0
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
26
26
  requirements:
27
- - - ">="
28
- - !ruby/object:Gem::Version
29
- version: 1.5.0
30
27
  - - "~>"
31
28
  - !ruby/object:Gem::Version
32
29
  version: '1.5'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 1.5.0
33
33
  description:
34
34
  email:
35
35
  - gitlab_rubygems@gitlab.com
@@ -69,7 +69,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
69
69
  - !ruby/object:Gem::Version
70
70
  version: '0'
71
71
  requirements: []
72
- rubygems_version: 3.0.3
72
+ rubygems_version: 3.1.4
73
73
  signing_key:
74
74
  specification_version: 4
75
75
  summary: GitLab experiment library built on top of scientist.