trailguide 0.1.30 → 0.1.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +676 -23
- data/app/controllers/trail_guide/admin/application_controller.rb +3 -3
- data/app/controllers/trail_guide/admin/experiments_controller.rb +21 -5
- data/app/views/layouts/trail_guide/admin/_header.erb +31 -3
- data/app/views/trail_guide/admin/experiments/_combined_experiment.html.erb +29 -7
- data/app/views/trail_guide/admin/experiments/_experiment.html.erb +29 -7
- data/app/views/trail_guide/admin/experiments/index.html.erb +1 -1
- data/config/initializers/trailguide.rb +33 -3
- data/config/routes/admin.rb +22 -0
- data/config/routes/engine.rb +13 -0
- data/config/routes.rb +5 -31
- data/lib/trail_guide/adapters/participants/multi.rb +2 -1
- data/lib/trail_guide/adapters/participants/redis.rb +4 -1
- data/lib/trail_guide/catalog.rb +12 -0
- data/lib/trail_guide/combined_experiment.rb +9 -1
- data/lib/trail_guide/config.rb +4 -4
- data/lib/trail_guide/experiments/base.rb +21 -5
- data/lib/trail_guide/experiments/config.rb +13 -4
- data/lib/trail_guide/helper.rb +48 -0
- data/lib/trail_guide/version.rb +1 -1
- data/lib/trailguide.rb +1 -1
- metadata +4 -2
data/README.md
CHANGED
@@ -2,24 +2,16 @@
|
|
2
2
|
|
3
3
|
TrailGuide is a rails engine providing a framework for running user experiments and A/B tests in rails apps.
|
4
4
|
|
5
|
-
## Acknowledgements
|
6
|
-
|
7
|
-
This gem is heavily inspired by the [split gem](https://github.com/splitrb/split). I've used split many times in the past and am a fan. It's an excellent alternative to trailguide, and really your best bet if you're not using rails. If you've used split in the past, you'll probably see a lot of familiar concepts and similarly named configuration variables. Parts of this project are even loosely modeled after some of the more brilliant patterns in split - like the user adapters for persistence.
|
8
|
-
|
9
|
-
### Motivation
|
10
|
-
|
11
|
-
While working on a project to more deeply integrate custom experiments into a rails app, I found myself digging into the split internals. Split has been the go-to for A/B testing in ruby for a while. It's grown and evolved over the years, but as I explored the codebase and the github repo it became clear I wouldn't be able to do a lot of what was required for the project without overriding much of the existing behavior. Additionally, there are some differing opinions and approaches taken here that directly conflicted with split's defaults - for example the way "combined experiments" work, or how split allows defining and running experiments directly inline, while trailguide requires configuration.
|
12
|
-
|
13
|
-
After spending so much time with split and struggling with some of the implementation, I saw what I thought was a clear model and path forward for a more customizable and extensible rails-focused framework.
|
14
|
-
|
15
5
|
## Getting Started
|
16
6
|
|
17
7
|
### Requirements
|
18
8
|
|
19
|
-
Currently only rails 5.x is officially supported, and trailguide requires redis
|
9
|
+
Currently only rails 5.x is officially tested/supported, and trailguide requires redis to store experiment metadata and (optionally) participants.
|
20
10
|
|
21
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.
|
22
12
|
|
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.
|
14
|
+
|
23
15
|
### Installation
|
24
16
|
|
25
17
|
Add this line to your Gemfile:
|
@@ -30,9 +22,86 @@ gem 'trailguide'
|
|
30
22
|
|
31
23
|
Then run `bundle install`.
|
32
24
|
|
25
|
+
### Engine & Admin Routes
|
26
|
+
|
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:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
# /config/routes.rb
|
31
|
+
|
32
|
+
Rails.application.routes.draw do
|
33
|
+
# ...
|
34
|
+
|
35
|
+
mount TrailGuide::Engine => 'api/experiments'
|
36
|
+
|
37
|
+
# ...
|
38
|
+
end
|
39
|
+
```
|
40
|
+
|
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.
|
42
|
+
|
43
|
+
```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
|
55
|
+
|
56
|
+
# ...
|
57
|
+
end
|
58
|
+
```
|
59
|
+
|
60
|
+
### Quick Start
|
61
|
+
|
62
|
+
Create and configure an experiment:
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
# config/experiments.rb
|
66
|
+
|
67
|
+
experiment :simple_ab do |config|
|
68
|
+
config.summary = "This is a simple A/B test" # optional
|
69
|
+
|
70
|
+
variant :a
|
71
|
+
variant :b
|
72
|
+
end
|
73
|
+
```
|
74
|
+
|
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
|
+
|
79
|
+
```ruby
|
80
|
+
def show
|
81
|
+
# 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"
|
87
|
+
end
|
88
|
+
|
89
|
+
# ...
|
90
|
+
end
|
91
|
+
|
92
|
+
def update
|
93
|
+
# mark this participant as having converted when they take a certain action
|
94
|
+
trailguide.convert(:simple_ab)
|
95
|
+
|
96
|
+
# ...
|
97
|
+
end
|
98
|
+
```
|
99
|
+
|
100
|
+
If you've mounted the admin engine, you can view your experiment's participants and conversions there.
|
101
|
+
|
33
102
|
## Configuration
|
34
103
|
|
35
|
-
The core engine and base experiment class have a number of configuration
|
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.
|
36
105
|
|
37
106
|
```ruby
|
38
107
|
# config/initializers/trailguide.rb
|
@@ -48,11 +117,11 @@ TrailGuide::Experiment.configure do |config|
|
|
48
117
|
end
|
49
118
|
```
|
50
119
|
|
51
|
-
Take a look at `config/initializers/trailguide.rb` in this for a full list of defaults and examples of available configuration.
|
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.
|
52
121
|
|
53
|
-
###
|
122
|
+
### Configuring Experiments
|
54
123
|
|
55
|
-
Before you can start running experiments in your app
|
124
|
+
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.
|
56
125
|
|
57
126
|
#### YAML
|
58
127
|
|
@@ -71,10 +140,10 @@ simple_ab:
|
|
71
140
|
# config/experiments/search/widget.yml
|
72
141
|
|
73
142
|
search_widget:
|
74
|
-
start_manually:
|
143
|
+
start_manually: false
|
75
144
|
algorithm: 'distributed'
|
76
145
|
variants:
|
77
|
-
- '
|
146
|
+
- 'original'
|
78
147
|
- 'simple'
|
79
148
|
- 'advanced'
|
80
149
|
```
|
@@ -87,12 +156,13 @@ The ruby DSL provides a more dynamic and flexible way to configure your experime
|
|
87
156
|
# config/experiments.rb
|
88
157
|
|
89
158
|
experiment :search_widget do |config|
|
90
|
-
config.start_manually =
|
159
|
+
config.start_manually = false
|
91
160
|
config.algorithm = :distributed
|
92
|
-
config.allow_multiple_goals = true
|
93
161
|
|
94
|
-
variant
|
95
|
-
|
162
|
+
# the first variant is your control by default, but you can declare any one as
|
163
|
+
# the control like we do below
|
164
|
+
variant :simple
|
165
|
+
variant :original, control: true
|
96
166
|
variant :advanced
|
97
167
|
|
98
168
|
goal :interacted
|
@@ -153,7 +223,6 @@ end
|
|
153
223
|
You can also use inheritance to setup base experiments and inherit configuration:
|
154
224
|
|
155
225
|
```ruby
|
156
|
-
|
157
226
|
class ApplicationExperiment < TrailGuide::Experiment
|
158
227
|
configure do |config|
|
159
228
|
# ... config, variants, etc.
|
@@ -170,11 +239,595 @@ class MyDefaultExperiment < TrailGuide::Experiment
|
|
170
239
|
end
|
171
240
|
```
|
172
241
|
|
242
|
+
You can even use these in your DSL-defined experiments by specifying a `class:` argument:
|
243
|
+
|
244
|
+
```ruby
|
245
|
+
# config/experiments.rb
|
246
|
+
|
247
|
+
experiment :my_inheriting_experiment, class: ApplicationExperiment do |config|
|
248
|
+
# ...
|
249
|
+
end
|
250
|
+
```
|
251
|
+
|
252
|
+
### Participant Adapters
|
253
|
+
|
254
|
+
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.
|
255
|
+
|
256
|
+
The following participant adapters are included with trailguide:
|
257
|
+
|
258
|
+
* `:cookie` (default) - stores participant assignments in a cookie in their browser
|
259
|
+
* `:session` - stores participant assignments in a hash in their rails session
|
260
|
+
* `: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)
|
261
|
+
* `: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)
|
262
|
+
* `:multi` - attempts to use the "best" available adapter based on the current context
|
263
|
+
* `:unity` - uses `TrailGuide::Unity` to attempt to unify visitor/user sessions based on your configuration
|
264
|
+
|
265
|
+
#### Cookie
|
266
|
+
|
267
|
+
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:
|
268
|
+
|
269
|
+
```ruby
|
270
|
+
TrailGuide.configure do |config|
|
271
|
+
# config.adapter = :cookie
|
272
|
+
config.adapter = TrailGuide::Adapters::Participants::Cookie.configure do |config|
|
273
|
+
config.cookie = :trailguide
|
274
|
+
config.path = '/'
|
275
|
+
config.expiration = 1.year.to_i
|
276
|
+
end
|
277
|
+
end
|
278
|
+
```
|
279
|
+
|
280
|
+
#### Session
|
281
|
+
|
282
|
+
The session adapter will store participation in a hash under a configurable key within the user's rails session.
|
283
|
+
|
284
|
+
```ruby
|
285
|
+
TrailGuide.configure do |config|
|
286
|
+
# use the symbol shortcut for defaults
|
287
|
+
config.adapter = :session
|
288
|
+
|
289
|
+
# or configure it
|
290
|
+
config.adapter = TrailGuide::Adapters::Participants::Session.configure do |config|
|
291
|
+
config.key = :trailguide
|
292
|
+
end
|
293
|
+
end
|
294
|
+
```
|
295
|
+
|
296
|
+
#### Redis
|
297
|
+
|
298
|
+
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`).
|
299
|
+
|
300
|
+
```ruby
|
301
|
+
TrailGuide.configure do |config|
|
302
|
+
# use the symbol shortcut for defaults
|
303
|
+
config.adapter = :redis
|
304
|
+
|
305
|
+
# or configure it
|
306
|
+
config.adapter = TrailGuide::Adapters::Participants::Redis.configure do |config|
|
307
|
+
config.namespace = :participants
|
308
|
+
config.expiration = nil
|
309
|
+
config.lookup = -> (context) { # context is wherever you're invoking trailguide, usually a controller or view
|
310
|
+
context.try(:trailguide_user).try(:id) ||
|
311
|
+
context.try(:current_user).try(:id)
|
312
|
+
}
|
313
|
+
end
|
314
|
+
end
|
315
|
+
```
|
316
|
+
|
317
|
+
#### Anonymous
|
318
|
+
|
319
|
+
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).
|
320
|
+
|
321
|
+
#### Multi
|
322
|
+
|
323
|
+
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.
|
324
|
+
|
325
|
+
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:
|
326
|
+
|
327
|
+
```ruby
|
328
|
+
TrailGuide.configure do |config|
|
329
|
+
# use the symbol shortcut for defaults
|
330
|
+
config.adapter = :multi
|
331
|
+
|
332
|
+
# or configure it
|
333
|
+
config.adapter = TrailGuide::Adapters::Participants::Multi.configure do |config|
|
334
|
+
# should be a proc that returns another adapter to be used
|
335
|
+
config.adapter = -> (context) do
|
336
|
+
if (context.respond_to?(:trailguide_user, true) && context.send(:trailguide_user).present?) ||
|
337
|
+
(context.respond_to?(:current_user, true) && context.send(:current_user).present?)
|
338
|
+
TrailGuide::Adapters::Participants::Redis
|
339
|
+
elsif context.respond_to?(:cookies, true)
|
340
|
+
TrailGuide::Adapters::Participants::Cookie
|
341
|
+
elsif context.respond_to?(:session, true)
|
342
|
+
TrailGuide::Adapters::Participants::Session
|
343
|
+
else
|
344
|
+
TrailGuide::Adapters::Participants::Anonymous
|
345
|
+
end
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
349
|
+
```
|
350
|
+
|
351
|
+
#### Unity
|
352
|
+
|
353
|
+
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.
|
354
|
+
|
355
|
+
You can configure the visitor cookie and the user id attribute, as well as the adapters to be used in each case:
|
356
|
+
|
357
|
+
```ruby
|
358
|
+
TrailGuide.configure do |config|
|
359
|
+
config.adapter = TrailGuide::Adapters::Participants::Unity.configure do |config|
|
360
|
+
# setup the visitor ID cookie and user ID attribute
|
361
|
+
config.visitor_cookie = :visitor_id # uses a cookie called visitor_id, must be set and managed by you separately
|
362
|
+
config.user_id_key = :uuid # uses current_user.uuid, defaults to current_user.id
|
363
|
+
|
364
|
+
# uses redis adapter for identified users
|
365
|
+
config.user_adapter = TrailGuide::Adapters::Participants::Redis.configure do |config|
|
366
|
+
config.namespace = 'unity:users'
|
367
|
+
config.lookup = -> (user_id) { user_id }
|
368
|
+
config.expiration = 1.year.seconds
|
369
|
+
end
|
370
|
+
|
371
|
+
# uses redis adapter for identified visitors
|
372
|
+
config.visitor_adapter = TrailGuide::Adapters::Participants::Redis.configure do |config|
|
373
|
+
config.namespace = 'unity:visitors'
|
374
|
+
config.lookup = -> (visitor_id) { visitor_id }
|
375
|
+
config.expiration = 1.year.seconds
|
376
|
+
end
|
377
|
+
|
378
|
+
# uses anonymous adapter for unidentified
|
379
|
+
config.anonymous_adapter = TrailGuide::Adapters::Participants::Anonymous
|
380
|
+
end
|
381
|
+
end
|
382
|
+
```
|
383
|
+
|
384
|
+
See the unity documentation for more info about unifying sessions.
|
385
|
+
|
386
|
+
#### Custom Adapters
|
387
|
+
|
388
|
+
**TODO** - In the meantime, checkout the cookie or session adapters for simple examples as a starting point.
|
389
|
+
|
390
|
+
```ruby
|
391
|
+
TrailGuide.configure do |config|
|
392
|
+
config.adapter = MyCustom::AdapterClass
|
393
|
+
end
|
394
|
+
```
|
395
|
+
|
396
|
+
### Algorithms
|
397
|
+
|
398
|
+
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.
|
399
|
+
|
400
|
+
The following algorithms are available:
|
401
|
+
|
402
|
+
* `:weighted` (default) - allows favoring variants by assigning them weights
|
403
|
+
* `:distributed` - totally even distribution across variants
|
404
|
+
* `:random` - truly random sampling of variants on assignment
|
405
|
+
* `:bandit` - a "multi-armed bandit" approach to assignment
|
406
|
+
|
407
|
+
#### Weighted
|
408
|
+
|
409
|
+
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.
|
410
|
+
|
411
|
+
```ruby
|
412
|
+
experiment :my_experiment do |config|
|
413
|
+
config.algorithm = :weighted
|
414
|
+
|
415
|
+
variant :a, weight: 2 # would be assigned roughly 40% of the time
|
416
|
+
variant :b, weight: 2 # would be assigned roughly 40% of the time
|
417
|
+
variant :c, weight: 1 # would be assigned roughly 20% of the time
|
418
|
+
end
|
419
|
+
```
|
420
|
+
|
421
|
+
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.
|
422
|
+
|
423
|
+
#### Distributed
|
424
|
+
|
425
|
+
The distributed algorithm ensures completely even distribution across all variants by always selecting from the variant(s) with the lowest number of participants.
|
426
|
+
|
427
|
+
```ruby
|
428
|
+
experiment :my_experiment do |config|
|
429
|
+
config.algorithm = :distributed
|
430
|
+
end
|
431
|
+
```
|
432
|
+
|
433
|
+
#### Random
|
434
|
+
|
435
|
+
The random algorithm provides totally random distribution by sampling from all variants on assignment.
|
436
|
+
|
437
|
+
```ruby
|
438
|
+
experiment :my_experiment do |config|
|
439
|
+
config.algorithm = :random
|
440
|
+
end
|
441
|
+
```
|
442
|
+
|
443
|
+
#### Multi-Armed Bandit
|
444
|
+
|
445
|
+
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.
|
446
|
+
|
447
|
+
```ruby
|
448
|
+
experiment :my_experiment do |config|
|
449
|
+
config.algorithm = :bandit
|
450
|
+
end
|
451
|
+
```
|
452
|
+
|
453
|
+
#### Custom
|
454
|
+
|
455
|
+
**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.
|
456
|
+
|
457
|
+
```ruby
|
458
|
+
experiment :my_experiment do |config|
|
459
|
+
config.algorithm = MyCustom::AlgorithmClass
|
460
|
+
end
|
461
|
+
```
|
462
|
+
|
173
463
|
## Usage
|
174
464
|
|
465
|
+
### Helpers
|
466
|
+
|
467
|
+
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.
|
468
|
+
|
469
|
+
When mixed in, the `trailguide` method provides a reference to the helper proxy, which in turn provides a few methods to perform your experiments.
|
470
|
+
|
471
|
+
```ruby
|
472
|
+
# enroll in an experiment or reuse previous assignment
|
473
|
+
trailguide.choose(:experiment_name)
|
474
|
+
trailguide.choose!(:experiment_name)
|
475
|
+
|
476
|
+
# choose, then automatically calls a method within the current context based on
|
477
|
+
# the selected variant
|
478
|
+
trailguide.run(:experiment_name)
|
479
|
+
trailguide.run!(:experiment_name)
|
480
|
+
|
481
|
+
# choose, then render a template or partial within the current context based on
|
482
|
+
# the selected variant
|
483
|
+
trailguide.render(:experiment_name)
|
484
|
+
trailguide.render!(:experiment_name)
|
485
|
+
|
486
|
+
# tracks a conversion for the participant's currently assigned variant
|
487
|
+
trailguide.convert(:experiment_name)
|
488
|
+
trailguide.convert!(:experiment_name)
|
489
|
+
```
|
490
|
+
|
491
|
+
As a general rule of thumb, the bang (`!`) methods will loudly raise exceptions on any failures, while the non-bang methods will log errors and do their best to gracefully continue.
|
492
|
+
|
493
|
+
#### Enrollment
|
494
|
+
|
495
|
+
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.
|
496
|
+
|
497
|
+
```ruby
|
498
|
+
class MyController < ApplicationController
|
499
|
+
def index
|
500
|
+
# choose inline
|
501
|
+
variant = trailguide.choose(:experiment_name) # TrailGuide::Variant instance
|
502
|
+
if variant == 'variant_one'
|
503
|
+
# ...
|
504
|
+
elsif variant == 'variant_two'
|
505
|
+
# ...
|
506
|
+
end
|
507
|
+
|
508
|
+
# use directly in a case or other comparison
|
509
|
+
case trailguide.choose(:experiment_name)
|
510
|
+
when :variant_one
|
511
|
+
# ...
|
512
|
+
when :variant_two
|
513
|
+
# ...
|
514
|
+
end
|
515
|
+
|
516
|
+
|
517
|
+
# pass in a block
|
518
|
+
trailguide.choose(:experiment_name) do |variant, metadata|
|
519
|
+
# ... do something based on the assigned variant
|
520
|
+
end
|
521
|
+
|
522
|
+
|
523
|
+
# also accepts additional metadata which can be used in custom algorithms and
|
524
|
+
# passed to blocks along with any configured variant metadata
|
525
|
+
trailguide.choose(:experiment_name, metadata: {foo: :bar}) do |variant, metadata|
|
526
|
+
# ...
|
527
|
+
end
|
528
|
+
end
|
529
|
+
end
|
530
|
+
```
|
531
|
+
|
532
|
+
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`:
|
533
|
+
|
534
|
+
```erb
|
535
|
+
<% variant = trailguide.choose(:experiment_name) %>
|
536
|
+
<h1><%= variant.name %></h1>
|
537
|
+
```
|
538
|
+
|
539
|
+
#### Running Methods
|
540
|
+
|
541
|
+
If you prefer, you can encapsulate your logic into methods for each variant and ask trailguide to execute the appropriate one for you automatically.
|
542
|
+
|
543
|
+
```ruby
|
544
|
+
class MyController < ApplicationController
|
545
|
+
def index
|
546
|
+
# this would call one of the methods below depending on assignment
|
547
|
+
trailguide.run(:experiment_name)
|
548
|
+
end
|
549
|
+
|
550
|
+
private
|
551
|
+
|
552
|
+
def variant_one(**metadata)
|
553
|
+
# ... do whatever, maybe use these almost like a `before_filter` to setup instance vars
|
554
|
+
end
|
555
|
+
|
556
|
+
def variant_two(**metadata)
|
557
|
+
# ...
|
558
|
+
end
|
559
|
+
end
|
560
|
+
```
|
561
|
+
|
562
|
+
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.
|
563
|
+
|
564
|
+
```ruby
|
565
|
+
class MyController < ApplicationController
|
566
|
+
def index
|
567
|
+
# this would call one of the methods below depending on assignment
|
568
|
+
trailguide.run(:experiment_name, methods: {
|
569
|
+
variant_one: :my_first_method,
|
570
|
+
variant_two: :my_second_method
|
571
|
+
},
|
572
|
+
metadata: {
|
573
|
+
# you can also optionally pass custom metadata through to choose
|
574
|
+
})
|
575
|
+
end
|
576
|
+
|
577
|
+
private
|
578
|
+
|
579
|
+
def my_first_method(**metadata)
|
580
|
+
# ... do whatever, maybe use these almost like a `before_filter` to setup instance vars
|
581
|
+
end
|
582
|
+
|
583
|
+
def my_second_method(**metadata)
|
584
|
+
# ...
|
585
|
+
end
|
586
|
+
end
|
587
|
+
```
|
588
|
+
|
589
|
+
You **can** use `trailguide.run` in your views, but the methods you're calling must be available in that context. This usually means defining them as helper methods, either in your controller via `helper_method` or in a helpers module.
|
590
|
+
|
591
|
+
#### Rendering
|
592
|
+
|
593
|
+
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.
|
594
|
+
|
595
|
+
```ruby
|
596
|
+
# config/experiments/homepage_ab.rb
|
597
|
+
experiment :homepage_ab do |config|
|
598
|
+
variant :old
|
599
|
+
variant :new
|
600
|
+
end
|
601
|
+
|
602
|
+
# app/controllers/homepage_controller.rb
|
603
|
+
class HomepageController < ApplicationController
|
604
|
+
def index
|
605
|
+
trailguide.render(:homepage_experiment)
|
606
|
+
end
|
607
|
+
end
|
608
|
+
|
609
|
+
# this would render one of these templates within the layout (instead of homepage/index.html.erb)
|
610
|
+
# app/views/homepage/homepage_ab/old.html.erb
|
611
|
+
# app/views/homepage/homepage_ab/new.html.erb
|
612
|
+
```
|
613
|
+
|
614
|
+
You can also use render in a view to render partials instead of templates.
|
615
|
+
|
616
|
+
```ruby
|
617
|
+
# config/experiments/homepage_hero.rb
|
618
|
+
experiment :homepage_hero do |config|
|
619
|
+
variant :old
|
620
|
+
variant :new
|
621
|
+
end
|
622
|
+
|
623
|
+
# app/controllers/homepage_controller.rb
|
624
|
+
class HomepageController < ApplicationController
|
625
|
+
def index
|
626
|
+
end
|
627
|
+
end
|
628
|
+
```
|
629
|
+
|
630
|
+
```erb
|
631
|
+
<!-- app/views/homepage/index.html.erb -->
|
632
|
+
<%= trailguide.render(:homepage_hero) %>
|
633
|
+
|
634
|
+
<!-- this would render one of these partials -->
|
635
|
+
<!-- app/views/homepage/homepage_hero/_old.html.erb -->
|
636
|
+
<!-- app/views/homepage/homepage_hero/_new.html.erb -->
|
637
|
+
```
|
638
|
+
|
639
|
+
By default the render method looks for templates or partials matching the assigned experiment and variant within the current render context path. For templates (in controllers) this means something like `app/views/your_controller/experiment_name/variant_name.*`, and for partials (in views) something like `app/views/your_controller/experiment_name/_variant_name.*` (note the underscore for partials, following rails' conventions).
|
640
|
+
|
641
|
+
You can override the prefix or the full paths to the individual templates via the `prefix:` and `templates:` keyword args respectively.
|
642
|
+
|
643
|
+
```ruby
|
644
|
+
# looks for variant templates in app/views/foo/bar/experiment_name/*
|
645
|
+
trailguide.render(:experiment_name, prefix: 'foo/bar')
|
646
|
+
|
647
|
+
# specify the path for each variant's template (relative to rails view path)
|
648
|
+
trailguide.render(:experiment_name, templates: {
|
649
|
+
variant_one: 'foo/bar/custom',
|
650
|
+
variant_two: 'other/custom/template'
|
651
|
+
})
|
652
|
+
|
653
|
+
# renders one of these
|
654
|
+
# app/views/foo/bar/custom.html.erb
|
655
|
+
# app/views/other/custom/template.html.erb
|
656
|
+
```
|
657
|
+
|
658
|
+
#### Conversion
|
659
|
+
|
660
|
+
In order to analyze performance and potentially select a winning variant, you'll want to track a conversion metric relevant to your experiment. This might mean clicking a button, creating an account, adding something to a shopping cart, completing an order, or some other interaction performed by the user. You can convert a participant from pretty much any context with `trailguide.convert`.
|
661
|
+
|
662
|
+
```ruby
|
663
|
+
# converts the participant in their assigned variant, or does nothing if they haven't been enrolled in the experiment
|
664
|
+
trailguide.convert(:experiment_name)
|
665
|
+
|
666
|
+
# requires a goal for experiments configured with multiple goals
|
667
|
+
trailguide.convert(:experiment_name, :goal_name)
|
668
|
+
```
|
669
|
+
|
670
|
+
### Service Objects & Background Jobs
|
671
|
+
|
672
|
+
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.
|
673
|
+
|
674
|
+
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.
|
675
|
+
|
676
|
+
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.
|
677
|
+
|
678
|
+
```ruby
|
679
|
+
# config/experiments.rb
|
680
|
+
experiment :welcome_discount do |config|
|
681
|
+
variant :10
|
682
|
+
variant :15
|
683
|
+
end
|
684
|
+
|
685
|
+
# app/controllers/users_controller.rb
|
686
|
+
class UsersController < ApplicationController
|
687
|
+
def create
|
688
|
+
# ... signup the user
|
689
|
+
amount = trailguide.choose(:welcome_discount)
|
690
|
+
flash[:info] = "Check your email for a $#{amount} discount!"
|
691
|
+
SendWelcomeEmailJob.perform_later(current_user)
|
692
|
+
end
|
693
|
+
end
|
694
|
+
|
695
|
+
# app/jobs/send_welcome_email_job.rb
|
696
|
+
class SendWelcomeEmailJob < ApplicationJob
|
697
|
+
include TrailGuide::Helper
|
698
|
+
|
699
|
+
def perform(user)
|
700
|
+
# set this to an instance var before choosing so it's available in the supported trailguide_user method
|
701
|
+
@user = user
|
702
|
+
|
703
|
+
amount = trailguide.choose(:welcome_discount)
|
704
|
+
UserMailer.welcome_email(@user, amount)
|
705
|
+
end
|
706
|
+
|
707
|
+
# using one of the supported adapters will automatically call this method if it exists
|
708
|
+
def trailguide_user
|
709
|
+
@user
|
710
|
+
end
|
711
|
+
end
|
712
|
+
```
|
713
|
+
|
714
|
+
If you're using a custom adapter, you'll need to make sure that your adapter is able to infer the participant from your context.
|
715
|
+
|
716
|
+
## JavaScript Client
|
717
|
+
|
718
|
+
There is a simple javascript client available that mimics the ruby usage as closely as possible, and is ready to be used with the rails asset pipeline. This client uses axios to hit the API, and requires that you mount it in your routes.
|
719
|
+
|
720
|
+
```javascript
|
721
|
+
// require the trailguide client in your application.js or wherever makes sense
|
722
|
+
//= require trailguide
|
723
|
+
|
724
|
+
// create a client instance
|
725
|
+
// make sure to pass in the route path where you've mounted the trailguide engine
|
726
|
+
var client = TrailGuide.client('/api/experiments');
|
727
|
+
|
728
|
+
// enroll in an experiment
|
729
|
+
client.choose('experiment_name');
|
730
|
+
|
731
|
+
// convert for an experiment with an optional goal
|
732
|
+
client.convert('experiment_name', 'optional_goal');
|
733
|
+
|
734
|
+
// return the participant's active experiments and their assigned variant group
|
735
|
+
client.active();
|
736
|
+
```
|
737
|
+
|
738
|
+
## Experiment Lifecycle
|
739
|
+
|
740
|
+
**TODO**
|
741
|
+
|
742
|
+
## Goals
|
743
|
+
|
744
|
+
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.
|
745
|
+
|
746
|
+
```ruby
|
747
|
+
experiment :button_color do |config|
|
748
|
+
variant :red
|
749
|
+
variant :green
|
750
|
+
variant :blue
|
751
|
+
|
752
|
+
goal :signed_up
|
753
|
+
goal :checked_out
|
754
|
+
|
755
|
+
# 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
|
756
|
+
# if this is true, a single participant may convert to more than one goal, but only once each
|
757
|
+
config.allow_multiple_goals = false
|
758
|
+
end
|
759
|
+
```
|
760
|
+
|
761
|
+
When you define one or more named goals for an experiment, you must pass one of the defined goals when converting.
|
762
|
+
|
763
|
+
```ruby
|
764
|
+
trailguide.convert(:button_color, :signed_up)
|
765
|
+
```
|
766
|
+
|
767
|
+
## Metrics
|
768
|
+
|
769
|
+
If you have multiple experiments that share a relevant conversion point, you can configure them with a shared metric. This allows you to reference and convert multiple experiments at once using that shared metric, and only experiments in which participants have been enrolled will be converted.
|
770
|
+
|
771
|
+
Shared metrics can only be used for conversion, not for enrollment, since experiments don't share assignments.
|
772
|
+
|
773
|
+
For example if you have multiple experiments where performing a search is considered to be a successful conversion, you can configure them all with the same shared metric then use that metric in your calls to `trailguide.convert`.
|
774
|
+
|
775
|
+
```ruby
|
776
|
+
experiment :first_search_experiment do |config|
|
777
|
+
config.metric = :perform_search
|
778
|
+
|
779
|
+
variant :a
|
780
|
+
variant :b
|
781
|
+
end
|
782
|
+
|
783
|
+
experiment :second_search_experiment do |config|
|
784
|
+
config.metric = :perform_search
|
785
|
+
|
786
|
+
variant :one
|
787
|
+
variant :two
|
788
|
+
variant :three
|
789
|
+
end
|
790
|
+
|
791
|
+
experiment :third_search_experiment do |config|
|
792
|
+
config.metric = :perform_search
|
793
|
+
|
794
|
+
variant :red
|
795
|
+
variant :blue
|
796
|
+
end
|
797
|
+
|
798
|
+
class SearchController < ApplicationController
|
799
|
+
def search
|
800
|
+
trailguide.convert(:perform_search)
|
801
|
+
# ...
|
802
|
+
end
|
803
|
+
end
|
804
|
+
```
|
805
|
+
|
806
|
+
Since experiments with defined goals require a goal to be passed in when converting, any experiments that are sharing a metric must define the same goals.
|
807
|
+
|
808
|
+
## Combined Experiments
|
809
|
+
|
810
|
+
**TODO**
|
811
|
+
|
812
|
+
## Filtering Requests
|
813
|
+
|
814
|
+
**TODO**
|
815
|
+
|
816
|
+
## Admin UI
|
817
|
+
|
818
|
+
**TODO**
|
819
|
+
|
820
|
+
## API
|
821
|
+
|
822
|
+
**TODO**
|
823
|
+
|
824
|
+
## RSpec Helpers
|
825
|
+
|
826
|
+
**TODO**
|
827
|
+
|
175
828
|
## Contributing
|
176
829
|
|
177
|
-
|
830
|
+
**TODO**
|
178
831
|
|
179
832
|
## License
|
180
833
|
|