trailguide 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +191 -293
  3. data/app/views/trail_guide/admin/experiments/_btn_join.html.erb +1 -1
  4. data/app/views/trail_guide/admin/experiments/_header.html.erb +3 -3
  5. data/config/initializers/admin.rb +19 -0
  6. data/config/initializers/experiment.rb +261 -0
  7. data/config/initializers/trailguide.rb +6 -279
  8. data/lib/trail_guide/adapters.rb +2 -0
  9. data/lib/trail_guide/adapters/experiments.rb +8 -0
  10. data/lib/trail_guide/adapters/experiments/redis.rb +48 -0
  11. data/lib/trail_guide/adapters/participants/cookie.rb +1 -0
  12. data/lib/trail_guide/adapters/participants/unity.rb +9 -1
  13. data/lib/trail_guide/adapters/variants.rb +8 -0
  14. data/lib/trail_guide/adapters/variants/redis.rb +52 -0
  15. data/lib/trail_guide/admin/engine.rb +1 -0
  16. data/lib/trail_guide/algorithms.rb +4 -0
  17. data/lib/trail_guide/algorithms/algorithm.rb +29 -0
  18. data/lib/trail_guide/algorithms/bandit.rb +9 -18
  19. data/lib/trail_guide/algorithms/distributed.rb +8 -15
  20. data/lib/trail_guide/algorithms/random.rb +2 -12
  21. data/lib/trail_guide/algorithms/static.rb +34 -0
  22. data/lib/trail_guide/algorithms/weighted.rb +5 -17
  23. data/lib/trail_guide/catalog.rb +79 -35
  24. data/lib/trail_guide/config.rb +2 -4
  25. data/lib/trail_guide/engine.rb +2 -1
  26. data/lib/trail_guide/experiments/base.rb +41 -24
  27. data/lib/trail_guide/experiments/combined_config.rb +4 -0
  28. data/lib/trail_guide/experiments/config.rb +59 -30
  29. data/lib/trail_guide/experiments/participant.rb +4 -2
  30. data/lib/trail_guide/helper.rb +4 -216
  31. data/lib/trail_guide/helper/experiment_proxy.rb +160 -0
  32. data/lib/trail_guide/helper/helper_proxy.rb +62 -0
  33. data/lib/trail_guide/metrics/config.rb +2 -0
  34. data/lib/trail_guide/metrics/goal.rb +17 -15
  35. data/lib/trail_guide/participant.rb +10 -2
  36. data/lib/trail_guide/unity.rb +17 -8
  37. data/lib/trail_guide/variant.rb +15 -11
  38. data/lib/trail_guide/version.rb +2 -2
  39. metadata +13 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ac4411fe00b16770b749c3ec2005e0d10f37cc05c2549d6fb8ff9613d31c7ba9
4
- data.tar.gz: 29af693f9ddb1855b60c627947067994834d05661580d793994637c9f92fb3af
3
+ metadata.gz: a1f31e734d2b0165ffe08887a4e7a21cceebe8c6aba8c4ff72d9393f1fd6095a
4
+ data.tar.gz: ea8cd225a315add5f4512f8bc38bd28bc372741c45185eb4c987069165c3ad4b
5
5
  SHA512:
6
- metadata.gz: 1f2418947bc8283761b0283fd485182abc8c1b6df44d9de7ae65e227286352065788c85262f31ab0e70cc70517310d278b858bf8ddea02d9af3ce12a02e87950
7
- data.tar.gz: 76d8edbad6605be76f323ec104fc80a180f72a86e4d86e3e0703bc8734145c8140bad2a6345667175c9fbb8e70a5b0bc5ad35713361761ba3dc434818743403d
6
+ metadata.gz: ccb5f9cd9e367481380b8afbc6a763149967bd59f0bbe6d7f0c0693d90ccae7d0b6a16bf9e2544ef4362504cb417cb327f44001a044d8bbc4e318a1377b6ac45
7
+ data.tar.gz: 19b6761660d76db06b36114496fd9f835d08fdf70e99a7e117369d800175a79f18e21c6436f7f37a84529e3370bdae2f8285a49e25ae00182cc05f801c8daea9
data/README.md CHANGED
@@ -1,16 +1,32 @@
1
1
  # TrailGuide
2
2
 
