trailguide 0.1.13 → 0.1.14

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c626f06613b7702e01f9c19ef77f6f77e022f0c1280cc13205e46a95bc3ff548
4
- data.tar.gz: a91f183473695792a9c067d3ca8e59d91177b0bf97bc1ae29d34c976ba177760
3
+ metadata.gz: e92b5781f0ec93e65f9538268fd8fa00b5be79008ed83337de2b7091020de719
4
+ data.tar.gz: 547d8afc88e203a7ce6d0995d0efaa0e582b643c80cc0ffdfc7b2743c31aa18b
5
5
  SHA512:
6
- metadata.gz: a0952f9bdb7840f6e201f78a0eb46bfcfe296bcfb115db735062923a8ae9e670c194e7c244225471a1484eaa4649a80f0612061fa4c1c81e7fa0c4824e89ddb3
7
- data.tar.gz: 5432d3d30169caa958a344f99d732f389cd1ed23c052a41a0f86a6ef1b39ad2fac7e337dc81d586875d190d3ef9f2236d3abf81268f8ee3714458582409a86b7
6
+ metadata.gz: 96d7e94e72fb228b63cac5331d74b8ec5440ad234e4350b47191343b797c83ad50d35a0c9d87131dfc8157fa99355df7e36cff159ce05ab88e70ae5b6315e02d
7
+ data.tar.gz: 1323352dda2d506d4153c9b70bffd90250077b4b9728fa732f878ca181670921758a4508754e5d38484ced8b8cd684eaa06d0489631b02a15f8261a3b7438efa
@@ -1,6 +1,6 @@
1
1
  module TrailGuide
2
2
  module Admin
3
- class ApplicationController < ActionController::Base
3
+ class ApplicationController < ::ApplicationController
4
4
  protect_from_forgery with: :exception
5
5
  end
6
6
  end
@@ -10,32 +10,50 @@ module TrailGuide
10
10
 
11
11
  def start
12
12
  experiment.start!
13
- redirect_to :back rescue redirect_to trail_guide_admin.experiments_path
13
+ redirect_to trail_guide_admin.experiments_path(anchor: experiment.experiment_name)
14
14
  end
15
15
 
16
16
  def stop
17
17
  experiment.stop!
18
- redirect_to :back rescue redirect_to trail_guide_admin.experiments_path
18
+ redirect_to trail_guide_admin.experiments_path(anchor: experiment.experiment_name)
19
19
  end
20
20
 
21
21
  def reset
22
22
  experiment.reset!
23
- redirect_to :back rescue redirect_to trail_guide_admin.experiments_path
23
+ redirect_to trail_guide_admin.experiments_path(anchor: experiment.experiment_name)
24
24
  end
25
25
 
26
26
  def resume
27
27
  experiment.resume!
28
- redirect_to :back rescue redirect_to trail_guide_admin.experiments_path
28
+ redirect_to trail_guide_admin.experiments_path(anchor: experiment.experiment_name)
29
29
  end
30
30
 
31
31
  def restart
32
32
  experiment.reset! && experiment.start!
33
- redirect_to :back rescue redirect_to trail_guide_admin.experiments_path
33
+ redirect_to trail_guide_admin.experiments_path(anchor: experiment.experiment_name)
34
+ end
35
+
36
+ def join
37
+ participant.exit!(experiment)
38
+ variant = experiment.variants.find { |var| var == params[:variant] }
39
+ variant.increment_participation!
40
+ participant.participating!(variant)
41
+ redirect_to trail_guide_admin.experiments_path(anchor: experiment.experiment_name)
42
+ end
43
+
44
+ def leave
45
+ participant.exit!(experiment)
46
+ redirect_to trail_guide_admin.experiments_path(anchor: experiment.experiment_name)
34
47
  end
35
48
 
36
49
  def winner
37
50
  experiment.declare_winner!(params[:variant])
38
- redirect_to :back rescue redirect_to trail_guide_admin.experiments_path
51
+ redirect_to trail_guide_admin.experiments_path(anchor: experiment.experiment_name)
52
+ end
53
+
54
+ def clear
55
+ experiment.clear_winner!
56
+ redirect_to trail_guide_admin.experiments_path(anchor: experiment.experiment_name)
39
57
  end
40
58
 
41
59
  private
@@ -43,6 +61,11 @@ module TrailGuide
43
61
  def experiment
44
62
  @experiment ||= TrailGuide.catalog.find(params[:id])
45
63
  end
