trailguide 0.1.30 → 0.1.31
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +676 -23
- data/app/controllers/trail_guide/admin/application_controller.rb +3 -3
- data/app/controllers/trail_guide/admin/experiments_controller.rb +21 -5
- data/app/views/layouts/trail_guide/admin/_header.erb +31 -3
- data/app/views/trail_guide/admin/experiments/_combined_experiment.html.erb +29 -7
- data/app/views/trail_guide/admin/experiments/_experiment.html.erb +29 -7
- data/app/views/trail_guide/admin/experiments/index.html.erb +1 -1
- data/config/initializers/trailguide.rb +33 -3
- data/config/routes/admin.rb +22 -0
- data/config/routes/engine.rb +13 -0
- data/config/routes.rb +5 -31
- data/lib/trail_guide/adapters/participants/multi.rb +2 -1
- data/lib/trail_guide/adapters/participants/redis.rb +4 -1
- data/lib/trail_guide/catalog.rb +12 -0
- data/lib/trail_guide/combined_experiment.rb +9 -1
- data/lib/trail_guide/config.rb +4 -4
- data/lib/trail_guide/experiments/base.rb +21 -5
- data/lib/trail_guide/experiments/config.rb +13 -4
- data/lib/trail_guide/helper.rb +48 -0
- data/lib/trail_guide/version.rb +1 -1
- data/lib/trailguide.rb +1 -1
- metadata +4 -2
@@ -18,7 +18,7 @@ module TrailGuide
|
|
18
18
|
|
19
19
|
def experiment_peekable?(experiment)
|
20
20
|
return false unless TrailGuide::Admin.configuration.peek_parameter
|
21
|
-
return false unless experiment.
|
21
|
+
return false unless experiment.started? && !experiment.stopped?
|
22
22
|
return false if experiment.target_sample_size_reached?
|
23
23
|
return true
|
24
24
|
end
|
@@ -30,7 +30,7 @@ module TrailGuide
|
|
30
30
|
helper_method :experiment_peeking?
|
31
31
|
|
32
32
|
def experiment_metrics_visible?(experiment)
|
33
|
-
return true unless experiment.
|
33
|
+
return true unless experiment.started? && !experiment.stopped?
|
34
34
|
return true if params[TrailGuide::Admin.configuration.peek_parameter] == experiment.experiment_name.to_s
|
35
35
|
return true if experiment.target_sample_size_reached?
|
36
36
|
return false
|
@@ -38,7 +38,7 @@ module TrailGuide
|
|
38
38
|
helper_method :experiment_metrics_visible?
|
39
39
|
|
40
40
|
def experiment_metric(experiment, metric)
|
41
|
-
return metric if experiment_metrics_visible?(experiment)
|
41
|
+
return helpers.number_with_delimiter(metric) if experiment_metrics_visible?(experiment)
|
42
42
|
return helpers.content_tag('span', nil, class: 'fas fa-eye-slash', data: {toggle: 'tooltip'}, title: "metrics are hidden until this experiment reaches it's target sample size")
|
43
43
|
end
|
44
44
|
helper_method :experiment_metric
|
@@ -5,32 +5,42 @@ module TrailGuide
|
|
5
5
|
(redirect_to :back rescue redirect_to trail_guide_admin.experiments_path) and return unless experiment.present?
|
6
6
|
end
|
7
7
|
|
8
|
+
before_action :experiments, only: [:index]
|
9
|
+
|
8
10
|
def index
|
9
11
|
end
|
10
12
|
|
11
13
|
def start
|
12
14
|
experiment.start!(self)
|
13
|
-
redirect_to trail_guide_admin.
|
15
|
+
redirect_to trail_guide_admin.scoped_experiments_path(scope: :running, anchor: experiment.experiment_name)
|
16
|
+
end
|
17
|
+
|
18
|
+
def pause
|
19
|
+
experiment.pause!(self)
|
20
|
+
redirect_to trail_guide_admin.scoped_experiments_path(scope: :paused, anchor: experiment.experiment_name)
|
14
21
|
end
|
15
22
|
|
16
23
|
def stop
|
17
24
|
experiment.stop!(self)
|
18
|
-
redirect_to trail_guide_admin.
|
25
|
+
redirect_to trail_guide_admin.scoped_experiments_path(scope: :stopped, anchor: experiment.experiment_name)
|
19
26
|
end
|
20
27
|
|
21
28
|
def reset
|
29
|
+
experiment.stop!(self)
|
22
30
|
experiment.reset!(self)
|
23
31
|
redirect_to trail_guide_admin.experiments_path(anchor: experiment.experiment_name)
|
24
32
|
end
|
25
33
|
|
26
34
|
def resume
|
27
35
|
experiment.resume!(self)
|
28
|
-
redirect_to trail_guide_admin.
|
36
|
+
redirect_to trail_guide_admin.scoped_experiments_path(scope: :running, anchor: experiment.experiment_name)
|
29
37
|
end
|
30
38
|
|
31
39
|
def restart
|
32
|
-
experiment.
|
33
|
-
|
40
|
+
experiment.stop!(self)
|
41
|
+
experiment.reset!(self)
|
42
|
+
experiment.start!(self)
|
43
|
+
redirect_to trail_guide_admin.scoped_experiments_path(scope: :running, anchor: experiment.experiment_name)
|
34
44
|
end
|
35
45
|
|
36
46
|
def join
|
@@ -58,6 +68,12 @@ module TrailGuide
|
|
58
68
|
|
59
69
|
private
|
60
70
|
|
71
|
+
def experiments
|
72
|
+
@experiments = TrailGuide.catalog
|
73
|
+
@experiments = @experiments.send(params[:scope]) if params[:scope].present?
|
74
|
+
@experiments = @experiments.by_started
|
75
|
+
end
|
76
|
+
|
61
77
|
def experiment
|
62
78
|
@experiment ||= TrailGuide.catalog.find(params[:id])
|
63
79
|
end
|
@@ -4,11 +4,39 @@
|
|
4
4
|
<%= TrailGuide::Admin.configuration.title %>
|
5
5
|
<% end %>
|
6
6
|
<div class="col-sm text-center">
|
7
|
-
|
7
|
+
<%= link_to trail_guide_admin.experiments_path, class: "text-dark" do %>
|
8
|
+
<strong class="total">
|
9
|
+
<%= TrailGuide.catalog.count %>
|
10
|
+
</strong>
|
11
|
+
<span>experiments</span>
|
12
|
+
<% end %>
|
13
|
+
|
8
14
|
<span>/</span>
|
9
|
-
|
15
|
+
|
16
|
+
<%= link_to trail_guide_admin.scoped_experiments_path(scope: :running), class: "text-dark" do %>
|
17
|
+
<strong class="total">
|
18
|
+
<%= TrailGuide.catalog.running.count %>
|
19
|
+
</strong>
|
20
|
+
<span>running</span>
|
21
|
+
<% end %>
|
22
|
+
|
10
23
|
<span>/</span>
|
11
|
-
|
24
|
+
|
25
|
+
<%= link_to trail_guide_admin.scoped_experiments_path(scope: :paused), class: "text-dark" do %>
|
26
|
+
<strong class="total">
|
27
|
+
<%= TrailGuide.catalog.paused.count %>
|
28
|
+
</strong>
|
29
|
+
<span>paused</span>
|
30
|
+
<% end %>
|
31
|
+
|
32
|
+
<span>/</span>
|
33
|
+
|
34
|
+
<%= link_to trail_guide_admin.scoped_experiments_path(scope: :stopped), class: "text-dark" do %>
|
35
|
+
<strong class="total">
|
36
|
+
<%= TrailGuide.catalog.stopped.count %>
|
37
|
+
</strong>
|
38
|
+
<span>stopped</span>
|
39
|
+
<% end %>
|
12
40
|
</div>
|
13
41
|
<span class="navbar-brand"><small class="text-muted"><%= TrailGuide::Admin.configuration.subtitle %></small></span>
|
14
42
|
</nav>
|
@@ -19,17 +19,37 @@
|
|
19
19
|
<% end %>
|
20
20
|
<% end %>
|
21
21
|
<% end %>
|
22
|
-
|
23
|
-
|
22
|
+
|
23
|
+
<% if combined_experiment.configuration.can_resume? %>
|
24
|
+
<%= link_to trail_guide_admin.pause_experiment_path(combined_experiment.experiment_name), class: 'btn btn-sm btn-warning', method: :put, data: {toggle: :tooltip}, title: 'pause this experiment - you will have the option to resume or reset' do %>
|
25
|
+
<span class="fas fa-pause" />
|
26
|
+
<% end %>
|
27
|
+
<% end %>
|
28
|
+
|
29
|
+
<%= link_to trail_guide_admin.stop_experiment_path(combined_experiment.experiment_name), class: 'btn btn-sm btn-danger', method: :put, data: {toggle: :tooltip}, title: 'stop this experiment - once stopped you will need to reset before restarting' do %>
|
30
|
+
<span class="fas fa-stop" />
|
24
31
|
<% end %>
|
32
|
+
|
25
33
|
<%= link_to trail_guide_admin.restart_experiment_path(combined_experiment.experiment_name), class: 'btn btn-sm btn-danger',method: :put, data: {toggle: :tooltip}, title: 'restart this experiment - will reset all data and restart the experiment' do %>
|
26
34
|
<span class="fas fa-redo" />
|
27
35
|
<% end %>
|
28
|
-
<% elsif combined_experiment.
|
29
|
-
|
36
|
+
<% elsif combined_experiment.paused? %>
|
37
|
+
<% if combined_experiment.configuration.can_resume? %>
|
38
|
+
<%= link_to trail_guide_admin.resume_experiment_path(combined_experiment.experiment_name), class: 'btn btn-sm btn-success',method: :put, data: {toggle: :tooltip}, title: 'resume this experiment to start bucketing users and serving variants again' do %>
|
39
|
+
<span class="fas fa-redo" />
|
40
|
+
<% end %>
|
41
|
+
<%= link_to trail_guide_admin.stop_experiment_path(combined_experiment.experiment_name), class: 'btn btn-sm btn-danger', method: :put, data: {toggle: :tooltip}, title: 'stop this experiment - once stopped you will need to reset before restarting' do %>
|
42
|
+
<span class="fas fa-stop" />
|
43
|
+
<% end %>
|
44
|
+
<% end %>
|
45
|
+
<%= link_to trail_guide_admin.restart_experiment_path(combined_experiment.experiment_name), class: 'btn btn-sm btn-danger',method: :put, data: {toggle: :tooltip}, title: 'restart this experiment - will reset all data and restart the experiment' do %>
|
30
46
|
<span class="fas fa-redo" />
|
31
47
|
<% end %>
|
32
|
-
<%
|
48
|
+
<% elsif combined_experiment.stopped? %>
|
49
|
+
<%= link_to trail_guide_admin.restart_experiment_path(combined_experiment.experiment_name), class: 'btn btn-sm btn-danger',method: :put, data: {toggle: :tooltip}, title: 'restart this experiment - will reset all data and restart the experiment' do %>
|
50
|
+
<span class="fas fa-redo" />
|
51
|
+
<% end %>
|
52
|
+
<% elsif !combined_experiment.started? %>
|
33
53
|
<%= link_to trail_guide_admin.start_experiment_path(combined_experiment.experiment_name), class: 'btn btn-sm btn-success',method: :put, data: {toggle: :tooltip}, title: 'start this experiment' do %>
|
34
54
|
<span class="fas fa-play" />
|
35
55
|
<% end %>
|
@@ -50,6 +70,8 @@
|
|
50
70
|
<span class="text-muted">—</span>
|
51
71
|
<% if combined_experiment.stopped? %>
|
52
72
|
<small class="text-muted"><%= combined_experiment.stopped_at.strftime('%b %e %Y @ %l:%M %p') %></small>
|
73
|
+
<% elsif combined_experiment.paused? %>
|
74
|
+
<small class="text-muted">paused</small>
|
53
75
|
<% else %>
|
54
76
|
<small class="text-muted">running</small>
|
55
77
|
<% end %>
|
@@ -147,9 +169,9 @@
|
|
147
169
|
<tr>
|
148
170
|
<th scope="row"> </th>
|
149
171
|
<th>
|
150
|
-
<span><%= combined_experiment.participants %></span>
|
172
|
+
<span><%= number_with_delimiter combined_experiment.participants %></span>
|
151
173
|
<% if combined_experiment.configuration.target_sample_size %>
|
152
|
-
<span class="text-muted">/ <%= combined_experiment.configuration.target_sample_size %></span>
|
174
|
+
<span class="text-muted">/ <%= number_with_delimiter combined_experiment.configuration.target_sample_size %></span>
|
153
175
|
<% end %>
|
154
176
|
</th>
|
155
177
|
<% if combined_experiment.goals.empty? %>
|
@@ -19,17 +19,37 @@
|
|
19
19
|
<% end %>
|
20
20
|
<% end %>
|
21
21
|
<% end %>
|
22
|
-
|
23
|
-
|
22
|
+
|
23
|
+
<% if experiment.configuration.can_resume? %>
|
24
|
+
<%= link_to trail_guide_admin.pause_experiment_path(experiment.experiment_name), class: 'btn btn-sm btn-warning', method: :put, data: {toggle: :tooltip}, title: 'pause this experiment - you will have the option to resume or reset' do %>
|
25
|
+
<span class="fas fa-pause" />
|
26
|
+
<% end %>
|
27
|
+
<% end %>
|
28
|
+
|
29
|
+
<%= link_to trail_guide_admin.stop_experiment_path(experiment.experiment_name), class: 'btn btn-sm btn-danger', method: :put, data: {toggle: :tooltip}, title: 'stop this experiment - once stopped you will need to reset before restarting' do %>
|
30
|
+
<span class="fas fa-stop" />
|
24
31
|
<% end %>
|
32
|
+
|
25
33
|
<%= link_to trail_guide_admin.restart_experiment_path(experiment.experiment_name), class: 'btn btn-sm btn-danger',method: :put, data: {toggle: :tooltip}, title: 'restart this experiment - will reset all data and restart the experiment' do %>
|
26
34
|
<span class="fas fa-redo" />
|
27
35
|
<% end %>
|
28
|
-
<% elsif experiment.
|
29
|
-
|
36
|
+
<% elsif experiment.paused? %>
|
37
|
+
<% if experiment.configuration.can_resume? %>
|
38
|
+
<%= link_to trail_guide_admin.resume_experiment_path(experiment.experiment_name), class: 'btn btn-sm btn-success',method: :put, data: {toggle: :tooltip}, title: 'resume this experiment to start bucketing users and serving variants again' do %>
|
39
|
+
<span class="fas fa-redo" />
|
40
|
+
<% end %>
|
41
|
+
<%= link_to trail_guide_admin.stop_experiment_path(experiment.experiment_name), class: 'btn btn-sm btn-danger', method: :put, data: {toggle: :tooltip}, title: 'stop this experiment - once stopped you will need to reset before restarting' do %>
|
42
|
+
<span class="fas fa-stop" />
|
43
|
+
<% end %>
|
44
|
+
<% end %>
|
45
|
+
<%= link_to trail_guide_admin.restart_experiment_path(experiment.experiment_name), class: 'btn btn-sm btn-danger',method: :put, data: {toggle: :tooltip}, title: 'restart this experiment - will reset all data and restart the experiment' do %>
|
30
46
|
<span class="fas fa-redo" />
|
31
47
|
<% end %>
|
32
|
-
<%
|
48
|
+
<% elsif experiment.stopped? %>
|
49
|
+
<%= link_to trail_guide_admin.restart_experiment_path(experiment.experiment_name), class: 'btn btn-sm btn-danger',method: :put, data: {toggle: :tooltip}, title: 'restart this experiment - will reset all data and restart the experiment' do %>
|
50
|
+
<span class="fas fa-redo" />
|
51
|
+
<% end %>
|
52
|
+
<% elsif !experiment.started? %>
|
33
53
|
<%= link_to trail_guide_admin.start_experiment_path(experiment.experiment_name), class: 'btn btn-sm btn-success',method: :put, data: {toggle: :tooltip}, title: 'start this experiment' do %>
|
34
54
|
<span class="fas fa-play" />
|
35
55
|
<% end %>
|
@@ -50,6 +70,8 @@
|
|
50
70
|
<span class="text-muted">—</span>
|
51
71
|
<% if experiment.stopped? %>
|
52
72
|
<small class="text-muted"><%= experiment.stopped_at.strftime('%b %e %Y @ %l:%M %p') %></small>
|
73
|
+
<% elsif experiment.paused? %>
|
74
|
+
<small class="text-muted">paused</small>
|
53
75
|
<% else %>
|
54
76
|
<small class="text-muted">running</small>
|
55
77
|
<% end %>
|
@@ -140,9 +162,9 @@
|
|
140
162
|
<tr>
|
141
163
|
<th scope="row"> </th>
|
142
164
|
<th>
|
143
|
-
<span><%= experiment.participants %></span>
|
165
|
+
<span><%= number_with_delimiter experiment.participants %></span>
|
144
166
|
<% if experiment.configuration.target_sample_size %>
|
145
|
-
<span class="text-muted">/ <%= experiment.configuration.target_sample_size %></span>
|
167
|
+
<span class="text-muted">/ <%= number_with_delimiter experiment.configuration.target_sample_size %></span>
|
146
168
|
<% end %>
|
147
169
|
</th>
|
148
170
|
<% if experiment.goals.empty? %>
|
@@ -9,7 +9,7 @@
|
|
9
9
|
<% end %>
|
10
10
|
|
11
11
|
<div class="experiments">
|
12
|
-
<%
|
12
|
+
<% @experiments.each do |experiment| %>
|
13
13
|
<% if experiment.combined? %>
|
14
14
|
<%= render 'combined_experiment', combined_experiment: experiment %>
|
15
15
|
<% else %>
|
@@ -1,6 +1,9 @@
|
|
1
1
|
# top-level trailguide rails engine configuration
|
2
2
|
#
|
3
3
|
TrailGuide.configure do |config|
|
4
|
+
# logger object
|
5
|
+
config.logger = Rails.logger
|
6
|
+
|
4
7
|
# url string or initialized Redis object
|
5
8
|
config.redis = ENV['REDIS_URL']
|
6
9
|
|
@@ -73,7 +76,7 @@ TrailGuide.configure do |config|
|
|
73
76
|
# callback when your participant adapter fails to initialize, and trailguide
|
74
77
|
# falls back to the anonymous adapter
|
75
78
|
config.on_adapter_failover = -> (adapter, error) do
|
76
|
-
|
79
|
+
TrailGuide.logger.error error
|
77
80
|
end
|
78
81
|
|
79
82
|
# list of user agents used by the default request filter proc below when
|
@@ -175,6 +178,13 @@ TrailGuide::Experiment.configure do |config|
|
|
175
178
|
# false default behavior, requests will be filtered based on your config
|
176
179
|
config.skip_request_filter = false
|
177
180
|
|
181
|
+
# whether or not this experiment can be resumed after it's stopped - allows
|
182
|
+
# temporarily "pausing" an experiment then resuming it without losing metrics
|
183
|
+
#
|
184
|
+
# true this experiment can be paused and resumed
|
185
|
+
# false this experiment can only be stopped and reset/restarted
|
186
|
+
config.can_resume = false
|
187
|
+
|
178
188
|
# set a default target sample size for all experiments - this will prevent
|
179
189
|
# metrics and stats from being displayed in the admin UI until the sample size
|
180
190
|
# is reached or the experiment is stopped
|
@@ -184,7 +194,7 @@ TrailGuide::Experiment.configure do |config|
|
|
184
194
|
# callback when connecting to redis fails and trailguide falls back to always
|
185
195
|
# returning control variants
|
186
196
|
config.on_redis_failover = -> (experiment, error) do
|
187
|
-
|
197
|
+
TrailGuide.logger.error error
|
188
198
|
end
|
189
199
|
|
190
200
|
# callback on experiment start, either manually via UI/console or
|
@@ -208,6 +218,16 @@ TrailGuide::Experiment.configure do |config|
|
|
208
218
|
#
|
209
219
|
# config.on_stop = -> (experiment, context) { ... }
|
210
220
|
|
221
|
+
# callback on experiment pause manually via UI/console, can be used for
|
222
|
+
# logging, tracking, etc.
|
223
|
+
#
|
224
|
+
# context may or may not be present depending on how you're triggering the
|
225
|
+
# action - if you're using the admin, this will be the admin controller
|
226
|
+
# context, if you're in a console you have the option to pass a context to
|
227
|
+
# `experiment.pause!` or not
|
228
|
+
#
|
229
|
+
# config.on_pause = -> (experiment, context) { ... }
|
230
|
+
|
211
231
|
# callback on experiment resume manually via UI/console, can be used for
|
212
232
|
# logging, tracking, etc.
|
213
233
|
#
|
@@ -218,8 +238,18 @@ TrailGuide::Experiment.configure do |config|
|
|
218
238
|
#
|
219
239
|
# config.on_resume = -> (experiment, context) { ... }
|
220
240
|
|
241
|
+
# callback on experiment delete manually via UI/console, can be used for
|
242
|
+
# logging, tracking, etc. - will also be triggered by a reset
|
243
|
+
#
|
244
|
+
# context may or may not be present depending on how you're triggering the
|
245
|
+
# action - if you're using the admin, this will be the admin controller
|
246
|
+
# context, if you're in a console you have the option to pass a context to
|
247
|
+
# `experiment.delete!` or not
|
248
|
+
#
|
249
|
+
# config.on_delete = -> (experiment, context) { ... }
|
250
|
+
|
221
251
|
# callback on experiment reset manually via UI/console, can be used for
|
222
|
-
# logging, tracking, etc.
|
252
|
+
# logging, tracking, etc. - will also trigger any on_delete callbacks
|
223
253
|
#
|
224
254
|
# context may or may not be present depending on how you're triggering the
|
225
255
|
# action - if you're using the admin, this will be the admin controller
|
@@ -0,0 +1,22 @@
|
|
1
|
+
TrailGuide::Admin::Engine.routes.draw do
|
2
|
+
resources :experiments, path: '/', only: [:index] do
|
3
|
+
member do
|
4
|
+
match :start, via: [:put, :post, :get]
|
5
|
+
match :pause, via: [:put, :post, :get]
|
6
|
+
match :stop, via: [:put, :post, :get]
|
7
|
+
match :reset, via: [:put, :post, :get]
|
8
|
+
match :resume, via: [:put, :post, :get]
|
9
|
+
match :restart, via: [:put, :post, :get]
|
10
|
+
|
11
|
+
match :join, via: [:put, :post, :get], path: 'join/:variant'
|
12
|
+
match :leave, via: [:put, :post, :get]
|
13
|
+
|
14
|
+
match :winner, via: [:put, :post, :get], path: 'winner/:variant'
|
15
|
+
match :clear, via: [:put, :post, :get]
|
16
|
+
end
|
17
|
+
|
18
|
+
collection do
|
19
|
+
get '/:scope', action: :index, as: :scoped
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
TrailGuide::Engine.routes.draw do
|
2
|
+
get '/' => 'experiments#index',
|
3
|
+
defaults: { format: :json }
|
4
|
+
match '/:experiment_name' => 'experiments#choose',
|
5
|
+
defaults: { format: :json },
|
6
|
+
via: [:get, :post]
|
7
|
+
match '/:experiment_name' => 'experiments#convert',
|
8
|
+
defaults: { format: :json },
|
9
|
+
via: [:put]
|
10
|
+
match '/:experiment_name/:checkpoint' => 'experiments#convert',
|
11
|
+
defaults: { format: :json },
|
12
|
+
via: [:put]
|
13
|
+
end
|
data/config/routes.rb
CHANGED
@@ -1,31 +1,5 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
via: [:get, :post]
|
7
|
-
match '/:experiment_name' => 'experiments#convert',
|
8
|
-
defaults: { format: :json },
|
9
|
-
via: [:put]
|
10
|
-
match '/:experiment_name/:checkpoint' => 'experiments#convert',
|
11
|
-
defaults: { format: :json },
|
12
|
-
via: [:put]
|
13
|
-
end
|
14
|
-
|
15
|
-
TrailGuide::Admin::Engine.routes.draw do
|
16
|
-
resources :experiments, path: '/', only: [:index] do
|
17
|
-
member do
|
18
|
-
match :start, via: [:put, :post, :get]
|
19
|
-
match :stop, via: [:put, :post, :get]
|
20
|
-
match :reset, via: [:put, :post, :get]
|
21
|
-
match :resume, via: [:put, :post, :get]
|
22
|
-
match :restart, via: [:put, :post, :get]
|
23
|
-
|
24
|
-
match :join, via: [:put, :post, :get], path: 'join/:variant'
|
25
|
-
match :leave, via: [:put, :post, :get]
|
26
|
-
|
27
|
-
match :winner, via: [:put, :post, :get], path: 'winner/:variant'
|
28
|
-
match :clear, via: [:put, :post, :get]
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
1
|
+
# this not only organizes the routes, but also ensures they're only included
|
2
|
+
# once (via ruby require behavior), because having two separate engines in the
|
3
|
+
# gem can cause routes to be parsed twice and double up
|
4
|
+
require_relative 'routes/engine'
|
5
|
+
require_relative 'routes/admin'
|
@@ -6,7 +6,8 @@ module TrailGuide
|
|
6
6
|
def initialize(&block)
|
7
7
|
configure do |config|
|
8
8
|
config.adapter = -> (context) do
|
9
|
-
if context.respond_to?(:
|
9
|
+
if (context.respond_to?(:trailguide_user, true) && context.send(:trailguide_user).present?) ||
|
10
|
+
(context.respond_to?(:current_user, true) && context.send(:current_user).present?)
|
10
11
|
TrailGuide::Adapters::Participants::Redis
|
11
12
|
elsif context.respond_to?(:cookies, true)
|
12
13
|
TrailGuide::Adapters::Participants::Cookie
|
@@ -6,7 +6,10 @@ module TrailGuide
|
|
6
6
|
def initialize(&block)
|
7
7
|
configure do |config|
|
8
8
|
config.namespace = :participants
|
9
|
-
config.lookup = -> (context) {
|
9
|
+
config.lookup = -> (context) {
|
10
|
+
context.try(:trailguide_user).try(:id) ||
|
11
|
+
context.try(:current_user).try(:id)
|
12
|
+
}
|
10
13
|
config.expiration = nil
|
11
14
|
|
12
15
|
yield(config) if block_given?
|
data/lib/trail_guide/catalog.rb
CHANGED
@@ -99,10 +99,22 @@ module TrailGuide
|
|
99
99
|
self.class.new(to_a.select(&:running?))
|
100
100
|
end
|
101
101
|
|
102
|
+
def paused
|
103
|
+
self.class.new(to_a.select(&:paused?))
|
104
|
+
end
|
105
|
+
|
102
106
|
def stopped
|
103
107
|
self.class.new(to_a.select(&:stopped?))
|
104
108
|
end
|
105
109
|
|
110
|
+
def ended
|
111
|
+
self.class.new(to_a.select(&:winner?))
|
112
|
+
end
|
113
|
+
|
114
|
+
def not_running
|
115
|
+
self.class.new(to_a.select { |e| !e.running? })
|
116
|
+
end
|
117
|
+
|
106
118
|
def by_started
|
107
119
|
scoped = to_a.sort do |a,b|
|
108
120
|
if a.running? && !b.running?
|
@@ -19,6 +19,10 @@ module TrailGuide
|
|
19
19
|
parent.start!
|
20
20
|
end
|
21
21
|
|
22
|
+
def pause!
|
23
|
+
parent.pause!
|
24
|
+
end
|
25
|
+
|
22
26
|
def stop!
|
23
27
|
parent.stop!
|
24
28
|
end
|
@@ -31,6 +35,10 @@ module TrailGuide
|
|
31
35
|
parent.started_at
|
32
36
|
end
|
33
37
|
|
38
|
+
def paused_at
|
39
|
+
parent.paused_at
|
40
|
+
end
|
41
|
+
|
34
42
|
def stopped_at
|
35
43
|
parent.stopped_at
|
36
44
|
end
|
@@ -41,7 +49,7 @@ module TrailGuide
|
|
41
49
|
|
42
50
|
# use the parent experiment as the algorithm and map to the matching variant
|
43
51
|
def algorithm_choose!(metadata: nil)
|
44
|
-
variant = parent.new(participant).choose!(metadata: metadata)
|
52
|
+
variant = parent.new(participant.participant).choose!(metadata: metadata)
|
45
53
|
variants.find { |var| var == variant.name }
|
46
54
|
end
|
47
55
|
end
|
data/lib/trail_guide/config.rb
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
module TrailGuide
|
2
2
|
class Config < Canfig::Config
|
3
3
|
DEFAULT_KEYS = [
|
4
|
-
:redis, :disabled, :override_parameter,
|
5
|
-
:
|
6
|
-
:
|
7
|
-
:cleanup_participant_experiments, :unity_ttl
|
4
|
+
:logger, :redis, :disabled, :override_parameter,
|
5
|
+
:allow_multiple_experiments, :adapter, :on_adapter_failover,
|
6
|
+
:filtered_ip_addresses, :filtered_user_agents, :request_filter,
|
7
|
+
:include_helpers, :cleanup_participant_experiments, :unity_ttl
|
8
8
|
].freeze
|
9
9
|
|
10
10
|
def initialize(*args, **opts, &block)
|
@@ -52,18 +52,25 @@ module TrailGuide
|
|
52
52
|
started
|
53
53
|
end
|
54
54
|
|
55
|
+
def pause!(context=nil)
|
56
|
+
return false unless running? && configuration.can_resume?
|
57
|
+
paused = TrailGuide.redis.hset(storage_key, 'paused_at', Time.now.to_i)
|
58
|
+
run_callbacks(:on_pause, context)
|
59
|
+
paused
|
60
|
+
end
|
61
|
+
|
55
62
|
def stop!(context=nil)
|
56
|
-
return false unless
|
63
|
+
return false unless started? && !stopped?
|
57
64
|
stopped = TrailGuide.redis.hset(storage_key, 'stopped_at', Time.now.to_i)
|
58
65
|
run_callbacks(:on_stop, context)
|
59
66
|
stopped
|
60
67
|
end
|
61
68
|
|
62
69
|
def resume!(context=nil)
|
63
|
-
return false unless
|
64
|
-
|
70
|
+
return false unless paused? && configuration.can_resume?
|
71
|
+
resumed = TrailGuide.redis.hdel(storage_key, 'paused_at')
|
65
72
|
run_callbacks(:on_resume, context)
|
66
|
-
|
73
|
+
resumed
|
67
74
|
end
|
68
75
|
|
69
76
|
def started_at
|
@@ -71,6 +78,11 @@ module TrailGuide
|
|
71
78
|
return Time.at(started.to_i) if started
|
72
79
|
end
|
73
80
|
|
81
|
+
def paused_at
|
82
|
+
paused = TrailGuide.redis.hget(storage_key, 'paused_at')
|
83
|
+
return Time.at(paused.to_i) if paused
|
84
|
+
end
|
85
|
+
|
74
86
|
def stopped_at
|
75
87
|
stopped = TrailGuide.redis.hget(storage_key, 'stopped_at')
|
76
88
|
return Time.at(stopped.to_i) if stopped
|
@@ -80,12 +92,16 @@ module TrailGuide
|
|
80
92
|
started_at && started_at <= Time.now
|
81
93
|
end
|
82
94
|
|
95
|
+
def paused?
|
96
|
+
paused_at && paused_at <= Time.now
|
97
|
+
end
|
98
|
+
|
83
99
|
def stopped?
|
84
100
|
stopped_at && stopped_at <= Time.now
|
85
101
|
end
|
86
102
|
|
87
103
|
def running?
|
88
|
-
started? && !stopped?
|
104
|
+
started? && !paused? && !stopped?
|
89
105
|
end
|
90
106
|
|
91
107
|
def declare_winner!(variant, context=nil)
|
@@ -5,13 +5,13 @@ module TrailGuide
|
|
5
5
|
:name, :summary, :preview_url, :algorithm, :metric, :variants, :goals,
|
6
6
|
:start_manually, :reset_manually, :store_override, :track_override,
|
7
7
|
:combined, :allow_multiple_conversions, :allow_multiple_goals,
|
8
|
-
:track_winner_conversions, :skip_request_filter, :target_sample_size
|
8
|
+
:track_winner_conversions, :skip_request_filter, :target_sample_size,
|
9
|
+
:can_resume
|
9
10
|
].freeze
|
10
11
|
|
11
12
|
CALLBACK_KEYS = [
|
12
|
-
:on_start, :on_stop, :on_resume, :on_winner, :on_reset,
|
13
|
-
:on_choose, :on_use, :on_convert,
|
14
|
-
:on_redis_failover,
|
13
|
+
:on_start, :on_stop, :on_pause, :on_resume, :on_winner, :on_reset,
|
14
|
+
:on_delete, :on_choose, :on_use, :on_convert, :on_redis_failover,
|
15
15
|
:allow_participation, :allow_conversion, :rollout_winner
|
16
16
|
].freeze
|
17
17
|
|
@@ -73,6 +73,10 @@ module TrailGuide
|
|
73
73
|
!!skip_request_filter
|
74
74
|
end
|
75
75
|
|
76
|
+
def can_resume?
|
77
|
+
!!can_resume
|
78
|
+
end
|
79
|
+
|
76
80
|
def name
|
77
81
|
@name ||= (self[:name] || experiment.name).try(:to_s).try(:underscore).try(:to_sym)
|
78
82
|
end
|
@@ -160,6 +164,11 @@ module TrailGuide
|
|
160
164
|
self[:on_stop] << (meth || block)
|
161
165
|
end
|
162
166
|
|
167
|
+
def on_pause(meth=nil, &block)
|
168
|
+
self[:on_pause] ||= []
|
169
|
+
self[:on_pause] << (meth || block)
|
170
|
+
end
|
171
|
+
|
163
172
|
def on_resume(meth=nil, &block)
|
164
173
|
self[:on_resume] ||= []
|
165
174
|
self[:on_resume] << (meth || block)
|