trailguide 0.2.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +191 -293
- data/app/views/trail_guide/admin/experiments/_btn_join.html.erb +1 -1
- data/app/views/trail_guide/admin/experiments/_header.html.erb +3 -3
- data/config/initializers/admin.rb +19 -0
- data/config/initializers/experiment.rb +261 -0
- data/config/initializers/trailguide.rb +6 -279
- data/lib/trail_guide/adapters.rb +2 -0
- data/lib/trail_guide/adapters/experiments.rb +8 -0
- data/lib/trail_guide/adapters/experiments/redis.rb +48 -0
- data/lib/trail_guide/adapters/participants/cookie.rb +1 -0
- data/lib/trail_guide/adapters/participants/unity.rb +9 -1
- data/lib/trail_guide/adapters/variants.rb +8 -0
- data/lib/trail_guide/adapters/variants/redis.rb +52 -0
- data/lib/trail_guide/admin/engine.rb +1 -0
- data/lib/trail_guide/algorithms.rb +4 -0
- data/lib/trail_guide/algorithms/algorithm.rb +29 -0
- data/lib/trail_guide/algorithms/bandit.rb +9 -18
- data/lib/trail_guide/algorithms/distributed.rb +8 -15
- data/lib/trail_guide/algorithms/random.rb +2 -12
- data/lib/trail_guide/algorithms/static.rb +34 -0
- data/lib/trail_guide/algorithms/weighted.rb +5 -17
- data/lib/trail_guide/catalog.rb +79 -35
- data/lib/trail_guide/config.rb +2 -4
- data/lib/trail_guide/engine.rb +2 -1
- data/lib/trail_guide/experiments/base.rb +41 -24
- data/lib/trail_guide/experiments/combined_config.rb +4 -0
- data/lib/trail_guide/experiments/config.rb +59 -30
- data/lib/trail_guide/experiments/participant.rb +4 -2
- data/lib/trail_guide/helper.rb +4 -216
- data/lib/trail_guide/helper/experiment_proxy.rb +160 -0
- data/lib/trail_guide/helper/helper_proxy.rb +62 -0
- data/lib/trail_guide/metrics/config.rb +2 -0
- data/lib/trail_guide/metrics/goal.rb +17 -15
- data/lib/trail_guide/participant.rb +10 -2
- data/lib/trail_guide/unity.rb +17 -8
- data/lib/trail_guide/variant.rb +15 -11
- data/lib/trail_guide/version.rb +2 -2
- metadata +13 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a1f31e734d2b0165ffe08887a4e7a21cceebe8c6aba8c4ff72d9393f1fd6095a
|
4
|
+
data.tar.gz: ea8cd225a315add5f4512f8bc38bd28bc372741c45185eb4c987069165c3ad4b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ccb5f9cd9e367481380b8afbc6a763149967bd59f0bbe6d7f0c0693d90ccae7d0b6a16bf9e2544ef4362504cb417cb327f44001a044d8bbc4e318a1377b6ac45
|
7
|
+
data.tar.gz: 19b6761660d76db06b36114496fd9f835d08fdf70e99a7e117369d800175a79f18e21c6436f7f37a84529e3370bdae2f8285a49e25ae00182cc05f801c8daea9
|
data/README.md
CHANGED
@@ -1,16 +1,32 @@
|
|
1
1
|
# TrailGuide
|
2
2
|
|
3
|
-
|
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
|
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
|
-
###
|
41
|
+
### Configuration
|
26
42
|
|
27
|
-
|
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
|
-
|
30
|
-
# /config/routes.rb
|
45
|
+
Configure redis by either setting a `REDIS_URL` environment variable:
|
31
46
|
|
32
|
-
|
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
|
-
|
51
|
+
Or you can create a config initializer, which is useful if you plan on configuring TrailGuide further:
|
42
52
|
|
43
53
|
```ruby
|
44
|
-
#
|
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 :
|
71
|
-
variant :
|
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
|
-
|
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
|
83
|
-
when :
|
84
|
-
# perform logic for group "
|
85
|
-
when :
|
86
|
-
# perform logic for group "
|
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
|
-
|
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
|
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
|
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
|
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
|
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
|
-
|
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
|
-
|
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
|
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
|
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
|
-
#
|
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
|
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,
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
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
|
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
|
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
|
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
|
-
##
|
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 =
|
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
|
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
|
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
|
-
|
834
|
-
|
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
|
-
|
843
|
-
|
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
|
-
|
851
|
-
|
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
|
859
|
-
#
|
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**
|