trailguide 0.1.13 → 0.1.14

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 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)