64
+
65
+ def participant
66
+ @participant ||= TrailGuide::Participant.new(self)
67
+ end
68
+ helper_method :participant
46
69
  end
47
70
  end
48
71
  end
@@ -0,0 +1,129 @@
1
+ <div class="row justify-content-center">
2
+ <div class="col-sm-12 col-md-10 col-lg-8">
3
+ <div class="row">
4
+ <div class="col-sm-12 col-md-6 col-lg-8">
5
+ <h3 style="margin: 0; padding: 0;" id="<%= combined_experiment.experiment_name %>">
6
+ <%= link_to combined_experiment.experiment_name.to_s.humanize.titleize, trail_guide_admin.experiments_path(anchor: combined_experiment.experiment_name), class: "text-dark" %>
7
+ </h3>
8
+ </div>
9
+ <div class="col-sm-12 col-md-6 col-lg-4 text-right">
10
+ <% if combined_experiment.running? %>
11
+ <%= link_to "stop", trail_guide_admin.stop_experiment_path(combined_experiment.experiment_name), class: 'btn btn-sm btn-warning', method: :put %>
12
+ <%= link_to "restart", trail_guide_admin.restart_experiment_path(combined_experiment.experiment_name), class: 'btn btn-sm btn-danger',method: :put %>
13
+ <% elsif combined_experiment.started? %>
14
+ <%= link_to "resume", trail_guide_admin.resume_experiment_path(combined_experiment.experiment_name), class: 'btn btn-sm btn-primary',method: :put %>
15
+ <% else %>
16
+ <%= link_to "start", trail_guide_admin.start_experiment_path(combined_experiment.experiment_name), class: 'btn btn-sm btn-success',method: :put %>
17
+ <% end %>
18
+ <%= link_to "reset", trail_guide_admin.reset_experiment_path(combined_experiment.experiment_name), class: 'btn btn-sm btn-outline-danger',method: :put %>
19
+ </div>
20
+ </div>
21
+
22
+ <div class="row">
23
+ <div class="col-sm-12 text-right">
24
+ <% if combined_experiment.started? %>
25
+ <small class="text-muted"><%= combined_experiment.started_at.strftime('%b %e %Y @ %l:%M %p') %></small>
26
+ <span class="text-muted">&mdash;</span>
27
+ <% if combined_experiment.stopped? %>
28
+ <small class="text-muted"><%= combined_experiment.stopped_at.strftime('%b %e %Y @ %l:%M %p') %></small>
29
+ <% else %>
30
+ <small class="text-muted">running</small>
31
+ <% end %>
32
+ <% else %>
33
+ <small class="text-muted">not running</small>
34
+ <% end %>
35
+ </div>
36
+ </div>
37
+
38
+ <table class="table table-hover">
39
+ <% combined_experiments = combined_experiment.combined.map { |e| TrailGuide.catalog.find(e) } %>
40
+ <% combined_experiments.each do |experiment| %>
41
+ <thead class="thead-light">
42
+ <tr>
43
+ <th scope="col">
44
+ <h4 style="margin: 0; padding: 0" id="<%= experiment.experiment_name %>">
45
+ <%= link_to experiment.experiment_name.to_s.humanize.titleize, trail_guide_admin.experiments_path(anchor: experiment.experiment_name), class: "text-dark" %>
46
+ </h4>
47
+ </th>
48
+
49
+ <th scope="col">Participants</th>
50
+
51
+ <% if experiment.goals.empty? %>
52
+ <th scope="col">Converted</th>
53
+ <% else %>
54
+ <% experiment.goals.each do |goal| %>
55
+ <th scope="col"><%= goal.to_s.humanize.titleize %></th>
56
+ <% end %>
57
+ <% end %>
58
+
59
+ <th>&nbsp;</th>
60
+ </tr>
61
+ </thead>
62
+
63
+ <tbody>
64
+ <% experiment.variants.each do |variant| %>
65
+ <tr class="<%= "table-secondary" if variant.control? %>">
66
+ <th scope="row">
67
+ <%= variant.name.to_s.humanize.titleize %>
68
+ <% if experiment.running? && !experiment.winner? && participant.variant(experiment) == variant %>
69
+ <span class="badge badge-secondary">joined</span>
70
+ <% end %>
71
+ <% if experiment.winner? && variant == experiment.winner %>
72
+ <span class="badge badge-primary">winner</span>
73
+ <% end %>
74
+ <% if variant.control? %>
75
+ <small class="text-muted">control</small>
76
+ <% end %>
77
+ </th>
78
+
79
+ <td><%= variant.participants %></td>
80
+
81
+ <% if experiment.goals.empty? %>
82
+ <td><%= variant.converted %></td>
83
+ <% else %>
84
+ <% experiment.goals.each do |goal| %>
85
+ <td><%= variant.converted(goal) %></td>
86
+ <% end %>
87
+ <% end %>
88
+
89
+ <td class="text-right">
90
+ <% if experiment.running? && !experiment.winner? %>
91
+ <% if participant.variant(experiment) == variant %>
92
+ <%= link_to "leave group", trail_guide_admin.leave_experiment_path(experiment.experiment_name), class: "btn btn-sm btn-outline-secondary", method: :put %>
93
+ <% else %>
94
+ <%= link_to "enter group", trail_guide_admin.join_experiment_path(experiment.experiment_name, variant.name), class: "btn btn-sm btn-secondary", method: :put %>
95
+ <% end %>
96
+ <% end %>
97
+
98
+ <% if !experiment.winner? || variant != experiment.winner %>
99
+ <%= link_to "select winner", trail_guide_admin.winner_experiment_path(experiment.experiment_name, variant.name), class: "btn btn-sm btn-#{experiment.winner? ? "outline-" : ""}primary", method: :put %>
100
+ <% elsif experiment.winner? && variant == experiment.winner %>
101
+ <%= link_to "remove winner", trail_guide_admin.clear_experiment_path(experiment.experiment_name), class: "btn btn-sm btn-warning", method: :put %>
102
+ <% end %>
103
+ </td>
104
+ </tr>
105
+ <% end %>
106
+ </tbody>
107
+ <% end %>
108
+
109
+ <tfoot class="thead-light">
110
+ <tr>
111
+ <th scope="row">Totals</th>
112
+ <th><%= combined_experiment.variants.sum(&:participants) %></th>
113
+ <% if combined_experiment.goals.empty? %>
114
+ <th><%= combined_experiments.sum { |e| e.variants.sum(&:converted) } %></th>
115
+ <% else %>
116
+ <% combined_experiment.goals.each do |goal| %>
117
+ <th><%= combined_experiments.sum { |e| e.variants.sum { |v| v.converted(goal) } } %></th>
118
+ <% end %>
119
+ <% end %>
120
+ <th>&nbsp;</th>
121
+ </tr>
122
+ </tfoot>
123
+ </table>
124
+ </div>
125
+ </div>
126
+
127
+ <br />
128
+ <br />
129
+ <br />
@@ -2,11 +2,8 @@
2
2
  <div class="col-sm-12 col-md-10 col-lg-8">
