trailguide 0.1.15 → 0.1.16

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: 2f3fe6f7f5a63ba2a075cb5ce550410ab6daed98a35ee565468bd45205ea784b
4
- data.tar.gz: fd1fe329b9c64e4a1da9acaeb47b5601a76311123104d1bd9b4e8dec21076c0b
3
+ metadata.gz: 5bee61db15d9f3cfea6ecfe502be220772f82c34a256a8bdcde1f093ab637cd3
4
+ data.tar.gz: b2f3a245b0643fb631c38f8dffb245a28a8456391fd06ac4d9c12b6f44e1aac3
5
5
  SHA512:
6
- metadata.gz: cd34a9c7628dcb95be218548167eac9fc59e4188ad90b3c528f0cab28bc87b08019736c87d210760b1a47d57344691c3a06561e3a8606030e82c3c29ddb15214
7
- data.tar.gz: fc2afd4f9335ce021eac70b65546a99f1f954928ab2cd8c6e3c442fb221e6e41c640ea2563873d640cb268b95a2de442b2f488bedb1608a8f5ceb98022f6b321
6
+ metadata.gz: 0357c34c58aa5ff3d8d5fab56a5b08f137cf686b44f563d3cb72eea422fb2f60d04a01732ff0f57926a3a36811e8dc82f5fdf3b643705219e75b9d91c0efad15
7
+ data.tar.gz: 8179d27ac67509e1b4ed26c67995d446994fdfec8fe21893c1b2042203de2128fb35714f6683b85bec0ce9f1beb67eca41c3fad3051194d232a8489fd0f882a2
@@ -13,3 +13,16 @@
13
13
  *= require_tree .
14
14
  *= require_self
15
15
  */
16
+
17
+ .navbar-brand img {
18
+ width: 30px;
19
+ height: 30px;
20
+ }
21
+
22
+ main {
23
+ padding-top: 40px;
24
+ }
25
+
26
+ .footer {
27
+ padding: 10px 15px;
28
+ }
@@ -0,0 +1,13 @@
1
+ .experiment {
2
+ margin-bottom: 40px;
3
+ }
4
+
5
+ .experiment table th, .experiment table td {
6
+ line-height: 31px;
7
+ }
8
+
9
+ .experiment table th h5 {
10
+ margin: 0;
11
+ padding: 0;
12
+ line-height: 31px;
13
+ }
@@ -2,6 +2,19 @@ module TrailGuide
2
2
  module Admin
3
3
  class ApplicationController < ::ApplicationController
4
4
  protect_from_forgery with: :exception
5
+
6
+ def preview_url(variant, *args, **opts)
7
+ config_url = variant.experiment.configuration.preview_url
8
+ opts = opts.merge({experiment: {variant.experiment.experiment_name => variant.name}})
9
+ if config_url.respond_to?(:call)
10
+ main_app.instance_exec *args, **opts, &config_url
11
+ elsif config_url.is_a?(Symbol)
12
+ main_app.send(config_url, *args, **opts)
13
+ else
14
+ config_url.to_s + "?experiment[#{variant.experiment.experiment_name}]=#{variant.name}"
15
+ end
16
+ end
17
+ helper_method :preview_url
5
18
  end
6
19
  end
7
20
  end
@@ -0,0 +1,5 @@
1
+ <div class="footer bg-light">
2
+ <div class="text-right">
3
+ <%= link_to "TrailGuide v#{TrailGuide::Version::VERSION}", "https://github.com/markrebec/trailguide", target: :blank, class: "text-muted" %>
4
+ </div>
5
+ </div>
@@ -0,0 +1,8 @@
1
+ <nav class="navbar navbar-expanded-sm navbar-light bg-light">
2
+ <%= link_to trail_guide_admin.experiments_path, class: "navbar-brand" do %>
3
+ <%= image_tag "trail_guide/trailguide.png" %>
4
+ <%= TrailGuide::Admin.configuration.title %>
5
+ <% end %>
6
+ <span class="navbar-brand"><small class="text-muted"><%= TrailGuide::Admin.configuration.subtitle %></small></span>
7
+ </nav>
8
+
@@ -11,8 +11,15 @@
11
11
  <%= stylesheet_link_tag "trail_guide/admin/application", media: "all" %>
12
12
  </head>
13
13
  <body>
14
+ <%= render 'layouts/trail_guide/admin/header' %>
14
15
 
