trailguide 0.1.30 → 0.1.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +676 -23
- data/app/controllers/trail_guide/admin/application_controller.rb +3 -3
- data/app/controllers/trail_guide/admin/experiments_controller.rb +21 -5
- data/app/views/layouts/trail_guide/admin/_header.erb +31 -3
- data/app/views/trail_guide/admin/experiments/_combined_experiment.html.erb +29 -7
- data/app/views/trail_guide/admin/experiments/_experiment.html.erb +29 -7
- data/app/views/trail_guide/admin/experiments/index.html.erb +1 -1
- data/config/initializers/trailguide.rb +33 -3
- data/config/routes/admin.rb +22 -0
- data/config/routes/engine.rb +13 -0
- data/config/routes.rb +5 -31
- data/lib/trail_guide/adapters/participants/multi.rb +2 -1
- data/lib/trail_guide/adapters/participants/redis.rb +4 -1
- data/lib/trail_guide/catalog.rb +12 -0
- data/lib/trail_guide/combined_experiment.rb +9 -1
- data/lib/trail_guide/config.rb +4 -4
- data/lib/trail_guide/experiments/base.rb +21 -5
- data/lib/trail_guide/experiments/config.rb +13 -4
- data/lib/trail_guide/helper.rb +48 -0
- data/lib/trail_guide/version.rb +1 -1
- data/lib/trailguide.rb +1 -1
- metadata +4 -2
@@ -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)
|