3
3
  <div class="row">
4
4
  <div class="col-sm-12 col-md-6 col-lg-8">
5
- <h3>
6
- <%= experiment.experiment_name.to_s.humanize.titleize %>
7
- <% if experiment.started? %>
8
- <small class="text-muted"><%= experiment.started_at.strftime('%b %e %Y @ %l:%M %p') %></small>
9
- <% end %>
5
+ <h3 style="margin: 0; padding: 0;" id="<%= experiment.experiment_name %>">
6
+ <%= link_to experiment.experiment_name.to_s.humanize.titleize, trail_guide_admin.experiments_path(anchor: experiment.experiment_name), class: "text-dark" %>
10
7
  </h3>
11
8
  </div>
12
9
  <div class="col-sm-12 col-md-6 col-lg-4 text-right">
@@ -22,6 +19,22 @@
22
19
  </div>
23
20
  </div>
24
21
 
22
+ <div class="row">
23
+ <div class="col-sm-12 text-right">
24
+ <% if experiment.started? %>
25
+ <small class="text-muted"><%= experiment.started_at.strftime('%b %e %Y @ %l:%M %p') %></small>
26
+ <span class="text-muted">&mdash;</span>
27
+ <% if experiment.stopped? %>
28
+ <small class="text-muted"><%= experiment.stopped_at.strftime('%b %e %Y @ %l:%M %p') %></small>
29
+ <% else %>
30
+ <small class="text-muted">running</small>
31
+ <% end %>
32
+ <% else %>
33
+ <small class="text-muted">not running</small>
34
+ <% end %>
35
+ </div>
36
+ </div>
37
+
25
38
  <table class="table table-hover">
26
39
  <thead class="thead-light">
27
40
  <tr>
@@ -46,6 +59,12 @@
46
59
  <tr class="<%= "table-secondary" if variant.control? %>">