15
- <%= yield %>
16
+ <main>
17
+ <div class="container-fluid">
18
+ <%= yield %>
19
+ </div>
20
+ </main>
21
+
22
+ <%= render 'layouts/trail_guide/admin/footer' %>
16
23
 
17
24
  <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
18
25
  <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
@@ -1,10 +1,10 @@
1
- <div class="row justify-content-center">
1
+ <div class="experiment row justify-content-center">
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 style="margin: 0; padding: 0;" id="<%= combined_experiment.experiment_name %>">
5
+ <h4 id="<%= combined_experiment.experiment_name %>">
6
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>
7
+ </h4>
8
8
  </div>
9
9
  <div class="col-sm-12 col-md-6 col-lg-4 text-right">
10
10
  <% if combined_experiment.running? %>
@@ -20,7 +20,10 @@
20
20
  </div>
21
21
 
22
22
  <div class="row">
23
- <div class="col-sm-12 text-right">
23
+ <div class="col-sm-12 col-md-6 col-lg-8">
24
+ <p><%= combined_experiment.configuration.summary %></p>
25
+ </div>
26
+ <div class="col-sm-12 col-md-6 col-lg-4 text-right">
24
27
  <% if combined_experiment.started? %>
25
28
  <small class="text-muted"><%= combined_experiment.started_at.strftime('%b %e %Y @ %l:%M %p') %></small>
26
29
  <span class="text-muted">&mdash;</span>
@@ -41,9 +44,9 @@
41
44
  <thead class="thead-light">
42
45
  <tr>
43
46
  <th scope="col">
44
- <h4 style="margin: 0; padding: 0" id="<%= experiment.experiment_name %>">
47
+ <h5 id="<%= experiment.experiment_name %>">
45
48
  <%= link_to experiment.experiment_name.to_s.humanize.titleize, trail_guide_admin.experiments_path(anchor: experiment.experiment_name), class: "text-dark" %>
46
- </h4>
49
+ </h5>
47
50
  </th>
48
51
 
49
52
  <th scope="col">Participants</th>
@@ -62,9 +65,14 @@
62
65
 
63
66
  <tbody>
64
67
  <% experiment.variants.each do |variant| %>
65
- <tr class="<%= "table-secondary" if variant.control? %>">
68
+ <tr>
66
69
  <th scope="row">
67
- <%= variant.name.to_s.humanize.titleize %>
70
+ <% if experiment.configuration.preview_url? %>
71
+ <%= link_to variant.name.to_s.humanize.titleize, preview_url(variant), target: :blank, class: "text-dark" %>
72
+ <% else %>
73
+ <%= variant.name.to_s.humanize.titleize %>
74
+ <% end %>
75
+
68
76
  <% if experiment.running? && !experiment.winner? && participant.variant(experiment) == variant %>
69
77
  <span class="badge badge-secondary">joined</span>
70
78
  <% end %>
@@ -108,7 +116,7 @@
108
116
 
109
117
  <tfoot class="thead-light">
110
118
  <tr>
111
- <th scope="row">Totals</th>
119
+ <th scope="row">&nbsp;</th>
112
120
  <th><%= combined_experiment.variants.sum(&:participants) %></th>
113
121
  <% if combined_experiment.goals.empty? %>
114
122
  <th><%= combined_experiments.sum { |e| e.variants.sum(&:converted) } %></th>
@@ -123,7 +131,3 @@
123
131
  </table>
124
132
  </div>
125
133
  </div>
126
-
127
- <br />
128
- <br />
129
- <br />
@@ -1,10 +1,10 @@
1
- <div class="row justify-content-center">
1
+ <div class="experiment row justify-content-center">
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 style="margin: 0; padding: 0;" id="<%= experiment.experiment_name %>">
5
+ <h4 id="<%= experiment.experiment_name %>">
6
6
  <%= link_to experiment.experiment_name.to_s.humanize.titleize, trail_guide_admin.experiments_path(anchor: experiment.experiment_name), class: "text-dark" %>
7
- </h3>
7
+ </h4>
8
8
  </div>
9
9
  <div class="col-sm-12 col-md-6 col-lg-4 text-right">
10
10
  <% if experiment.running? %>
@@ -20,7 +20,10 @@
20
20
  </div>
21
21
 
22
22
  <div class="row">
23
- <div class="col-sm-12 text-right">
23
+ <div class="col-sm-12 col-md-6 col-lg-8">
24
+ <p><%= experiment.configuration.summary %></p>
25
+ </div>
26
+ <div class="col-sm-12 col-md-6 col-lg-4 text-right">
24
27
  <% if experiment.started? %>
