trailguide 0.1.31 → 0.2.0
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 +73 -8
- data/app/assets/javascripts/trail_guide/admin/application.js +20 -1
- data/app/assets/stylesheets/trail_guide/admin/application.css +22 -0
- data/app/assets/stylesheets/trail_guide/admin/experiments.css +36 -3
- data/app/controllers/trail_guide/admin/application_controller.rb +59 -8
- data/app/controllers/trail_guide/admin/experiments_controller.rb +209 -16
- data/app/controllers/trail_guide/admin/groups_controller.rb +34 -0
- data/app/controllers/trail_guide/experiments_controller.rb +1 -1
- data/app/views/layouts/trail_guide/admin/_calculator.erb +24 -0
- data/app/views/layouts/trail_guide/admin/_footer.html.erb +27 -0
- data/app/views/layouts/trail_guide/admin/_header.html.erb +147 -0
- data/app/views/layouts/trail_guide/admin/_import_modal.html.erb +45 -0
- data/app/views/layouts/trail_guide/admin/application.html.erb +17 -3
- data/app/views/trail_guide/admin/experiments/_alert_peek.html.erb +19 -0
- data/app/views/trail_guide/admin/experiments/_alert_state.html.erb +49 -0
- data/app/views/trail_guide/admin/experiments/_btn_analyze.html.erb +11 -0
- data/app/views/trail_guide/admin/experiments/_btn_analyze_goal.html.erb +5 -0
- data/app/views/trail_guide/admin/experiments/_btn_convert.html.erb +33 -0
- data/app/views/trail_guide/admin/experiments/_btn_enroll.html.erb +3 -0
- data/app/views/trail_guide/admin/experiments/_btn_join.html.erb +11 -0
- data/app/views/trail_guide/admin/experiments/_btn_leave.html.erb +7 -0
- data/app/views/trail_guide/admin/experiments/_btn_pause.html.erb +5 -0
- data/app/views/trail_guide/admin/experiments/_btn_peek.html.erb +13 -0
- data/app/views/trail_guide/admin/experiments/_btn_reset.html.erb +5 -0
- data/app/views/trail_guide/admin/experiments/_btn_restart.html.erb +5 -0
- data/app/views/trail_guide/admin/experiments/_btn_resume.html.erb +5 -0
- data/app/views/trail_guide/admin/experiments/_btn_schedule.html.erb +6 -0
- data/app/views/trail_guide/admin/experiments/_btn_start.html.erb +5 -0
- data/app/views/trail_guide/admin/experiments/_btn_stop.html.erb +5 -0
- data/app/views/trail_guide/admin/experiments/_experiment.html.erb +68 -172
- data/app/views/trail_guide/admin/experiments/_header.html.erb +87 -0
- data/app/views/trail_guide/admin/experiments/_start_modal.html.erb +57 -0
- data/app/views/trail_guide/admin/experiments/_tbody.html.erb +112 -0
- data/app/views/trail_guide/admin/experiments/_thead.html.erb +38 -0
- data/app/views/trail_guide/admin/experiments/index.html.erb +8 -16
- data/app/views/trail_guide/admin/experiments/show.html.erb +78 -0
- data/app/views/trail_guide/admin/groups/index.html.erb +40 -0
- data/app/views/trail_guide/admin/groups/show.html.erb +9 -0
- data/app/views/trail_guide/admin/orphans/_alert.html.erb +44 -0
- data/config/initializers/trailguide.rb +72 -9
- data/config/routes/admin.rb +19 -12
- data/lib/trail_guide/admin/engine.rb +2 -0
- data/lib/trail_guide/admin.rb +2 -0
- data/lib/trail_guide/calculators/bayesian.rb +98 -0
- data/lib/trail_guide/calculators/calculator.rb +58 -0
- data/lib/trail_guide/calculators/score.rb +68 -0
- data/lib/trail_guide/calculators.rb +8 -0
- data/lib/trail_guide/catalog.rb +134 -19
- data/lib/trail_guide/combined_experiment.rb +8 -3
- data/lib/trail_guide/config.rb +6 -1
- data/lib/trail_guide/engine.rb +2 -0
- data/lib/trail_guide/errors.rb +30 -1
- data/lib/trail_guide/experiment.rb +0 -1
- data/lib/trail_guide/experiments/base.rb +189 -53
- data/lib/trail_guide/experiments/config.rb +82 -13
- data/lib/trail_guide/experiments/participant.rb +21 -1
- data/lib/trail_guide/helper.rb +59 -32
- data/lib/trail_guide/metrics/checkpoint.rb +24 -0
- data/lib/trail_guide/metrics/config.rb +45 -0
- data/lib/trail_guide/metrics/funnel.rb +24 -0
- data/lib/trail_guide/metrics/goal.rb +89 -0
- data/lib/trail_guide/metrics.rb +9 -0
- data/lib/trail_guide/participant.rb +54 -21
- data/lib/trail_guide/variant.rb +34 -12
- data/lib/trail_guide/version.rb +2 -2
- data/lib/trailguide.rb +4 -0
- metadata +112 -7
- data/app/views/layouts/trail_guide/admin/_footer.erb +0 -10
- data/app/views/layouts/trail_guide/admin/_header.erb +0 -42
- data/app/views/trail_guide/admin/experiments/_combined_experiment.html.erb +0 -189
- data/config/routes.rb +0 -5
@@ -0,0 +1,78 @@
|
|
1
|
+
<div class="experiment row justify-content-center">
|
2
|
+
<div class="col-sm-12 col-md-10 col-lg-8">
|
3
|
+
<%= render partial: "header", locals: { experiment: @experiment } %>
|
4
|
+
|
5
|
+
<% if @experiment.combined? %>
|
6
|
+
<table class="table table-hover">
|
7
|
+
<% combined_experiments = @experiment.combined.map { |e| TrailGuide.catalog.find(e) } %>
|
8
|
+
<% combined_experiments.each do |experiment| %>
|
9
|
+
<%= render partial: 'thead', locals: { experiment: experiment } %>
|
10
|
+
<%= render partial: 'tbody', locals: { experiment: experiment, calculator: experiment_calculator(experiment) } %>
|
11
|
+
<% end %>
|
12
|
+
|
13
|
+
<tfoot class="thead-light">
|
14
|
+
<tr>
|
15
|
+
<th class="btn-col"> </th>
|
16
|
+
<th scope="row"> </th>
|
17
|
+
<th>
|
18
|
+
<span><%= number_with_delimiter @experiment.participants %></span>
|
19
|
+
<% if @experiment.configuration.target_sample_size %>
|
20
|
+
<span class="text-muted">/ <%= number_with_delimiter @experiment.configuration.target_sample_size %></span>
|
21
|
+
<% end %>
|
22
|
+
</th>
|
23
|
+
<% if @experiment.goals.empty? %>
|
24
|
+
<th><%= experiment_metric @experiment, combined_experiments.sum(&:converted) %></th>
|
25
|
+
<% else %>
|
26
|
+
<% @experiment.goals.each do |goal| %>
|
27
|
+
<th><%= experiment_metric @experiment, combined_experiments.sum { |e| e.converted(goal) } %></th>
|
28
|
+
<% end %>
|
29
|
+
<% end %>
|
30
|
+
<th class="btn-col"> </th>
|
31
|
+
</tr>
|
32
|
+
</tfoot>
|
33
|
+
</table>
|
34
|
+
<% else %>
|
35
|
+
<table class="table table-hover">
|
36
|
+
<%= render partial: 'thead', locals: { experiment: @experiment } %>
|
37
|
+
<%= render partial: 'tbody', locals: { experiment: @experiment, calculator: experiment_calculator(@experiment) } %>
|
38
|
+
|
39
|
+
<tfoot class="thead-light">
|
40
|
+
<tr>
|
41
|
+
<th class="btn-col"> </th>
|
42
|
+
<th scope="row"> </th>
|
43
|
+
<th>
|
44
|
+
<span><%= number_with_delimiter @experiment.participants %></span>
|
45
|
+
<% if @experiment.configuration.target_sample_size %>
|
46
|
+
<span class="text-muted">/ <%= number_with_delimiter @experiment.configuration.target_sample_size %></span>
|
47
|
+
<% end %>
|
48
|
+
</th>
|
49
|
+
<% if @experiment.goals.empty? %>
|
50
|
+
<th><%= experiment_metric @experiment, @experiment.converted %></th>
|
51
|
+
<% else %>
|
52
|
+
<% @experiment.goals.each do |goal| %>
|
53
|
+
<th><%= experiment_metric @experiment, @experiment.converted(goal) %></th>
|
54
|
+
<% end %>
|
55
|
+
<% end %>
|
56
|
+
<th class="btn-col"> </th>
|
57
|
+
</tr>
|
58
|
+
</tfoot>
|
59
|
+
</table>
|
60
|
+
<% end %>
|
61
|
+
|
62
|
+
<div class="row" style="display: none;">
|
63
|
+
<div class="col-12">
|
64
|
+
<% if @analyzing && experiment_metrics_visible?(@experiment) %>
|
65
|
+
<div class="alert alert-primary">
|
66
|
+
<h6>
|
67
|
+
<span class="fas fa-chart-line"></span>
|
68
|
+
Analysis
|
69
|
+
</h6>
|
70
|
+
<p><small>TODO</small></p>
|
71
|
+
</div>
|
72
|
+
<% end %>
|
73
|
+
</div>
|
74
|
+
</div>
|
75
|
+
|
76
|
+
<%= render partial: 'alert_peek', locals: {experiment: @experiment} %>
|
77
|
+
</div>
|
78
|
+
</div>
|
@@ -0,0 +1,40 @@
|
|
1
|
+
<%= render partial: 'trail_guide/admin/orphans/alert' %>
|
2
|
+
|
3
|
+
<div class="row justify-content-center">
|
4
|
+
<div class="col-12 col-md-10 col-lg-8">
|
5
|
+
<div class="experiments">
|
6
|
+
<% @groups.each do |group| %>
|
7
|
+
<button href="<%= trail_guide_admin.group_path(group) %>" class="experiment btn btn-outline-secondary rounded-lg text-left">
|
8
|
+
<div class="row align-items-center">
|
9
|
+
<div class="col-12 col-md-8 col-lg-9 experiment-text">
|
10
|
+
<h4 id="<%= group %>">
|
11
|
+
<%= group.to_s.humanize.titleize %>
|
12
|
+
</h4>
|
13
|
+
</div>
|
14
|
+
<div class="col-12 col-md-4 col-lg-3 text-center text-md-right">
|
15
|
+
<div class="row align-items-center">
|
16
|
+
<div class="col-6 col-md-7 col-lg-8 experiment-text text-center">
|
17
|
+
<h4>
|
18
|
+
<span class="fas fa-flask"></span>
|
19
|
+
×
|
20
|
+
<%= TrailGuide.catalog.select(group).count %>
|
21
|
+
</h4>
|
22
|
+
</div>
|
23
|
+
|
24
|
+
<div class="col-6 col-md-5 col-lg-4 experiment-text text-center">
|
25
|
+
<h4>
|
26
|
+
<% if TrailGuide.catalog.select(group).all? { |e| e.goals.map(&:name).include?(group) } %>
|
27
|
+
<span class="fas fa-fill" data-toggle="tooltip" title="this group represents a shared conversion goal between multiple experiments"></span>
|
28
|
+
<% else %>
|
29
|
+
<span class="fas fa-th-large" data-toggle="tooltip" title="this group is not linked to any shared goals, and is purely organizational"></span>
|
30
|
+
<% end %>
|
31
|
+
</h4>
|
32
|
+
</div>
|
33
|
+
</div>
|
34
|
+
</div>
|
35
|
+
</div>
|
36
|
+
</button>
|
37
|
+
<% end %>
|
38
|
+
</div>
|
39
|
+
</div>
|
40
|
+
</div>
|
@@ -0,0 +1,9 @@
|
|
1
|
+
<div class="row justify-content-center">
|
2
|
+
<div class="col-12 col-md-10 col-lg-8">
|
3
|
+
<div class="experiments">
|
4
|
+
<% @experiments.each do |experiment| %>
|
5
|
+
<%= render 'trail_guide/admin/experiments/experiment', experiment: experiment %>
|
6
|
+
<% end %>
|
7
|
+
</div>
|
8
|
+
</div>
|
9
|
+
</div>
|
@@ -0,0 +1,44 @@
|
|
1
|
+
<% unless TrailGuide.configuration.ignore_orphaned_groups? || TrailGuide.catalog.orphans.empty? %>
|
2
|
+
<div class="row justify-content-center">
|
3
|
+
<div class="col-sm-12 col-md-10 col-lg-8">
|
4
|
+
<div class="alert alert-warning alert-dismissable">
|
5
|
+
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
6
|
+
<span aria-hidden="true">×</span>
|
7
|
+
</button>
|
8
|
+
|
9
|
+
<h5>
|
10
|
+
<span class="fas fa-ghost"></span>
|
11
|
+
|
12
|
+
Orphaned Groups
|
13
|
+
</h5>
|
14
|
+
|
15
|
+
<br />
|
16
|
+
<p>TrailGuide has detected <strong><%= TrailGuide.catalog.orphans.values.sum(&:count) %></strong> calls to <code>trailguide.convert</code> against <strong><%= TrailGuide.catalog.orphans.count %></strong> unknown group <%= "name".pluralize(TrailGuide.catalog.orphans.count) %>.</p>
|
17
|
+
|
18
|
+
<p><small>This usually means that the last experiment that belonged to a given group was removed without realizing it, and references to the group were overlooked. <strong>Don't worry, your users won't notice a thing</strong>, and you can find all the logged references below. You can find <%= link_to "more info here.", "https://github.com/markrebec/trailguide#orphaned-groups", target: :blank %></small></p>
|
19
|
+
|
20
|
+
<div class="accordion bg-transparent border-0" id="orphans">
|
21
|
+
<% TrailGuide.catalog.orphans.each do |orphan,traces| %>
|
22
|
+
<div class="card">
|
23
|
+
<button class="btn btn-outline-light text-left btn-block" type="button" data-toggle="collapse" data-target="#orphan-<%= orphan %>" aria-expanded="true" aria-controls="collapseOne">
|
24
|
+
<strong><code><%= orphan %></code></strong>
|
25
|
+
</button>
|
26
|
+
|
27
|
+
<div id="orphan-<%= orphan %>" class="collapse" aria-labelledby="orphan-btn-<%= orphan %>" data-parent="#orphans">
|
28
|
+
<div class="card-body">
|
29
|
+
<ul style="list-style: none;">
|
30
|
+
<% traces.each do |trace| %>
|
31
|
+
<li><code><%= trace %></code></li>
|
32
|
+
<% end %>
|
33
|
+
</ul>
|
34
|
+
</div>
|
35
|
+
</div>
|
36
|
+
</div>
|
37
|
+
<% end %>
|
38
|
+
</div>
|
39
|
+
|
40
|
+
<p><small>These warnings will expire after 15 minutes of inactivity, and they will disappear automatically once you remove the references listed above and are no longer being reactivated. If you prefer to clear these warnings immediately after cleaning up, you can run <code>TrailGuide.catalog.adopted(:group_name)</code> from a console, rake task, or background job.</small></p>
|
41
|
+
</div>
|
42
|
+
</div>
|
43
|
+
</div>
|
44
|
+
<% end %>
|
@@ -17,6 +17,17 @@ TrailGuide.configure do |config|
|
|
17
17
|
# false you'll need to include the helper module manually where you want
|
18
18
|
config.include_helpers = true
|
19
19
|
|
20
|
+
# whether or not to ignore orphaned group/experiment conversion calls in the
|
21
|
+
# admin - this can be useful if you intentionally leave calls to
|
22
|
+
# `trailguide.convert(:group_name)` at key points in your application, but
|
23
|
+
# also periodically add/remove experiments using those groups
|
24
|
+
#
|
25
|
+
# true will not track orphaned convert calls nor expose them in the admin
|
26
|
+
# interface
|
27
|
+
# false will track orphaned convert calls, and notify you via the admin
|
28
|
+
# interface so you can track them down and remove them as you'd like
|
29
|
+
config.ignore_orphaned_groups = false
|
30
|
+
|
20
31
|
# request param for overriding/previewing variants - allows previewing
|
21
32
|
# variants with request params
|
22
33
|
# i.e. example.com/somepage/?experiment[my_experiment]=option_b
|
@@ -56,11 +67,13 @@ TrailGuide.configure do |config|
|
|
56
67
|
# a couple milliseconds to participant initialization, but isn't strictly
|
57
68
|
# necessary, since expired enrollment will never affect future experiment
|
58
69
|
# participation - it might be good practice if you're using redis to store
|
59
|
-
# participant data without expiration
|
70
|
+
# participant data without expiration, or to avoid overflowing a client cookie
|
60
71
|
#
|
61
|
-
# true will clean up any old/inactive experiment keys for each
|
62
|
-
# participant the first time they're
|
72
|
+
# true will explicitly clean up any old/inactive experiment keys for each
|
73
|
+
# participant the first time they're initialized during a script
|
63
74
|
# execution (web request, etc.)
|
75
|
+
# :inline will only clean up any old/inactive experiment keys that are
|
76
|
+
# encountered when referencing a participant's active experiments
|
64
77
|
# false will skip the cleanup process entirely
|
65
78
|
config.cleanup_participant_experiments = true
|
66
79
|
|
@@ -125,6 +138,19 @@ TrailGuide::Experiment.configure do |config|
|
|
125
138
|
# the experiment if they encounter it again
|
126
139
|
config.reset_manually = true
|
127
140
|
|
141
|
+
# whether or not to store individual participation when returning a variant
|
142
|
+
#
|
143
|
+
# this can be useful if you are using a custom, content-based algorithm where
|
144
|
+
# the variant is determined by content rather than user bucketing, and you
|
145
|
+
# want to treat participation more like impressions (i.e. for seo experiments)
|
146
|
+
#
|
147
|
+
# true participation is incremented the first time a participant is
|
148
|
+
# enrolled, and the participant is assigned their selection for future
|
149
|
+
# reference, stored via the configured participant adapter
|
150
|
+
# false participation will be incremented every time a variant is returned,
|
151
|
+
# and the participant will not have their assignment stored
|
152
|
+
config.store_participation = true
|
153
|
+
|
128
154
|
# whether or not to enter participants into a variant when using the override
|
129
155
|
# parameter to preview variants
|
130
156
|
#
|
@@ -167,6 +193,21 @@ TrailGuide::Experiment.configure do |config|
|
|
167
193
|
# false prevents converting to multiple goals for a single participant
|
168
194
|
config.allow_multiple_goals = false
|
169
195
|
|
196
|
+
# whether or not to enable calibration before an experiment is started - the
|
197
|
+
# participants and conversions will be tracked for your control group while
|
198
|
+
# an experiment remains unstarted, which can be useful for gathering a
|
199
|
+
# baseline conversion rate if you don't already have one
|
200
|
+
#
|
201
|
+
# control is always returned for unstarted experiments by default, and this
|
202
|
+
# configuration only affects whether or not to track metrics
|
203
|
+
#
|
204
|
+
# this setting only applies when start_manually is also true
|
205
|
+
#
|
206
|
+
# true metrics for participation and conversion will be tracked for the
|
207
|
+
# control group until the experiment is started
|
208
|
+
# false metrics will not be tracked while the experiment remains unstarted
|
209
|
+
config.enable_calibration = false
|
210
|
+
|
170
211
|
# whether or not to skip the request filtering for this experiment - can be
|
171
212
|
# useful when defining content-based experiments with custom algorithms which
|
172
213
|
# bucket participants strictly based on additional content metadata and you
|
@@ -208,6 +249,18 @@ TrailGuide::Experiment.configure do |config|
|
|
208
249
|
#
|
209
250
|
# config.on_start = -> (experiment, context) { ... }
|
210
251
|
|
252
|
+
# callback on experiment schedule manually via UI/console, can be used for logging,
|
253
|
+
# tracking, etc.
|
254
|
+
#
|
255
|
+
# experiments can only be scheduled if config.start_manually is true
|
256
|
+
#
|
257
|
+
# context may or may not be present depending on how you're triggering the
|
258
|
+
# action - if you're using the admin, this will be the admin controller
|
259
|
+
# context, if you're in a console you have the option to pass a context to
|
260
|
+
# `experiment.schedule!` or not
|
261
|
+
#
|
262
|
+
# config.on_schedule = -> (experiment, start_at, stop_at, context) { ... }
|
263
|
+
|
211
264
|
# callback on experiment stop manually via UI/console, can be used for
|
212
265
|
# logging, tracking, etc.
|
213
266
|
#
|
@@ -272,26 +325,28 @@ TrailGuide::Experiment.configure do |config|
|
|
272
325
|
# callback when a participant is entered into a variant for the first time,
|
273
326
|
# can be used for logging, tracking, etc.
|
274
327
|
#
|
275
|
-
# config.on_choose = -> (experiment, variant, metadata) { ... }
|
328
|
+
# config.on_choose = -> (experiment, variant, participant, metadata) { ... }
|
276
329
|
|
277
330
|
# callback every time a participant is returned a variant in the experiment,
|
278
331
|
# can be used for logging, tracking, etc.
|
279
332
|
#
|
280
|
-
# config.on_use = -> (experiment, variant, metadata) { ... }
|
333
|
+
# config.on_use = -> (experiment, variant, participant, metadata) { ... }
|
281
334
|
|
282
335
|
# callback when a participant converts for a variant in the experiment, can be
|
283
336
|
# used for logging, tracking, etc.
|
284
337
|
#
|
285
|
-
# config.on_convert = -> (experiment, variant,
|
338
|
+
# config.on_convert = -> (experiment, checkpoint, variant, participant, metadata) { ... }
|
286
339
|
|
287
340
|
|
288
341
|
# callback that can short-circuit participation based on your own logic, which
|
289
342
|
# gets called *after* all the core engine checks (i.e. that the user is
|
290
343
|
# not excluded or already participating, etc.)
|
291
344
|
#
|
345
|
+
# `allowed` will be the value returned by any previous callbacks in the chain
|
346
|
+
#
|
292
347
|
# should return true or false
|
293
348
|
#
|
294
|
-
# config.allow_participation = -> (experiment, metadata) { ... return true }
|
349
|
+
# config.allow_participation = -> (experiment, allowed, participant, metadata) { ... return true }
|
295
350
|
|
296
351
|
|
297
352
|
# callback that can short-circuit conversion based on your own logic, which
|
@@ -299,9 +354,11 @@ TrailGuide::Experiment.configure do |config|
|
|
299
354
|
# participating in the experiment, is within the bounds of the experiment
|
300
355
|
# configuration for allow_multiple_*, etc.)
|
301
356
|
#
|
357
|
+
# `allowed` will be the value returned by any previous callbacks in the chain
|
358
|
+
#
|
302
359
|
# should return true or false
|
303
360
|
#
|
304
|
-
# config.allow_conversion = -> (experiment, checkpoint, metadata) { ... return true }
|
361
|
+
# config.allow_conversion = -> (experiment, allowed, checkpoint, variant, participant, metadata) { ... return true }
|
305
362
|
|
306
363
|
|
307
364
|
# callback that can be used to modify the rollout of a selected winner - for
|
@@ -309,9 +366,15 @@ TrailGuide::Experiment.configure do |config|
|
|
309
366
|
# gem to do a "feature rollout" from your control variant to your winner for
|
310
367
|
# all users
|
311
368
|
#
|
369
|
+
# be aware that when using this alongside track_winner_conversions, whatever
|
370
|
+
# variant is returned from this callback chain is what will be tracked for
|
371
|
+
# participation and conversion as long as the experiment is still running
|
372
|
+
#
|
373
|
+
# `winner` will be the variant returned by any previous callbacks in the chain
|
374
|
+
#
|
312
375
|
# must return an experiment variant
|
313
376
|
#
|
314
|
-
# config.rollout_winner = -> (experiment, winner) { ... return variant }
|
377
|
+
# config.rollout_winner = -> (experiment, winner, participant) { ... return variant }
|
315
378
|
end
|
316
379
|
|
317
380
|
# admin ui configuration
|
data/config/routes/admin.rb
CHANGED
@@ -1,22 +1,29 @@
|
|
1
1
|
TrailGuide::Admin::Engine.routes.draw do
|
2
|
-
resources :
|
2
|
+
resources :groups, only: [:index, :show]
|
3
|
+
|
4
|
+
resources :experiments, path: '/', only: [:index, :show] do
|
3
5
|
member do
|
4
|
-
match :start,
|
5
|
-
match :
|
6
|
-
match :
|
7
|
-
match :
|
8
|
-
match :
|
9
|
-
match :
|
6
|
+
match :start, via: [:put, :post, :get]
|
7
|
+
match :schedule, via: [:put, :post]
|
8
|
+
match :pause, via: [:put, :post, :get]
|
9
|
+
match :stop, via: [:put, :post, :get]
|
10
|
+
match :reset, via: [:put, :post, :get]
|
11
|
+
match :resume, via: [:put, :post, :get]
|
12
|
+
match :restart, via: [:put, :post, :get]
|
10
13
|
|
11
|
-
match :
|
12
|
-
match :
|
14
|
+
match :enroll, via: [:put, :post, :get], path: 'enroll'
|
15
|
+
match :join, via: [:put, :post, :get], path: 'join/:variant'
|
16
|
+
match :convert, via: [:put, :post, :get], path: 'convert/:goal'
|
17
|
+
match :leave, via: [:put, :post, :get]
|
13
18
|
|
14
|
-
match :winner,
|
15
|
-
match :clear,
|
19
|
+
match :winner, via: [:put, :post, :get], path: 'winner/:variant'
|
20
|
+
match :clear, via: [:put, :post, :get]
|
16
21
|
end
|
17
22
|
|
18
23
|
collection do
|
19
|
-
|
24
|
+
put '/import', action: :import, as: :import
|
25
|
+
get '/export', action: :index, as: :export, format: [:json]
|
26
|
+
get '/scope/:scope', action: :index, as: :scoped
|
20
27
|
end
|
21
28
|
end
|
22
29
|
end
|
data/lib/trail_guide/admin.rb
CHANGED
@@ -0,0 +1,98 @@
|
|
1
|
+
module TrailGuide
|
2
|
+
module Calculators
|
3
|
+
class Bayesian < Calculator
|
4
|
+
def self.enabled?
|
5
|
+
!!(defined?(::Integration) && (defined?(::Rubystats) || defined?(::Distribution)))
|
6
|
+
end
|
7
|
+
|
8
|
+
attr_reader :beta
|
9
|
+
|
10
|
+
def initialize(*args, beta: nil, **opts)
|
11
|
+
raise NoIntegrationLibrary if !defined?(::Integration)
|
12
|
+
|
13
|
+
if beta.nil?
|
14
|
+
# prefer rubystats if not specified
|
15
|
+
if defined?(::Rubystats)
|
16
|
+
beta = :rubystats
|
17
|
+
elsif defined?(::Distribution)
|
18
|
+
beta = :distribution
|
19
|
+
else
|
20
|
+
raise NoBetaDistributionLibrary
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
case beta.to_sym
|
25
|
+
when :distribution
|
26
|
+
raise NoBetaDistributionLibrary, beta unless defined?(::Distribution)
|
27
|
+
TrailGuide.logger.debug "Using Distribution::Beta to calculate beta distributions"
|
28
|
+
TrailGuide.logger.debug "GSL detected, Distribution::Beta will use GSL for better performance" if defined?(::GSL)
|
29
|
+
when :rubystats
|
30
|
+
raise NoBetaDistributionLibrary, beta unless defined?(::Rubystats)
|
31
|
+
TrailGuide.logger.debug "Using Rubystats::BetaDistribution to calculate beta distributions"
|
32
|
+
else
|
33
|
+
raise UnknownBetaDistributionLibrary, beta
|
34
|
+
end
|
35
|
+
|
36
|
+
super(*args, **opts)
|
37
|
+
@beta = beta.to_sym
|
38
|
+
end
|
39
|
+
|
40
|
+
def pdf(variant, z)
|
41
|
+
x = variant.subset
|
42
|
+
n = variant.superset
|
43
|
+
if beta == :distribution
|
44
|
+
Distribution::Beta.pdf(z, x+1, n-x+1)
|
45
|
+
else
|
46
|
+
Rubystats::BetaDistribution.new(x+1, n-x+1).pdf(z)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def cdf(variant, z)
|
51
|
+
x = variant.subset
|
52
|
+
n = variant.superset
|
53
|
+
if beta == :distribution
|
54
|
+
Distribution::Beta.cdf(z, x+1, n-x+1)
|
55
|
+
else
|
56
|
+
Rubystats::BetaDistribution.new(x+1, n-x+1).cdf(z)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def variant_probability(variant)
|
61
|
+
Integration.integrate(0, 1, tolerance: 1e-4) do |z|
|
62
|
+
vpdf = pdf(variant, z)
|
63
|
+
variants.each do |var|
|
64
|
+
next if var == variant
|
65
|
+
vpdf = vpdf * cdf(var, z)
|
66
|
+
end
|
67
|
+
vpdf
|
68
|
+
end * 100.0
|
69
|
+
end
|
70
|
+
|
71
|
+
def calculate!
|
72
|
+
variants_with_conversion.each do |variant|
|
73
|
+
expvar = experiment.variants.find { |var| var.name == variant.name }
|
74
|
+
vprob = variant_probability(variant)
|
75
|
+
variant.probability = vprob
|
76
|
+
variant.significance = TrailGuide::Calculators::SIGNIFICANT_PROBABILITIES.reverse.find { |pct| vprob >= pct } || 0
|
77
|
+
|
78
|
+
#if worst && variant.measure > worst.measure
|
79
|
+
# variant.difference = (variant.measure - worst.measure) / worst.measure * 100
|
80
|
+
#end
|
81
|
+
if base
|
82
|
+
if variant.measure > base.measure
|
83
|
+
variant.difference = (variant.measure - base.measure) / base.measure * 100
|
84
|
+
elsif base.measure > variant.measure
|
85
|
+
variant.difference = -((base.measure - variant.measure) / base.measure * 100)
|
86
|
+
else
|
87
|
+
variant.difference = 0
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
@choice = best && best.probability >= probability ? best : nil
|
93
|
+
|
94
|
+
self
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module TrailGuide
|
2
|
+
module Calculators
|
3
|
+
SIGNIFICANT_PROBABILITIES = [10, 50, 75, 90, 95, 99, 99.9]
|
4
|
+
DEFAULT_PROBABILITY = 90
|
5
|
+
|
6
|
+
class Calculator
|
7
|
+
attr_reader :experiment, :goal, :probability, :choice
|
8
|
+
|
9
|
+
def initialize(experiment, probability=TrailGuide::Calculators::DEFAULT_PROBABILITY, base: :default, goal: nil, against: nil)
|
10
|
+
@experiment = experiment
|
11
|
+
@probability = probability
|
12
|
+
@base_type = base
|
13
|
+
@goal = goal
|
14
|
+
@against = against
|
15
|
+
end
|
16
|
+
|
17
|
+
def variants
|
18
|
+
@variants ||= experiment.variants.map do |variant|
|
19
|
+
superset = @against ? variant.converted(@against) : variant.participants
|
20
|
+
converts = variant.converted(goal)
|
21
|
+
measure = (converts.to_f / superset.to_f) rescue 0
|
22
|
+
measure = 0 if measure.nan?
|
23
|
+
|
24
|
+
Struct.new(:name, :control, :superset, :subset, :measure,
|
25
|
+
:difference, :probability, :significance, :z_score)
|
26
|
+
.new(variant.name, variant.control?, superset, converts, measure, 0, 0, nil, nil)
|
27
|
+
end.sort_by { |v| v.measure }
|
28
|
+
end
|
29
|
+
|
30
|
+
def variants_with_conversion
|
31
|
+
@variants_with_conversion ||= variants.select { |variant| variant.measure > 0.0 }
|
32
|
+
end
|
33
|
+
|
34
|
+
def base
|
35
|
+
@base ||= case @base_type
|
36
|
+
when :control
|
37
|
+
# use the control as the "base"
|
38
|
+
variants.find { |variant| variant.control }
|
39
|
+
else
|
40
|
+
# use the second-best converting as the "base" (default behavior)
|
41
|
+
variants[-2]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def best
|
46
|
+
@best ||= variants_with_conversion.last
|
47
|
+
end
|
48
|
+
|
49
|
+
def worst
|
50
|
+
@worst ||= variants_with_conversion.first
|
51
|
+
end
|
52
|
+
|
53
|
+
def calculate!
|
54
|
+
raise NotImplementedError, "You must define a calculate! method on your calculator class"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module TrailGuide
|
2
|
+
module Calculators
|
3
|
+
class Score < Calculator
|
4
|
+
# array of [z-score, percentage]
|
5
|
+
def self.z_score_probabilities
|
6
|
+
@@z_score_probabilities ||= begin
|
7
|
+
avg = 50.0
|
8
|
+
norm_dist = []
|
9
|
+
(0.0..3.1).step(0.01) { |x| norm_dist << [x, avg += 1 / Math.sqrt(2 * Math::PI) * Math::E ** (-x ** 2 / 2)] }
|
10
|
+
norm_dist
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.all_probabilities
|
15
|
+
@@all_probabilities ||= (0.0..100.0).step(0.1).map { |pct| [z_score_probabilities.find { |x,a| a >= pct }.first, pct] }.reverse
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.significant_probabilities
|
19
|
+
@@significant_probabilities ||= TrailGuide::Calculators::SIGNIFICANT_PROBABILITIES.map { |pct| [z_score_probabilities.find { |x,a| a >= pct }.first, pct] }.reverse
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.z_score_probability(score)
|
23
|
+
score = score.abs
|
24
|
+
probability = all_probabilities.find { |z,p| score >= z }
|
25
|
+
probability ? probability.last : 0
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.significant_probability(score)
|
29
|
+
score = score.abs
|
30
|
+
probability = significant_probabilities.find { |z,p| score >= z }
|
31
|
+
probability ? probability.last : 0
|
32
|
+
end
|
33
|
+
|
34
|
+
def calculate!
|
35
|
+
pc = base.measure
|
36
|
+
nc = base.superset
|
37
|
+
variants_with_conversion.each do |var|
|
38
|
+
p = var.measure
|
39
|
+
n = var.superset
|
40
|
+
z_score = (p - pc) / ((p * (1-p)/n) + (pc * (1-pc)/nc)).abs ** 0.5
|
41
|
+
|
42
|
+
var.z_score = z_score
|
43
|
+
var.probability = self.class.z_score_probability(z_score)
|
44
|
+
var.probability = -(var.probability) if var.z_score.negative?
|
45
|
+
var.significance = self.class.significant_probability(z_score)
|
46
|
+
var.significance = -(var.significance) if var.z_score.negative?
|
47
|
+
|
48
|
+
#if worst && var.measure > worst.measure
|
49
|
+
# var.difference = (var.measure - worst.measure) / worst.measure * 100
|
50
|
+
#end
|
51
|
+
if base
|
52
|
+
if var.measure > base.measure
|
53
|
+
var.difference = (var.measure - base.measure) / base.measure * 100
|
54
|
+
elsif base.measure > var.measure
|
55
|
+
var.difference = -((base.measure - var.measure) / base.measure * 100)
|
56
|
+
else
|
57
|
+
var.difference = 0
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
@choice = best && best.probability >= probability ? best : nil
|
63
|
+
|
64
|
+
self
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|