47
60
  <th scope="row">
48
61
  <%= variant.name.to_s.humanize.titleize %>
62
+ <% if experiment.running? && !experiment.winner? && participant.variant(experiment) == variant %>
63
+ <span class="badge badge-secondary">joined</span>
64
+ <% end %>
65
+ <% if experiment.winner? && variant == experiment.winner %>
66
+ <span class="badge badge-primary">winner</span>
67
+ <% end %>
49
68
  <% if variant.control? %>
50
69
  <small class="text-muted">control</small>
51
70
  <% end %>
@@ -62,12 +81,18 @@
62
81
  <% end %>
63
82
 
64
83
  <td class="text-right">
65
- <% if experiment.winner? %>
66
- <% if variant == experiment.winner %>
67
- <span class="badge badge-primary">winner</span>
84
+ <% if experiment.running? && !experiment.winner? %>
85
+ <% if participant.variant(experiment) == variant %>
86
+ <%= link_to "leave group", trail_guide_admin.leave_experiment_path(experiment.experiment_name), class: "btn btn-sm btn-outline-secondary", method: :put %>
87
+ <% else %>
88
+ <%= link_to "enter group", trail_guide_admin.join_experiment_path(experiment.experiment_name, variant.name), class: "btn btn-sm btn-secondary", method: :put %>
68
89
  <% end %>
69
- <% else %>
70
- <%= link_to "select winner", trail_guide_admin.winner_experiment_path(experiment.experiment_name, variant.name), class: 'btn btn-sm btn-outline-primary', method: :put %>
90
+ <% end %>
91
+
92
+ <% if !experiment.winner? || variant != experiment.winner %>
93
+ <%= link_to "select winner", trail_guide_admin.winner_experiment_path(experiment.experiment_name, variant.name), class: "btn btn-sm btn-#{experiment.winner? ? "outline-" : ""}primary", method: :put %>
94
+ <% elsif experiment.winner? && variant == experiment.winner %>
95
+ <%= link_to "remove winner", trail_guide_admin.clear_experiment_path(experiment.experiment_name), class: "btn btn-sm btn-warning", method: :put %>
71
96
  <% end %>
72
97
  </td>
73
98
  </tr>
@@ -1,13 +1,17 @@
1
1
  <div class="container-fluid">
2
2
  <div class="row justify-content-center">
3
3
  <div class="col-sm-12 col-md-10 col-lg-8">
4
- <h1>Experiments</h1>
4
+ <h1><%= link_to "Experiments", trail_guide_admin.experiments_path, class: "text-dark" %></h1>
5
5
  </div>
6
6
  </div>
7
7
 
8
8
  <hr />
9
9
 
10
10
  <% TrailGuide.catalog.each do |experiment| %>
11
- <%= render 'experiment', experiment: experiment %>
11
+ <% if experiment.combined? %>
12
+ <%= render 'combined_experiment', combined_experiment: experiment %>
13
+ <% else %>
14
+ <%= render 'experiment', experiment: experiment %>
15
+ <% end %>
12
16
  <% end %>
13
17
  </div>
data/config/routes.rb CHANGED
@@ -21,7 +21,12 @@ if defined?(TrailGuide::Admin::Engine)
21
21
  match :reset, via: [:put, :post, :get]
22
22
  match :resume, via: [:put, :post, :get]
23
23
  match :restart, via: [:put, :post, :get]
24
+
25
+ match :join, via: [:put, :post, :get], path: 'join/:variant'
26
+ match :leave, via: [:put, :post, :get]
27
+
24
28
  match :winner, via: [:put, :post, :get], path: 'winner/:variant'
29
+ match :clear, via: [:put, :post, :get]
25
30
  end
26
31
  end
27
32
  end
@@ -18,8 +18,21 @@ module TrailGuide
18
18
  def select(name)
19
19
  catalog.select(name)
20
20
  end
21
+
22
+ def combined_experiment(combined, name)
23
+ experiment = Class.new(TrailGuide::CombinedExperiment)
24
+ experiment.configure combined.configuration.to_h.merge({
25
+ name: name.to_s.underscore.to_sym,
26
+ parent: combined,
27
+ combined: [],
28
+ variants: combined.configuration.variants.map { |var| Variant.new(experiment, var.name, metadata: var.metadata, weight: var.weight, control: var.control?) },
29
+ # TODO also map goals once they're separate classes
30
+ })
31
+ experiment
32
+ end
21
33
  end