25
28
  <small class="text-muted"><%= experiment.started_at.strftime('%b %e %Y @ %l:%M %p') %></small>
26
29
  <span class="text-muted">&mdash;</span>
@@ -38,7 +41,7 @@
38
41
  <table class="table table-hover">
39
42
  <thead class="thead-light">
40
43
  <tr>
41
- <th scope="col">Group</th>
44
+ <th scope="col">&nbsp;</th>
42
45
 
43
46
  <th scope="col">Participants</th>
44
47
 
@@ -56,9 +59,14 @@
56
59
 
57
60
  <tbody>
58
61
  <% experiment.variants.each do |variant| %>
59
- <tr class="<%= "table-secondary" if variant.control? %>">
62
+ <tr>
60
63
  <th scope="row">
61
- <%= variant.name.to_s.humanize.titleize %>
64
+ <% if experiment.configuration.preview_url? %>
65
+ <%= link_to variant.name.to_s.humanize.titleize, preview_url(variant), target: :blank, class: "text-dark" %>
66
+ <% else %>
67
+ <%= variant.name.to_s.humanize.titleize %>
68
+ <% end %>
69
+
62
70
  <% if experiment.running? && !experiment.winner? && participant.variant(experiment) == variant %>
63
71
  <span class="badge badge-secondary">joined</span>
64
72
  <% end %>
@@ -101,7 +109,7 @@
101
109
 
102
110
  <tfoot class="thead-light">
103
111
  <tr>
104
- <th scope="row">Totals</th>
112
+ <th scope="row">&nbsp;</th>
105
113
  <th><%= experiment.variants.sum(&:participants) %></th>
106
114
  <% if experiment.goals.empty? %>
107
115
  <th><%= experiment.variants.sum(&:converted) %></th>
@@ -116,7 +124,3 @@
116
124
  </table>
117
125
  </div>
118
126
  </div>
119
-
120
- <br />
121
- <br />
122
- <br />
@@ -1,12 +1,4 @@
1
- <div class="container-fluid">
2
- <div class="row justify-content-center">
3
- <div class="col-sm-12 col-md-10 col-lg-8">
4
- <h1><%= link_to "Experiments", trail_guide_admin.experiments_path, class: "text-dark" %></h1>
5
- </div>
6
- </div>
7
-
8
- <hr />
9
-
1
+ <div class="experiments">
10
2
  <% TrailGuide.catalog.each do |experiment| %>
11
3
  <% if experiment.combined? %>
12
4
  <%= render 'combined_experiment', combined_experiment: experiment %>
@@ -0,0 +1 @@
1
+ Rails.application.config.assets.precompile += %w( trail_guide/trailguide.png )
@@ -2,5 +2,11 @@ require "trail_guide/admin/engine"
2
2
 
3
3
  module TrailGuide
4
4
  module Admin
5
+ include Canfig::Module
6
+
7
+ configure do |config|
8
+ config.title = 'TrailGuide'
9
+ config.subtitle = 'Experiments and A/B Tests'
10
+ end
5
11
  end
6
12
  end
@@ -1,5 +1,14 @@
1
1
  module TrailGuide
2
2
  class Catalog
3
+ class DSL
4
+ def self.experiment(name, &block)
5
+ Class.new(TrailGuide::Experiment) do
6
+ configure name: name
7
+ configure &block
8
+ end
9
+ end
10
+ end
11
+
3
12
  include Enumerable
4
13
 
5
14
  class << self
@@ -7,16 +16,52 @@ module TrailGuide
7
16
  @catalog ||= new
8
17
  end
9
18
 
10
- def register(klass)
11
- catalog.register(klass)
12
- end
19
+ def load_experiments!
20
+ @catalog = nil
21
+
22
+ # Load experiments from YAML configs if any exists
23
+ load_yaml_experiments(Rails.root.join("config/experiments.yml"))
24
+ Dir[Rails.root.join("config/experiments/**/*.yml")].each { |f| load_yaml_experiments(f) }
13
25
 
14
- def find(name)
15
- catalog.find(name)
26
+ # Load experiments from ruby configs if any exist
27
+ DSL.instance_eval(File.read(Rails.root.join("config/experiments.rb"))) if File.exists?(Rails.root.join("config/experiments.rb"))
28
+ Dir[Rails.root.join("config/experiments/**/*.rb")].each { |f| DSL.instance_eval(File.read(f)) }
29
+
30
+ # Load any experiment classes defined in the app
31
+ Dir[Rails.root.join("app/experiments/**/*.rb")].each { |f| load f }
16
32
  end
