trailguide 0.1.30 → 0.1.31

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.running?
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.running?
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.experiments_path(anchor: experiment.experiment_name)
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.experiments_path(anchor: experiment.experiment_name)
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.experiments_path(anchor: experiment.experiment_name)
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.reset!(self) && experiment.start!(self)
33
- redirect_to trail_guide_admin.experiments_path(anchor: experiment.experiment_name)
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
- <strong class="total"><%= TrailGuide.catalog.count %></strong> experiments
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
- <strong class="running"><%= TrailGuide.catalog.running.count %></strong> running
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
- <strong class="stopped"><%= TrailGuide.catalog.stopped.count %></strong> stopped
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
- <%= link_to trail_guide_admin.stop_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 %>
23
- <span class="fas fa-pause" />
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.started? %>
29
- <%= 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 %>
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
- <% else %>
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">&mdash;</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">&nbsp;</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
- <%= link_to trail_guide_admin.stop_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 %>
23
- <span class="fas fa-pause" />
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.started? %>
29
- <%= 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 %>
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
- <% else %>
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">&mdash;</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">&nbsp;</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
- <% TrailGuide.catalog.by_started.each do |experiment| %>
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
- Rails.logger.error("#{error.class.name}: #{error.message}")
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
- Rails.logger.error("#{error.class.name}: #{error.message}")
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
- 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
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?(:current_user, true) && context.send(:current_user).present?
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) { context.current_user.id }
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?
@@ -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
@@ -1,10 +1,10 @@
1
1
  module TrailGuide
2
2
  class Config < Canfig::Config
3
3
  DEFAULT_KEYS = [
4
- :redis, :disabled, :override_parameter, :allow_multiple_experiments,
5
- :adapter, :on_adapter_failover, :filtered_ip_addresses,
6
- :filtered_user_agents, :request_filter, :include_helpers,
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 running?
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 started? && stopped?
64
- restarted = TrailGuide.redis.hdel(storage_key, 'stopped_at')
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
- restarted
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, :on_delete,
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)