3
- TrailGuide is a rails engine providing a framework for running user experiments and A/B tests in rails apps.
3
+ [![Build Status](https://travis-ci.org/markrebec/trailguide.svg?branch=master)](https://travis-ci.org/markrebec/trailguide)
4
+ [![Coverage Status](https://coveralls.io/repos/github/markrebec/trailguide/badge.svg?branch=master)](https://coveralls.io/github/markrebec/trailguide?branch=master)
5
+
6
+ TrailGuide is a framework to enable running A/B tests, user experiments and content experiments in rails applications. It is backed by redis making it extremely fast, and provides configuration options allowing for flexible, robust experiments and behavior.
7
+
8
+ **NOTE**: I'm currently working towards an official release, refactoring a few things and filling in additional spec coverage and documentation. The current release is stable, however there may be slight changes to the API and usage between now and 1.0.0. In the meantime any feedback, issues or pull requests are welcome!
9
+
10
+ ## Features
11
+
12
+ * **Fast** - TrailGuide makes efficient use of redis, storing a few simple metadata key/value pairs for experiments. Combined with ruby class-based experiments, efficient built-in algorithms, and participant adapters, enrolling in an experiment takes only a few milliseconds.
13
+ * **Flexible** - Core behavior, participant assignment and individual experiments are highly configurable and can be used in almost any context, with options to control everything from variant options and metrics, to enrollment, conversion behavior, request filtering and more.
14
+ * **Hooks and Callbacks** - There are a number of hooks available to handle things like failover cases or filtering participation/conversion, as well as callbacks to enable logging/tracking/whatever when lifecycle or enrollment events occur. (TODO wiki link to callbacks page)
15
+ * **Algorithms** - TrailGuide comes with a few built-in algorithms for common use cases, or you can create your own algorithm class by following a simple interface. (TODO wiki link to algorithm page)
16
+ * **Conversion Goals** - Define simple conversion goals or more complex funnels, and track how your variants are performing against those goals.
17
+ * **Experiment Groups** - If you're running a large number of experiments, it can be helpful to organize them into logical groups, which can also be referenced when converting multiple experiments against shared conversion goals.
18
+ * **Combined Experiments** - Combined experiments are a way to share configuration (variants, goals, groups, flags, etc.), lifecycle (running, paused, etc.) and participation between two or more experiments, while tracking conversion and managing winning variants individually.
19
+ * **Analysis** - Once your experiment is complete TrailGuide can provide you with results using the built-in analyzers, either z-score (default) or bayesian. (TODO wiki link to analysis)
4
20
 
5
21
  ## Getting Started
6
22
 
7
23
  ### Requirements
8
24
 
9
- Currently only rails 5.x is officially tested/supported, and trailguide requires redis to store experiment metadata and (optionally) participants.
25
+ Currently only rails 5.x is officially tested/supported, and TrailGuide requires redis to store experiment metadata and (optionally) participants' assignment.
10
26
 
11
- `docker-compose` is a great way to run redis in development. Take a look at the `docker-compose.yml` in the root of this repo for an example.
27
+ `docker-compose` is a great way to run redis in development. Take a look at the [`docker-compose.yml` in the root of this repo](https://github.com/markrebec/trailguide/blob/master/docker-compose.yml) for an example.
12
28
 
13
- In production I recommend configuring redis as a persistent datastore (rather than a cache), in order to avoid evicting experiment or participant keys unexpectedly.
29
+ In production I recommend configuring redis as a persistent datastore (rather than a cache), in order to avoid evicting experiment or participant keys unexpectedly. You can [read more about key eviction and policies here](https://redis.io/topics/lru-cache).
14
30
 
15
31
  ### Installation
16
32
 
@@ -22,38 +38,25 @@ gem 'trailguide'
22
38
 
23
39
  Then run `bundle install`.
24
40
 
25
- ### Engine & Admin Routes
41
+ ### Configuration
26
42
 
27
- If you plan on using the included javascript client, or if you just want an API to interact with experiments in other ways, you can mount the engine in your route config:
43
+ By default the redis client will attempt to connect to redis on `localhost:6379`, which is usually fine for development/testing but won't work in other environments.
28
44
 
29
- ```ruby
30
- # /config/routes.rb
45
+ Configure redis by either setting a `REDIS_URL` environment variable:
31
46
 
32
- Rails.application.routes.draw do
33
- # ...
34
-
35
- mount TrailGuide::Engine => 'api/experiments'
36
-
37
- # ...
38
- end
47
+ ```
48
+ REDIS_URL=redis://127.0.0.1:6379
39
49
  ```
40
50
 
41
- You can also mount the admin engine to manage and analyze your experiments via the built-in admin UI. You'll probably want to wrap this in some sort of authentication, though the details will vary between applications. If you're already mounting other admin engines (i.e. something like `sidekiq` or `flipper`), you should be able to apply the same technique to trailguide.
51
+ Or you can create a config initializer, which is useful if you plan on configuring TrailGuide further:
42
52
 
43
53
  ```ruby
44
- # /config/routes.rb
45
-
46
- Rails.application.routes.draw do
47
- # ...
48
-
49
- mount TrailGuide::Engine => 'api/experiments'
50
-
51
- # example auth route helper
52
- authenticate :user, lambda { |u| u.admin? } do
53
- mount TrailGuide::Admin::Engine => 'admin/trailguide'
54
- end
54
+ # config/initializers/trailguide.rb
55
55
 
56
- # ...
56
+ TrailGuide.configure do |config|
57
+ config.redis = 'redis://127.0.0.1:6379'
58
+ # or you can also use your own client
59
+ # config.redis = Redis.new(url: 'redis://127.0.0.1:6379')
57
60
  end
58
61
  ```
59
62
 
@@ -67,23 +70,23 @@ Create and configure an experiment:
67
70
  experiment :simple_ab do |config|
68
71
  config.summary = "This is a simple A/B test" # optional
69
72
 
70
- variant :a
71
- variant :b
73
+ variant :alpha # the first variant is always the "control" group unless declared otherwise
74
+ variant :bravo
72
75
  end
73
76
  ```
74
77
 
75
- Start your experiment either via the admin UI or from a rails console with `TrailGuide.catalog.find(:simple_ab).start!` to enable enrollment.
76
-
77
- Then use it (in controller actions for this example):
78
+ Then use it in controller:
78
79
 
79
80
  ```ruby
81
+ # app/controllers/things_controller.rb
82
+
80
83
  def show
81
84
  # enroll in the experiment and do something based on the assigned variant group
82
- case trailguide.choose(:simple_ab)
83
- when :a
84
- # perform logic for group "a"
85
- when :b
86
- # perform logic for group "b"
85
+ case trailguide(:simple_ab)
86
+ when :alpha
87
+ # perform your logic for group "alpha"
88
+ when :bravo
89
+ # perform your logic for group "bravo"
87
90
  end
88
91
 
89
92
  # ...
@@ -97,11 +100,60 @@ def update
97
100
  end
98
101
  ```
99
102
 
100
- If you've mounted the admin engine, you can view your experiment's participants and conversions there.
103
+ Or a view:
104
+
105
+ ```erb
106
+ # app/views/things/show.html.erb
107
+
108
+ <% if trailguide(:simple_ab) == :alpha %>
109
+ <div>...render alpha...</div>
110
+ <% else %>
111
+ <div>...render bravo...</div>
112
+ <% end %>
113
+ ```
114
+
115
+ TODO link to examples of `run`, `render`, etc.
116
+
117
+ Until your experiment is started, only the "control" group (in this case `:alpha`) will be served to visitors. Start your experiment either via the admin UI or from a rails console with `TrailGuide.catalog.find(:simple_ab).start!` to begin enrolling participants and serving variants.
118
+
119
+ ### API / JavaScript Client
120
+
121
+ If you plan on using the included javascript client, or if you just want an API to interact with experiments in other ways, you can mount the engine in your route config:
122
+
123
+ ```ruby
124
+ # config/routes.rb
125
+
126
+ Rails.application.routes.draw do
127
+
128
+ mount TrailGuide::Engine => 'api/experiments'
129
+
130
+ # ...
131
+ end
132
+ ```
133
+
134
+ ### Admin UI
135
+
136
+ You can also mount the admin engine to manage and analyze your experiments via the built-in admin UI. You'll probably want to wrap this in some sort of authentication, although the details will vary between applications. If you're already mounting other admin engines (i.e. something like `sidekiq` or `flipper`), you should be able to apply the same technique to trailguide.
137
+
138
+ ```ruby
139
+ # config/routes.rb
140
+
141
+ Rails.application.routes.draw do
142
+
143
+ mount TrailGuide::Engine => 'api/experiments'
144
+
145
+ # example auth route helper
146
+ authenticate :user, lambda { |u| u.admin? } do
147
+ mount TrailGuide::Admin::Engine => 'admin/trailguide'
148
+ end
149
+
150
+ # ...
151
+ end
152
+ ```
101
153
 
102
154
  ## Configuration
103
155
 
104
- The core engine and base experiment class have a number of configuration options available to customize behavior and hook into various pieces of functionality. The best way to configure trailguide is via a config initializer, and this gem configures it's own defaults the same way.
156
+ The core engine and base experiment class have a number of configuration options available to customize behavior and hook into various pieces of functionality with callbacks. The best way to configure trailguide is via a config initializer, and this gem configures it's own defaults the same way.
105
157
 
106
158
  ```ruby
107
159
  # config/initializers/trailguide.rb
@@ -112,20 +164,43 @@ TrailGuide.configure do |config|
112
164
  end
113
165
 
114
166
  TrailGuide::Experiment.configure do |config|
167
+ # all experiments inherit from the top-level experiment class,
168
+ # which allows you to provide global experiment configuration
169
+ # here, and override custom behavior on a per-experiment basis
115
170
  config.algorithm = :weighted
116
171
  # ...
117
172
  end
118
173
  ```
119
174
 
120
- Take a look at [`config/initializers/trailguide.rb`](https://github.com/markrebec/trailguide/blob/master/config/initializers/trailguide.rb) in this repo for a full list of defaults and examples of the available configuration options.
175
+ Take a look at [`the config initializers in this repo`](https://github.com/markrebec/trailguide/blob/master/config/initializers) for a full list of defaults and examples of the available configuration options.
121
176
 
122
177
  ### Configuring Experiments
123
178
 
124
179
  Before you can start running experiments in your app you'll need to define and configure them. There are a few options for defining experiments - YAML files, a ruby DSL, or custom classes - and they all inherit the base `TrailGuide::Experiment.configuration` for defaults, which can be overridden per-experiment.
125
180
 
181
+ #### Experiments Paths
182
+
183
+ By default, TrailGuide will look for experiment configs in `config/experiments.*` and `config/experiments/**/*`, and will load custom experiment classes from `app/experiments/**/*`. You can override this behavior with the `TrailGuide.configuration.paths` object in your config initializer.
184
+
185
+ ```ruby
186
+ # config/initializers/trailguide.rb
187
+
188
+ TrailGuide.configure do |config|
189
+ # you can append a single file or a glob pattern onto the experiment config loadpaths
190
+ config.paths.configs << 'foo/bar/experiments/**/*'
191
+
192
+ # or you can explicitly override the values with your own
193
+ config.paths.configs = ['foo/bar/baz.rb', 'other/path/**/*']
194
+
195
+ # you can also append or override the path(s) from which custom experiment classes are loaded
196
+ config.paths.classes = ['lib/experiments/**/*']
197
+ end
198
+
199
+ ```
200
+
126
201
  #### YAML
127
202
 
128
- YAML files are an easy way to configure simple experiments. They can be put in `config/experiments.yml` or `config/experiments/**/*.yml`:
203
+ YAML files are an easy way to configure simple experiments. They will be loaded based on your path configurations above, and by default can be put in `config/experiments.yml` or `config/experiments/**/*.yml`:
129
204
 
130
205
  ```yaml
131
206
  # config/experiments.yml
@@ -150,7 +225,7 @@ search_widget:
150
225
 
151
226
  #### Ruby DSL
152
227
 
153
- The ruby DSL provides a more dynamic and flexible way to configure your experiments, and allows you to define custom behavior via callbacks and options. You can put these experiments in `config/experiments.rb` or `config/experiments/**/*.rb`:
228
+ The ruby DSL provides a more dynamic and robust way to configure your experiments, allowing you to define custom behavior via callbacks and other options. Like YAML experiments, they're loaded based on your path configurations, and by default You can put these experiments in `config/experiments.rb` or `config/experiments/**/*.rb`:
154
229
 
155
230
  ```ruby
156
231
  # config/experiments.rb
@@ -180,9 +255,9 @@ end
180
255
 
181
256
  #### Custom Classes
182
257
 
183
- You can also take it a step further and define your own custom experiment classes, inheriting from `TrailGuide::Experiment`. This allows you to add or override all sorts of additional behavior on top of all the standard configuration provided by the DSL. In fact, the YAML and ruby DSL configs both use this to parse experiments into anonmymous classes extending `TrailGuide::Experiment`.
258
+ You can also take it a step further and define your own custom experiment classes, inheriting from `TrailGuide::Experiment`. This allows you to add or override all sorts of additional behavior on top of all the standard configuration provided by the DSL. In fact, the YAML and ruby DSL configs both use this to parse experiments into anonmymous classes inheriting from `TrailGuide::Experiment`.
184
259
 
185
- You can put these classes anywhere rails will autoload them (or require them yourself), but I recommend `app/experiments/**/*.rb`:
260
+ You can put these classes anywhere rails will autoload them (i.e. `app/whatever`), or you can put them somewhere like `lib/experiments` and require them yourself, but TrailGuide will also attempt to load them for you based on your path configurations, which by default looks in `app/experiments/**/*.rb`:
186
261
 
187
262
  ```ruby
188
263
  # app/experiments/my_complex_experiment.rb
@@ -251,218 +326,11 @@ You can even use these in your DSL-defined experiments by specifying a `class:`
251
326
  # config/experiments.rb
252
327
 
253
328
  experiment :my_inheriting_experiment, class: ApplicationExperiment do |config|
254
- # ...
255
- end
256
- ```
257
-
258
- ### Participant Adapters
259
-
260
- While all experiment configuration, metadata and metrics are stored in redis, there are various adapters available for participants to control where individual assignments are stored for each user. These adapters are configurable and extensible, so you can customize them or even create your own by following a simple interface.
261
-
262
- The following participant adapters are included with trailguide:
263
-
264
- * `:cookie` (default) - stores participant assignments in a cookie in their browser
265
- * `:session` - stores participant assignments in a hash in their rails session
266
- * `:redis` - stores participant assignments in redis, under a configurable key identifier (usually `current_user.id` or a cookie storing some sort of tracking/visitor ID for logged out users)
267
- * `:anonymous` - temporary storage, in a local hash, that only exists for as long as you have a handle on the participant object (usually a last resort fallback)
268
- * `:multi` - attempts to use the "best" available adapter based on the current context
269
- * `:unity` - uses `TrailGuide::Unity` to attempt to unify visitor/user sessions based on your configuration
270
-
271
- #### Cookie
272
-
273
- This is the default adapter, which stores participation details in a cookie in the user's browser. If you want to configure the cookie name, path or expiration, you can do so directly in your initializer:
274
-
275
- ```ruby
276
- TrailGuide.configure do |config|
277
- # config.adapter = :cookie
278
- config.adapter = TrailGuide::Adapters::Participants::Cookie.configure do |config|
279
- config.cookie = :trailguide
280
- config.path = '/'
281
- config.expiration = 1.year.to_i
282
- end
283
- end
284
- ```
285
-
286
- #### Session
287
-
288
- The session adapter will store participation in a hash under a configurable key within the user's rails session.
289
-
290
- ```ruby
291
- TrailGuide.configure do |config|
292
- # use the symbol shortcut for defaults
293
- config.adapter = :session
294
-
295
- # or configure it
296
- config.adapter = TrailGuide::Adapters::Participants::Session.configure do |config|
297
- config.key = :trailguide
298
- end
299
- end
300
- ```
329
+ # you can configure this experiment just like any other DSL-based experiment,
330
+ # the only difference is that the resulting anonymous class will inherit from
331
+ # ApplicationExperiment rather than directly from TrailGuide::Experiment
301
332
 
302
- #### Redis
303
-
304
- The redis adapter stores participation details in a configurable redis key, which makes it great for ensuring consistency across visits and even devices. While the cookie and session adapters are restricted to a single browser or even a single browsing session, the redis adapter is more persistent and controllable, with the tradeoff being that you'll need to be able to identify your users in some way (i.e. `current_user.id`).
305
-
306
- ```ruby
307
- TrailGuide.configure do |config|
308
- # use the symbol shortcut for defaults
309
- config.adapter = :redis
310
-
311
- # or configure it
312
- config.adapter = TrailGuide::Adapters::Participants::Redis.configure do |config|
313
- config.namespace = :participants
314
- config.expiration = nil
315
- config.lookup = -> (context) { # context is wherever you're invoking trailguide, usually a controller or view
316
- context.try(:trailguide_user).try(:id) ||
317
- context.try(:current_user).try(:id)
318
- }
319
- end
320
- end
321
- ```
322
-
323
- #### Anonymous
324
-
325
- The anonymous adapter is a simple, ephemeral ruby hash that only exists for as long as you have a reference to that local participant object. It's generally only used as a last resort when there's no way to identify a participant who is enrolling in an experiment, because there's no way to get a new reference later on (for example to track conversion).
326
-
327
- #### Multi
328
-
329
- The multi adapter will attempt to use the "best" available adapter, depending on the context from which trailguide is being invoked (controller, view, background job, etc.). It comes with a default configuration that prefers to use redis if a `trailguide_user` or `current_user` is available, otherwise tries to use cookies if possible, then session if possible, falling back to anonymous as a last resort.
330
-
331
- You can use the multi adapter to wrap any adapter selection logic you like, the only requirement is that you return one of the other adapters:
332
-
333
- ```ruby
334
- TrailGuide.configure do |config|
335
- # use the symbol shortcut for defaults
336
- config.adapter = :multi
337
-
338
- # or configure it
339
- config.adapter = TrailGuide::Adapters::Participants::Multi.configure do |config|
340
- # should be a proc that returns another adapter to be used
341
- config.adapter = -> (context) do
342
- if (context.respond_to?(:trailguide_user, true) && context.send(:trailguide_user).present?) ||
343
- (context.respond_to?(:current_user, true) && context.send(:current_user).present?)
344
- TrailGuide::Adapters::Participants::Redis
345
- elsif context.respond_to?(:cookies, true)
346
- TrailGuide::Adapters::Participants::Cookie
347
- elsif context.respond_to?(:session, true)
348
- TrailGuide::Adapters::Participants::Session
349
- else
350
- TrailGuide::Adapters::Participants::Anonymous
351
- end
352
- end
353
- end
354
- end
355
- ```
356
-
357
- #### Unity
358
-
359
- The unity adapter is a wrapper around `TrailGuide::Unity`, which attempts to unify user/visitor sessions, then selects and configures the appropriate adapter. It looks for an available, configurable "user ID" (`current_user`) and "visitor ID" (from a cookie) and configures the redis adapter appropriately. If there is no identifying information available it falls back to the anonymous adapter.
360
-
361
- You can configure the visitor cookie and the user id attribute, as well as the adapters to be used in each case:
362
-
363
- ```ruby
364
- TrailGuide.configure do |config|
365
- config.adapter = TrailGuide::Adapters::Participants::Unity.configure do |config|
366
- # setup the visitor ID cookie and user ID attribute
367
- config.visitor_cookie = :visitor_id # uses a cookie called visitor_id, must be set and managed by you separately
368
- config.user_id_key = :uuid # uses current_user.uuid, defaults to current_user.id
369
-
370
- # uses redis adapter for identified users
371
- config.user_adapter = TrailGuide::Adapters::Participants::Redis.configure do |config|
372
- config.namespace = 'unity:users'
373
- config.lookup = -> (user_id) { user_id }
374
- config.expiration = 1.year.seconds
375
- end
376
-
377
- # uses redis adapter for identified visitors
378
- config.visitor_adapter = TrailGuide::Adapters::Participants::Redis.configure do |config|
379
- config.namespace = 'unity:visitors'
380
- config.lookup = -> (visitor_id) { visitor_id }
381
- config.expiration = 1.year.seconds
382
- end
383
-
384
- # uses anonymous adapter for unidentified
385
- config.anonymous_adapter = TrailGuide::Adapters::Participants::Anonymous
386
- end
387
- end
388
- ```
389
-
390
- See the unity documentation for more info about unifying sessions.
391
-
392
- #### Custom Adapters
393
-
394
- **TODO** - In the meantime, checkout the cookie or session adapters for simple examples as a starting point.
395
-
396
- ```ruby
397
- TrailGuide.configure do |config|
398
- config.adapter = MyCustom::AdapterClass
399
- end
400
- ```
401
-
402
- ### Algorithms
403
-
404
- There are a few common assignment algorithms included in trailguide, and it's easy to define your own and configure your experiments to use them. Algorithms can either be configured globally for all experiments in your initializer, or overridden individually per-experiment.
405
-
406
- The following algorithms are available:
407
-
408
- * `:weighted` (default) - allows favoring variants by assigning them weights
409
- * `:distributed` - totally even distribution across variants
410
- * `:random` - truly random sampling of variants on assignment
411
- * `:bandit` - a "multi-armed bandit" approach to assignment
412
-
413
- #### Weighted
414
-
415
- This is the default algorithm, which allows weighted assignment to variants based on each variant's configuration. All things being equal (all variants having equal weights), it's essentially a random sampling that will provide mostly even distribution across a large enough sample size. The default weight for all variants is 1.
416
-
417
- ```ruby
418
- experiment :my_experiment do |config|
419
- config.algorithm = :weighted
420
-
421
- variant :a, weight: 2 # would be assigned roughly 40% of the time
422
- variant :b, weight: 2 # would be assigned roughly 40% of the time
423
- variant :c, weight: 1 # would be assigned roughly 20% of the time
424
- end
425
- ```
426
-
427
- Note that the weighted algorithm is the only one that takes variant weight into account, and the other algorithms will simply ignore it if it's defined.
428
-
429
- #### Distributed
430
-
431
- The distributed algorithm ensures completely even distribution across all variants by always selecting from the variant(s) with the lowest number of participants.
432
-
433
- ```ruby
434
- experiment :my_experiment do |config|
435
- config.algorithm = :distributed
436
- end
437
- ```
438
-
439
- #### Random
440
-
441
- The random algorithm provides totally random distribution by sampling from all variants on assignment.
442
-
443
- ```ruby
444
- experiment :my_experiment do |config|
445
- config.algorithm = :random
446
- end
447
- ```
448
-
449
- #### Multi-Armed Bandit
450
-
451
- The bandit algorithm in trailguide was heavily inspired by [the split gem](https://github.com/splitrb/split#algorithms), and will automatically weight variants based on their performance over time. You can [read more about this approach](http://stevehanov.ca/blog/index.php?id=132) if you're interested.
452
-
453
- ```ruby
454
- experiment :my_experiment do |config|
455
- config.algorithm = :bandit
456
- end
457
- ```
458
-
459
- #### Custom
460
-
461
- **TODO** - In the meantime, take a look at the included algorithms as a starting point. Essentially as long as you accept an experiment and return a variant, the rest is up to you.
462
-
463
- ```ruby
464
- experiment :my_experiment do |config|
465
- config.algorithm = MyCustom::AlgorithmClass
333
+ # ...
466
334
  end
467
335
  ```
468
336
 
@@ -470,16 +338,16 @@ end
470
338
 
471
339
  ### Helpers
472
340
 
473
- The `TrailGuide::Helper` module is available to be mixed into just about any context, and provides a helper proxy with an easy API to interact with trailguide. These helpers are mixed into controllers and views as helper methods by default. You can disable this behavior by setting the `config.include_helpers` option to `false` if you'd rather explicitly include it where you want to use it.
341
+ The `TrailGuide::Helper` module is available to be mixed into just about any context, and provides an easy way to interact with TrailGuide experiments. These helpers are mixed into controllers and views as helper methods by default, but you can disable this behavior by setting the `config.include_helpers` option to `false` if you'd rather explicitly include it where you want to use it.
474
342
 
475
- When mixed in, the `trailguide` method provides a reference to the helper proxy, which in turn provides a few methods to perform your experiments.
343
+ When mixed in, the `trailguide` method provides a reference to the helper proxy, which in turn provides a few methods to interact with your experiments.
476
344
 
477
345
  ```ruby
478
346
  # enroll in an experiment or reuse previous assignment
479
347
  trailguide.choose(:experiment_name)
480
348
  trailguide.choose!(:experiment_name)
481
349
 
482
- # choose, then automatically calls a method within the current context based on
350
+ # chooses, then automatically calls a method within the current context based on
483
351
  # the selected variant
484
352
  trailguide.run(:experiment_name)
485
353
  trailguide.run!(:experiment_name)
@@ -489,7 +357,7 @@ trailguide.run!(:experiment_name)
489
357
  trailguide.render(:experiment_name)
490
358
  trailguide.render!(:experiment_name)
491
359
 
492
- # tracks a conversion for the participant's currently assigned variant
360
+ # tracks a conversion only if participating, for the participant's currently assigned variant
493
361
  trailguide.convert(:experiment_name)
494
362
  trailguide.convert!(:experiment_name)
495
363
  ```
@@ -498,7 +366,7 @@ As a general rule of thumb, the bang (`!`) methods will loudly raise exceptions
498
366
 
499
367
  #### Enrollment
500
368
 
501
- The `choose` method will either enroll a participant into an experiment for the first time or return their previously assigned variant if they've already been enrolled. It can accept a block to execute and returns a `TrailGuide::Variant` object, but can be compared directly to strings or symbols.
369
+ The `choose` method will either enroll a participant into an experiment for the first time or return their previously assigned variant if they've already been enrolled. It can accept an optional block to execute, and returns a `TrailGuide::Variant` object, *which can be compared directly to strings or symbols* or access it's properties directly (i.e. `variant.metadata[:foo]`).
502
370
 
503
371
  ```ruby
504
372
  class MyController < ApplicationController
@@ -535,7 +403,7 @@ class MyController < ApplicationController
535
403
  end
536
404
  ```
537
405
 
538
- You can also call `trailguide.choose` from your view templates, though you probably want to keep any complex logic in your controllers (or maybe helpers). This would print out the variant name into an `h1`:
406
+ You can also call `trailguide.choose` from your view templates and partials, though you probably want to keep any complex logic in your controllers (or maybe helpers). This would print out the variant name into an `h1`:
539
407
 
540
408
  ```erb
541
409
  <% variant = trailguide.choose(:experiment_name) %>
@@ -544,7 +412,7 @@ You can also call `trailguide.choose` from your view templates, though you proba
544
412
 
545
413
  #### Running Methods
546
414
 
547
- If you prefer, you can encapsulate your logic into methods for each variant and ask trailguide to execute the appropriate one for you automatically.
415
+ If you prefer, you can encapsulate your logic into methods for each variant and ask trailguide to execute the appropriate one for you automatically within the given context.
548
416
 
549
417
  ```ruby
550
418
  class MyController < ApplicationController
@@ -565,25 +433,26 @@ class MyController < ApplicationController
565
433
  end
566
434
  ```
567
435
 
568
- By default the above will attempt to call methods with a name matching your variant name, but you can configure custom methods via the `methods:` keyword argument.
436
+ By default the above will attempt to call methods with a name matching your assigned variant name, but you can configure custom methods via the `methods:` keyword argument.
569
437
 
570
438
  ```ruby
571
439
  class MyController < ApplicationController
572
440
  def index
573
441
  # this would call one of the methods below depending on assignment
574
- trailguide.run(:experiment_name, methods: {
575
- variant_one: :my_first_method,
576
- variant_two: :my_second_method
577
- },
578
- metadata: {
579
- # you can also optionally pass custom metadata through to choose
580
- })
442
+ trailguide.run(:experiment_name,
443
+ methods: {
444
+ variant_one: :my_first_method,
445
+ variant_two: :my_second_method
446
+ },
447
+ metadata: {
448
+ # you can also optionally pass custom metadata through
449
+ })
581
450
  end
582
451
 
583
452
  private
584
453
 
585
454
  def my_first_method(**metadata)
586
- # ... do whatever, maybe use these almost like a `before_filter` to setup instance vars
455
+ # ... do whatever - you could use these almost like a `before_filter` to setup different instance vars depending on assignment
587
456
  end
588
457
 
589
458
  def my_second_method(**metadata)
@@ -596,7 +465,7 @@ You **can** use `trailguide.run` in your views, but the methods you're calling m
596
465
 
597
466
  #### Rendering
598
467
 
599
- Many experiments include some sort of UI component, and trailguide provides a handy shortcut to render different paths when that pattern suits your needs. The `trailguide.render` method can be used in controllers to render templates or in views to render partials, and uses rails' underlying render logic for each context.
468
+ Many experiments include some sort of UI component, and trailguide provides a handy shortcut to automatically render different templates/partials when that pattern suits your needs. The `trailguide.render` method can be used in controllers to render templates or in views to render partials, and uses rails' underlying render logic for each context.
600
469
 
601
470
  ```ruby
602
471
  # config/experiments/homepage_ab.rb
@@ -677,7 +546,7 @@ trailguide.convert(:experiment_name, :goal_name)
677
546
 
678
547
  The way you use trailguide outside of a request context will mostly depend on the participant adapter being used. To get started, you'll need to include the `TrailGuide::Helper` module into whatever class or context you're working with.
679
548
 
680
- The `:cookie` and `:session` adapters **will not work** in a background context, but the default `:redis`, `:multi` and `:unity` adapters will work if provided with a `trailguide_user`. This assumes that the `trailguide_user` matches whatever user you're assigning within your request contexts (which is commonly `current_user`) if you want assignments to match up and be consistent, and the default configurations for these supported adapters all look for either a `trailguide_user` or a `current_user` so they should work in most contexts.
549
+ The `:cookie` and `:session` adapters **will not work** in a background context, but the default `:redis`, `:multi` and `:unity` adapters will work if provided with a `trailguide_user`. This assumes that the `trailguide_user` matches the same user elsewhere in your request contexts (like in your controllers and views), which is commonly `current_user`. The default configurations for these supported adapters all look for either a `trailguide_user` or a `current_user` so they should work in most contexts.
681
550
 
682
551
  A simple example might be sending a welcome email in a background job with a variable discount amount depending on what variant the user was enrolled into during signup.
683
552
 
@@ -741,11 +610,7 @@ client.convert('experiment_name', 'optional_goal');
741
610
  client.active();
742
611
  ```
743
612
 
744
- ## Experiment Lifecycle
745
-
746
- **TODO**
747
-
748
- ## Goals
613
+ ## Conversion Goals
749
614
 
750
615
  You can configure experiment goals if a single experiment requires multiple conversion goals, or if you just want to define a single named goal to be more explicit.
751
616
 
@@ -759,8 +624,8 @@ experiment :button_color do |config|
759
624
  goal :checked_out
760
625
 
761
626
  # if this is false (default), once a participant converts to one of the defined goals, they will not be able to convert to any of the others unless the experiment is reset
762
- # if this is true, a single participant may convert to more than one goal, but only once each
763
- config.allow_multiple_goals = false
627
+ # if this is true, a single participant may convert to more than one goal, but only once each, which allows for simple conversion funnels
628
+ config.allow_multiple_goals = true
764
629
  end
765
630
  ```
766
631
 
@@ -770,7 +635,7 @@ When you define one or more named goals for an experiment, you must pass one of
770
635
  trailguide.convert(:button_color, :signed_up)
771
636
  ```
772
637
 
773
- ## Groups
638
+ ## Conversion Groups
774
639
 
775
640
  If you have multiple experiments that share a relevant conversion point, you can configure them with a shared group. This allows you to reference and convert multiple experiments at once using that shared group, and only experiments in which participants have been enrolled will be converted.
776
641
 
@@ -813,15 +678,15 @@ end
813
678
 
814
679
  ### Orphaned Groups
815
680
 
816
- Sometimes in the real world, you might accidentally remove all the experiments that were sharing a given group, but miss one of the conversion calls that used one of it's groups. Maybe you forgot to search through your code for references to the group, or maybe you just didn't know you were removing the last experiment in that group. Ideally you'd be testing your code thoroughly, and you'd catch the problem before hitting production, but trailguide has a built-in safe guard just in case.
681
+ Sometimes in the real world, you might accidentally remove all the experiments that were sharing a given group, but miss one of the conversion calls that used one of those groups. Maybe you forgot to search through your code for references to the group, or maybe you just didn't know you were removing the last experiment in that group. Ideally you'd be testing your code thoroughly, and you'd catch the problem before hitting production, but trailguide has a built-in safe guard just in case.
817
682
 
818
- Instead of raising a `TrailGuide::NoExperimentsError` when no experiments match your arguments like `trailguide.choose` and related methods do, the `trailguide.convert` method will log a warning and return `false` as if no conversion happened.
683
+ Instead of raising a `TrailGuide::NoExperimentsError` when no experiments match your arguments like the `trailguide.choose` and related methods do, the `trailguide.convert` method will log a warning and return `false` as if no conversion happened.
819
684
 
820
685
  After a failed conversion for an orphaned group, the next time you visit the trailguide admin dashboard you'll see an alert with the details of any logged orphaned groups. If you wish to ignore orphaned groups entirely, perhaps so you can leave conversion calls in your application while you regularly rotate experiments into and out of those groups, you can set the `TrailGuide.configuration.ignore_orphaned_groups = true` config option in your initializer.
821
686
 
822
687
  ### Groups with Goals
823
688
 
824
- Since grouping is only useful when converting, and experiments with defined goals require a goal to be passed in when converting, **any experiments that are sharing a group must define the same goals in order to be converted together.** Not all goals need to overlap, but you will only be able to convert goals that are shared when referencing a group.
689
+ Since grouping is primarily useful when converting, and experiments with defined goals require a goal to be passed in when converting, **any experiments that are sharing a group must define the same goals in order to be converted together.** Not all goals need to overlap, but you will only be able to convert goals that are shared when referencing a group.
825
690
 
826
691
  If you're grouping your experiments, that probably means you have multiple experiments that are all being used in the same area of your app and therefore are likely sharing the same (or similar) conversion goals. You can assign your groups and goals the same names to make converting easier by referencing a single key:
827
692
 
@@ -830,8 +695,8 @@ experiment :first_search_experiment do |config|
830
695
  variant :alpha
831
696
  variant :bravo
832
697
 
833
- config.groups = [:click_search, :click_banner, :search_experiments]
834
- config.goals = [:click_search, :click_banner, :custom_goal]
698
+ groups :click_search, :click_banner, :search_experiments
699
+ goals :click_search, :click_banner, :custom_goal
835
700
  end
836
701
 
837
702
  experiment :second_search_experiment do |config|
@@ -839,24 +704,24 @@ experiment :second_search_experiment do |config|
839
704
  variant :two
840
705
  variant :three
841
706
 
842
- config.groups = [:click_search, :click_banner, :search_experiments]
843
- config.goals = [:click_search, :click_banner, :some_other_goal]
707
+ groups :click_search, :click_banner, :search_experiments
708
+ goals :click_search, :click_banner, :some_other_goal
844
709
  end
845
710
 
846
711
  experiment :third_search_experiment do |config|
847
712
  variant :red
848
713
  variant :blue
849
714
 
850
- config.groups = [:click_search, :click_banner, :search_experiments]
851
- config.goals = [:click_search, :click_banner]
715
+ groups :click_search, :click_banner, :search_experiments
716
+ goals :click_search, :click_banner
852
717
  end
853
718
 
854
719
  # then to convert all three experiments for the click_search group, against the
855
720
  # click_search goal
856
721
  trailguide.convert(:click_search)
857
722
 
858
- # the above is the equivalent of calling their group name (in this case not
859
- # matching the goal name) and the goal name
723
+ # the above is the equivalent of calling them by a shared group name (in this example
724
+ # the :search_experiments group) and the conversion goal name
860
725
  trailguide.convert(:search_experiments, :click_search)
861
726
 
862
727
  # or the equivalent of converting each of the three experiments individually
@@ -870,6 +735,39 @@ trailguide.convert(:third_search_experiment, :click_search)
870
735
  trailguide.convert(:first_search_experiment, :custom_goal)
871
736
  ```
872
737
 
738
+ ### Metrics
739
+
740
+ Metrics are a quick way to combine groups and goals and remove some of the boilerplate when sharing them between experiments. Using the `metrics` config, we can simplify the above examples a bit:
741
+
742
+ ```ruby
743
+ experiment :first_search_experiment do |config|
744
+ variant :alpha
745
+ variant :bravo
746
+
747
+ metrics :click_search, :click_banner # this configures a group and a goal for each metric
748
+ group :search_experiments # add another group (without a goal)
749
+ goal :custom_goal # add another goal (without a group)
750
+ end
751
+
752
+ experiment :second_search_experiment do |config|
753
+ variant :one
754
+ variant :two
755
+ variant :three
756
+
757
+ metrics :click_search, :click_banner # this configures a group and a goal for each metric
758
+ group :search_experiments # add another group (without a goal)
759
+ goal :some_other_goal # add another goal (without a group)
760
+ end
761
+
762
+ experiment :third_search_experiment do |config|
763
+ variant :red
764
+ variant :blue
765
+
766
+ metrics :click_search, :click_banner # this configures a group and a goal for each metric
767
+ group :search_experiments # add another group (without a goal)
768
+ end
769
+ ```
770
+
873
771
  ## Combined Experiments
874
772
 
875
773
  **TODO**