17
33
 
18
- def select(name)
19
- catalog.select(name)
34
+ def load_yaml_experiments(file)
35
+ experiments = (YAML.load_file(file) || {} rescue {})
36
+ .symbolize_keys.map { |k,v| [k, v.symbolize_keys] }.to_h
37
+
38
+ experiments.each do |name, options|
39
+ expvars = options[:variants].map do |var|
40
+ if var.is_a?(Array)
41
+ [var[0], var[1].symbolize_keys]
42
+ else
43
+ [var]
44
+ end
45
+ end
46
+
47
+ DSL.experiment(name) do |config|
48
+ expvars.each do |expvar|
49
+ variant *expvar
50
+ end
51
+ # TODO also map goals once they're real classes
52
+ config.control = options[:control] if options[:control]
53
+ config.metric = options[:metric] if options[:metric]
54
+ config.algorithm = options[:algorithm] if options[:algorithm]
55
+ config.goals = options[:goals] if options[:goals]
56
+ config.combined = options[:combined] if options[:combined]
57
+ config.reset_manually = options[:reset_manually] if options.key?(:reset_manually)
58
+ config.start_manually = options[:start_manually] if options.key?(:start_manually)
59
+ config.store_override = options[:store_override] if options.key?(:store_override)
60
+ config.track_override = options[:track_override] if options.key?(:track_override)
61
+ config.allow_multiple_conversions = options[:allow_multiple_conversions] if options.key?(:allow_multiple_conversions)
62
+ config.allow_multiple_goals = options[:allow_multiple_goals] if options.key?(:allow_multiple_goals)
63
+ end
64
+ end
20
65
  end
21
66
 
22
67
  def combined_experiment(combined, name)
@@ -7,63 +7,9 @@ module TrailGuide
7
7
  end
8
8
 
9
9
  initializer "trailguide" do |app|
10
- TrailGuide::Engine.load_experiments
10
+ TrailGuide::Catalog.load_experiments!
11
11
  ActionController::Base.send :include, TrailGuide::Helper
12
12
  ActionController::Base.helper TrailGuide::Helper
13
13
  end
14
-
15
- def self.load_experiments
16
- # Load experiments from YAML configs if any exists
17
- load_yaml_experiments(Rails.root.join("config/experiments.yml"))
18
- Dir[Rails.root.join("config/experiments/**/*.yml")].each { |f| load_yaml_experiments(f) }
19
-
20
- # Load experiments from ruby configs if any exist
21
- DSL.instance_eval(File.read(Rails.root.join("config/experiments.rb"))) if File.exists?(Rails.root.join("config/experiments.rb"))
22
- Dir[Rails.root.join("config/experiments/**/*.rb")].each { |f| DSL.instance_eval(File.read(f)) }
23
-
24
- # Load any experiment classes defined in the app
25
- Dir[Rails.root.join("app/experiments/**/*.rb")].each { |f| load f }
26
- end
27
-
28
- def self.load_yaml_experiments(file)
29
- experiments = (YAML.load_file(file) || {} rescue {})
30
- .symbolize_keys.map { |k,v| [k, v.symbolize_keys] }.to_h
31
-
32
- experiments.each do |name, options|
33
- expvars = options[:variants].map do |var|
34
- if var.is_a?(Array)
35
- [var[0], var[1].symbolize_keys]
36
- else
37
- [var]
38
- end
39
- end
40
-
41
- DSL.experiment(name) do |config|
42
- expvars.each do |expvar|
43
- variant *expvar
44
- end
45
- config.control = options[:control] if options[:control]
46
- config.metric = options[:metric] if options[:metric]
47
- config.algorithm = options[:algorithm] if options[:algorithm]
48
- config.goals = options[:goals] if options[:goals]
49
- config.combined = options[:combined] if options[:combined]
50
- config.reset_manually = options[:reset_manually] if options.key?(:reset_manually)
51
- config.start_manually = options[:start_manually] if options.key?(:start_manually)
52
- config.store_override = options[:store_override] if options.key?(:store_override)
53
- config.track_override = options[:track_override] if options.key?(:track_override)
54
- config.allow_multiple_conversions = options[:allow_multiple_conversions] if options.key?(:allow_multiple_conversions)
55
- config.allow_multiple_goals = options[:allow_multiple_goals] if options.key?(:allow_multiple_goals)
56
- end
57
- end
58
- end
59
-
60
- class DSL
61
- def self.experiment(name, &block)
62
- Class.new(TrailGuide::Experiment) do
63
- configure name: name
64
- configure &block
65
- end
66
- end
67
- end
68
14
  end