22
34
 
35
+ delegate :combined_experiment, to: :class
23
36
  attr_reader :experiments
24
37
 
25
38
  def initialize(experiments=[])
@@ -30,15 +43,34 @@ module TrailGuide
30
43
  experiments.each(&block)
31
44
  end
32
45
 
46
+ def all
47
+ experiments.map do |exp|
48
+ if exp.combined?
49
+ exp.combined.map { |name| combined_experiment(exp, name) }
50
+ else
51
+ exp
52
+ end
53
+ end.flatten
54
+ end
55
+
33
56
  def find(name)
34
57
  if name.is_a?(Class)
35
58
  experiments.find { |exp| exp == name }
36
59
  else
37
- experiments.find do |exp|
60
+ experiment = experiments.find do |exp|
38
61
  exp.experiment_name == name.to_s.underscore.to_sym ||
39
62
  exp.metric == name.to_s.underscore.to_sym ||
40
63
  exp.name == name.to_s.classify
41
64
  end
65
+ return experiment if experiment.present?
66
+
67
+ combined = experiments.find do |exp|
68
+ next unless exp.combined?
69
+ exp.combined.any? { |combo| combo.to_s.underscore.to_sym == name.to_s.underscore.to_sym }
70
+ end
71
+ return nil unless combined.present?
72
+
73
+ return combined_experiment(combined, name)
42
74
  end
43
75
  end
44
76
 
@@ -46,10 +78,18 @@ module TrailGuide
46
78
  if name.is_a?(Class)
47
79
  experiments.select { |exp| exp == name }
48
80
  else
81
+ # TODO we can be more efficient than mapping twice here
49
82
  experiments.select do |exp|
50
83
  exp.experiment_name == name.to_s.underscore.to_sym ||
51
84
  exp.metric == name.to_s.underscore.to_sym ||
52
- exp.name == name.to_s.classify
85
+ exp.name == name.to_s.classify ||
86
+ (exp.combined? && exp.combined.any? { |combo| combo.to_s.underscore.to_sym == name.to_s.underscore.to_sym })
87
+ end.map do |exp|
88
+ if exp.combined? && exp.combined.any? { |combo| combo.to_s.underscore.to_sym == name.to_s.underscore.to_sym }
89
+ combined_experiment(exp, name)
90
+ else
91
+ exp
92
+ end
53
93
  end
54
94
  end
55
95
  end
@@ -0,0 +1,48 @@
1
+ require "trail_guide/experiments/base"
2
+ require "trail_guide/experiments/combined_config"
3
+
4
+ module TrailGuide
5
+ class CombinedExperiment < Experiments::Base
6
+ class << self
7
+ delegate :parent, to: :configuration
8
+
9
+ def configuration
10
+ @configuration ||= Experiments::CombinedConfig.new(self)
11
+ end
12
+
13
+ # TODO if just I delegate on this inheriting class, will that override the
14
+ # defined methods on the base class? and will they interplay nicely? like
15
+ # with `started?` calling `started_at`, etc.?
16
+ #
17
+ # really wishing i'd written some specs right about now :-P
18
+ def start!
19
+ parent.start!
20
+ end
21
+
22
+ def stop!
23
+ parent.stop!
24
+ end
25
+
26
+ def resume!
27
+ parent.resume!
28
+ end
29
+
30
+ def started_at
31
+ parent.started_at
32
+ end
33
+
34
+ def stopped_at
35
+ parent.stopped_at
36
+ end
37
+ end
38
+
39
+ delegate :parent, to: :class
40
+ delegate :running?, :started?, :started_at, :start!, to: :parent
41
+
42
+ # use the parent experiment as the algorithm and map to the matching variant
43
+ def algorithm_choose!(metadata: nil)
44
+ variant = parent.new(participant).choose!(metadata: metadata)
45
+ variants.find { |var| var == variant.name }
46
+ end
47
+ end
48
+ end
@@ -46,6 +46,7 @@ module TrailGuide
46
46
  config.metric = options[:metric] if options[:metric]
47
47
  config.algorithm = options[:algorithm] if options[:algorithm]
48
48
  config.goals = options[:goals] if options[:goals]
49
+ config.combined = options[:combined] if options[:combined]
49
50
  config.reset_manually = options[:reset_manually] if options.key?(:reset_manually)
50
51
  config.start_manually = options[:start_manually] if options.key?(:start_manually)
51
52
  config.store_override = options[:store_override] if options.key?(:store_override)