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.
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 as a datastore for experiment metadata.
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 flags available to customize behavior and hook into various pieces of functionality. The preferred way to configure trailguide is via a config initializer, and the gem sets it's config defaults via it's own initializer.
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
- ### Defining Experiments
122
+ ### Configuring Experiments
54
123
 
55
- 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.
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: true
143
+ start_manually: false
75
144
  algorithm: 'distributed'
76
145
  variants:
77
- - 'basic'
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 = true
159
+ config.start_manually = false
91
160
  config.algorithm = :distributed
92
- config.allow_multiple_goals = true
93
161
 
94
- variant :basic
95
- variant :simple, control: true
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
- Contribution directions go here.
830
+ **TODO**
178
831
 
179
832
  ## License
180
833