69
15
  end
@@ -158,7 +158,7 @@ module TrailGuide
158
158
  end
159
159
 
160
160
  attr_reader :participant
161
- delegate :configuration, :experiment_name, :variants, :control, :funnels,
161
+ delegate :configuration, :experiment_name, :variants, :control, :goals,
162
162
  :storage_key, :running?, :started?, :started_at, :start!, :resettable?,
163
163
  :winner?, :allow_multiple_conversions?, :allow_multiple_goals?,
164
164
  :track_winner_conversions?, :callbacks, to: :class
@@ -221,8 +221,8 @@ module TrailGuide
221
221
  def convert!(checkpoint=nil, metadata: nil)
222
222
  return false if !running? || (winner? && !track_winner_conversions?)
223
223
  return false unless participating?
224
- raise InvalidGoalError, "Invalid goal checkpoint `#{checkpoint}` for `#{experiment_name}`." unless checkpoint.present? || funnels.empty?
225
- raise InvalidGoalError, "Invalid goal checkpoint `#{checkpoint}` for `#{experiment_name}`." unless checkpoint.nil? || funnels.any? { |funnel| funnel == checkpoint.to_s.underscore.to_sym }
224
+ raise InvalidGoalError, "Invalid goal checkpoint `#{checkpoint}` for `#{experiment_name}`." unless checkpoint.present? || goals.empty?
225
+ raise InvalidGoalError, "Invalid goal checkpoint `#{checkpoint}` for `#{experiment_name}`." unless checkpoint.nil? || goals.any? { |goal| goal == checkpoint.to_s.underscore.to_sym }
226
226
  # TODO eventually allow progressing through funnel checkpoints towards goals
227
227
  if converted?(checkpoint)
228
228
  return false unless allow_multiple_conversions?
@@ -14,7 +14,14 @@ module TrailGuide
14
14
  end
15
15
 
16
16
  def self.default_config
17
- { name: nil, metric: nil, variants: [], goals: [], combined: [] }
17
+ { name: nil,
18
+ metric: nil,
19
+ variants: [],
20
+ goals: [],
21
+ combined: [],
22
+ summary: nil,
23
+ preview_url: nil,
24
+ }
18
25
  end
19
26
 
20
27
  def self.callbacks_config
@@ -115,6 +122,10 @@ module TrailGuide
115
122
  !combined.empty?
116
123
  end
117
124
 
125
+ def preview_url?
126
+ !!preview_url
127
+ end
128
+
118
129
  def on_choose(meth=nil, &block)
119
130
  callbacks[:on_choose] << (meth || block)
120
131
  end
@@ -48,7 +48,7 @@ module TrailGuide
48
48
 
49
49
  def converted?(experiment, checkpoint=nil)
50
50
  return false unless experiment.started?
51
- if experiment.funnels.empty?
51
+ if experiment.goals.empty?
52
52
  raise InvalidGoalError, "You provided the checkpoint `#{checkpoint}` but the experiment `#{experiment.experiment_name}` does not have any goals defined." unless checkpoint.nil?
53
53
  storage_key = "#{experiment.storage_key}:converted"
54
54
  return false unless adapter.key?(storage_key)
@@ -56,15 +56,15 @@ module TrailGuide
56
56
  converted_at = Time.at(adapter[storage_key].to_i)
57
57
  converted_at >= experiment.started_at
58
58
  elsif !checkpoint.nil?
59
- raise InvalidGoalError, "Invalid goal checkpoint `#{checkpoint}` for experiment `#{experiment.experiment_name}`." unless experiment.funnels.any? { |funnel| funnel == checkpoint.to_s.underscore.to_sym }
59
+ raise InvalidGoalError, "Invalid goal checkpoint `#{checkpoint}` for experiment `#{experiment.experiment_name}`." unless experiment.goals.any? { |goal| goal == checkpoint.to_s.underscore.to_sym }
60
60
  storage_key = "#{experiment.storage_key}:#{checkpoint.to_s.underscore}"
61
61
  return false unless adapter.key?(storage_key)
62
62
 
63
63
  converted_at = Time.at(adapter[storage_key].to_i)
64
64
  converted_at >= experiment.started_at
65
65
  else
66
- experiment.funnels.each do |funnel|
67
- storage_key = "#{experiment.storage_key}:#{funnel.to_s}"
66
+ experiment.goals.each do |goal|
67
+ storage_key = "#{experiment.storage_key}:#{goal.to_s}"
68
68
  next unless adapter.key?(storage_key)
69
69
  converted_at = Time.at(adapter[storage_key].to_i)
70
70
  return true if converted_at >= experiment.started_at
@@ -90,9 +90,9 @@ module TrailGuide
90
90
  adapter.delete(variant.experiment.storage_key)
91
91
  adapter.delete(variant.storage_key)
92
92
  adapter.delete(storage_key)
93
- variant.experiment.funnels.each do |funnel|
94
- funnel_key = "#{variant.experiment.storage_key}:#{funnel.to_s}"
95
- adapter.delete(funnel_key)
93
+ variant.experiment.goals.each do |goal|
94
+ goal_key = "#{variant.experiment.storage_key}:#{goal.to_s}"
95
+ adapter.delete(goal_key)
96
96
  end
97
97
  else
98
98
  adapter[storage_key] = Time.now.to_i
@@ -57,14 +57,14 @@ module TrailGuide
57
57
  end
58
58
 
59
59
  def converted(checkpoint=nil)
60
- if experiment.funnels.empty?
60
+ if experiment.goals.empty?
61
61
  raise InvalidGoalError, "You provided the checkpoint `#{checkpoint}` but the experiment `#{experiment.experiment_name}` does not have any goals defined." unless checkpoint.nil?
62
62
  (TrailGuide.redis.hget(storage_key, 'converted') || 0).to_i
63
63
  elsif !checkpoint.nil?
64
- raise InvalidGoalError, "Invalid goal checkpoint `#{checkpoint}` for experiment `#{experiment.experiment_name}`." unless experiment.funnels.any? { |funnel| funnel == checkpoint.to_s.underscore.to_sym }
64
+ raise InvalidGoalError, "Invalid goal checkpoint `#{checkpoint}` for experiment `#{experiment.experiment_name}`." unless experiment.goals.any? { |goal| goal == checkpoint.to_s.underscore.to_sym }
65
65
  (TrailGuide.redis.hget(storage_key, checkpoint.to_s.underscore) || 0).to_i
66
66
  else
67
- experiment.funnels.sum do |checkpoint|
67
+ experiment.goals.sum do |checkpoint|
68
68
  (TrailGuide.redis.hget(storage_key, checkpoint.to_s.underscore) || 0).to_i
69
69
  end
70
70
  end
@@ -2,7 +2,7 @@ module TrailGuide
2
2
  module Version
3
3
  MAJOR = 0
4
4
  MINOR = 1
5
- PATCH = 15
5
+ PATCH = 16
6
6
  VERSION = "#{MAJOR}.#{MINOR}.#{PATCH}"
7
7
 
8
8
  class << self
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: trailguide
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.15
4
+ version: 0.1.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mark Rebec
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-03-07 00:00:00.000000000 Z
11
+ date: 2019-03-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -104,16 +104,21 @@ files:
104
104
  - MIT-LICENSE
105
105
  - README.md
106
106
  - app/assets/config/trail_guide/admin_manifest.js
107
+ - app/assets/images/trail_guide/trailguide.png
107
108
  - app/assets/javascripts/trail_guide/admin/application.js
108
109
  - app/assets/javascripts/trailguide.js
109
110
  - app/assets/stylesheets/trail_guide/admin/application.css
111
+ - app/assets/stylesheets/trail_guide/admin/experiments.css
110
112
  - app/controllers/trail_guide/admin/application_controller.rb
111
113
  - app/controllers/trail_guide/admin/experiments_controller.rb
112
114
  - app/controllers/trail_guide/experiments_controller.rb
115
+ - app/views/layouts/trail_guide/admin/_footer.erb
116
+ - app/views/layouts/trail_guide/admin/_header.erb
113
117
  - app/views/layouts/trail_guide/admin/application.html.erb
114
118
  - app/views/trail_guide/admin/experiments/_combined_experiment.html.erb
115
119
  - app/views/trail_guide/admin/experiments/_experiment.html.erb
116
120
  - app/views/trail_guide/admin/experiments/index.html.erb
121
+ - config/initializers/assets.rb
117
122
  - config/routes.rb
118
123
  - lib/trail_guide/adapters.rb
119
124
  - lib/trail_guide/adapters/participants.rb