vanity 1.4.0 → 1.5.0.beta

Sign up to get free protection for your applications and to get access to all the features.
@@ -18,7 +18,7 @@ module Vanity
18
18
  def initialize(options)
19
19
  @mongo = Mongo::Connection.new(options[:host], options[:port], :connect=>false)
20
20
  @options = options.clone
21
- @options[:database] ||= (@options[:path] && @options[:path].split("/")[1])
21
+ @options[:database] ||= (@options[:path] && @options[:path].split("/")[1]) || "vanity"
22
22
  connect!
23
23
  end
24
24
 
@@ -38,7 +38,7 @@ module Vanity
38
38
  end
39
39
 
40
40
  def connect!
41
- @mongo.connect_to_master
41
+ @mongo.connect
42
42
  database = @mongo.db(@options[:database])
43
43
  database.authenticate @options[:username], @options[:password], true if @options[:username]
44
44
  @metrics = database.collection("vanity.metrics")
@@ -49,7 +49,7 @@ module Vanity
49
49
 
50
50
  def to_s
51
51
  userinfo = @options.values_at(:username, :password).join(":") if @options[:username]
52
- URI::Generic.build(:scheme=>"mongo", :userinfo=>userinfo, :host=>@options[:host], :port=>@options[:port], :path=>"/#{@options[:database]}").to_s
52
+ URI::Generic.build(:scheme=>"mongodb", :userinfo=>userinfo, :host=>@options[:host], :port=>@options[:port], :path=>"/#{@options[:database]}").to_s
53
53
  end
54
54
 
55
55
  def flushdb
@@ -16,7 +16,7 @@ module Vanity
16
16
  class RedisAdapter < AbstractAdapter
17
17
  def initialize(options)
18
18
  @options = options.clone
19
- @options[:db] = @options[:database] || (@options[:path] && @options[:path].split("/")[1].to_i)
19
+ @options[:db] ||= @options[:database] || (@options[:path] && @options[:path].split("/")[1].to_i)
20
20
  @options[:thread_safe] = true
21
21
  connect!
22
22
  end
@@ -22,12 +22,16 @@ module Vanity
22
22
  end
23
23
 
24
24
  # Escape HTML.
25
- def h(html)
26
- CGI.escapeHTML(html)
25
+ def vanity_h(html)
26
+ CGI.escapeHTML(html.to_s)
27
+ end
28
+
29
+ def vanity_html_safe(text)
30
+ text
27
31
  end
28
32
 
29
33
  # Dumbed down from Rails' simple_format.
30
- def simple_format(text, options={})
34
+ def vanity_simple_format(text, options={})
31
35
  open = "<p #{options.map { |k,v| "#{k}=\"#{CGI.escapeHTML v}\"" }.join(" ")}>"
32
36
  text = open + text.gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
33
37
  gsub(/\n\n+/, "</p>\n\n#{open}"). # 2+ newline -> paragraph
@@ -81,7 +81,9 @@ module Vanity
81
81
  attr_reader :id
82
82
 
83
83
  # Time stamp when experiment was created.
84
- attr_reader :created_at
84
+ def created_at
85
+ @created_at ||= connection.get_experiment_created_at(@id)
86
+ end
85
87
 
86
88
  # Time stamp when experiment was completed.
87
89
  attr_reader :completed_at
@@ -166,7 +168,6 @@ module Vanity
166
168
  def save
167
169
  return unless @playground.collecting?
168
170
  connection.set_experiment_created_at @id, Time.now
169
- @created_at = connection.get_experiment_created_at(@id)
170
171
  end
171
172
 
172
173
  protected
@@ -1,5 +1,16 @@
1
1
  module Vanity
2
2
  module Rails #:nodoc:
3
+ def self.load!
4
+ Vanity.playground.load_path = ::Rails.root + Vanity.playground.load_path
5
+ Vanity.playground.logger ||= Rails.logger
6
+
7
+ # Do this at the very end of initialization, allowing you to change
8
+ # connection adapter, turn collection on/off, etc.
9
+ ::Rails.configuration.after_initialize do
10
+ Vanity.playground.load!
11
+ end
12
+ end
13
+
3
14
  # The use_vanity method will setup the controller to allow testing and
4
15
  # tracking of the current user.
5
16
  module UseVanity
@@ -7,7 +18,7 @@ module Vanity
7
18
  #
8
19
  # Call with the name of a method that returns an object whose identity
9
20
  # will be used as the Vanity identity. Confusing? Let's try by example:
10
- #
21
+ #
11
22
  # class ApplicationController < ActionController::Base
12
23
  # use_vanity :current_user
13
24
  #
@@ -15,7 +26,7 @@ module Vanity
15
26
  # User.find(session[:user_id])
16
27
  # end
17
28
  # end
18
- #
29
+ #
19
30
  # If that method (current_user in this example) returns nil, Vanity will
20
31
  # set the identity for you (using a cookie to remember it across
21
32
  # requests). It also uses this mechanism if you don't provide an
@@ -69,7 +80,7 @@ module Vanity
69
80
  # intercepted, the alternative is chosen, and the user redirected to the
70
81
  # same request URL sans _vanity parameter. This only works for GET
71
82
  # requests.
72
- #
83
+ #
73
84
  # For example, if the user requests the page
74
85
  # http://example.com/?_vanity=2907dac4de, the first alternative of the
75
86
  # :null_abc experiment is chosen and the user redirected to
@@ -80,7 +91,7 @@ module Vanity
80
91
  Vanity.playground.experiments.each do |id, experiment|
81
92
  if experiment.respond_to?(:alternatives)
82
93
  experiment.alternatives.each do |alt|
83
- if hash = hashes.delete(experiment.fingerprint(alt))
94
+ if hash = hashes.delete(experiment.fingerprint(alt))
84
95
  experiment.chooses alt.value
85
96
  break
86
97
  end
@@ -120,11 +131,11 @@ module Vanity
120
131
  # def index
121
132
  # render action: ab_test(:new_page)
122
133
  # end
123
- # @example A/B test inside ERB template (condition)
134
+ # @example A/B test inside ERB template (condition)
124
135
  # <%= if ab_test(:banner) %>100% less complexity!<% end %>
125
- # @example A/B test inside ERB template (value)
136
+ # @example A/B test inside ERB template (value)
126
137
  # <%= ab_test(:greeting) %> <%= current_user.name %>
127
- # @example A/B test inside ERB template (capture)
138
+ # @example A/B test inside ERB template (capture)
128
139
  # <% ab_test :features do |count| %>
129
140
  # <%= count %> features to choose from!
130
141
  # <% end %>
@@ -137,6 +148,22 @@ module Vanity
137
148
  value
138
149
  end
139
150
  end
151
+
152
+ def vanity_h(text)
153
+ h(text)
154
+ end
155
+
156
+ def vanity_html_safe(text)
157
+ if text.respond_to?(:html_safe!)
158
+ text.html_safe!
159
+ else
160
+ text
161
+ end
162
+ end
163
+
164
+ def vanity_simple_format(text, html_options={})
165
+ vanity_html_safe(simple_format(text, html_options))
166
+ end
140
167
  end
141
168
 
142
169
 
@@ -151,13 +178,13 @@ module Vanity
151
178
  # Step 3: Open your browser to http://localhost:3000/vanity
152
179
  module Dashboard
153
180
  def index
154
- render :template=>Vanity.template("_report"), :content_type=>Mime::HTML, :layout=>true
181
+ render :file=>Vanity.template("_report"), :content_type=>Mime::HTML, :layout=>false
155
182
  end
156
183
 
157
184
  def chooses
158
185
  exp = Vanity.playground.experiment(params[:e])
159
186
  exp.chooses(exp.alternatives[params[:a].to_i].value)
160
- render :partial=>Vanity.template("experiment"), :locals=>{ :experiment=>exp }
187
+ render :file=>Vanity.template("_experiment"), :locals=>{:experiment=>exp}
161
188
  end
162
189
  end
163
190
  end
@@ -179,7 +206,7 @@ if defined?(ActionController)
179
206
  # Sets Vanity.context to the current controller, so you can do things like:
180
207
  # experiment(:simple).chooses(:green)
181
208
  def setup_controller_request_and_response
182
- setup_controller_request_and_response_without_vanity
209
+ setup_controller_request_and_response_without_vanity
183
210
  Vanity.context = @controller
184
211
  end
185
212
  end
@@ -187,18 +214,17 @@ if defined?(ActionController)
187
214
  end
188
215
 
189
216
 
190
- # Automatically configure Vanity. Uses the
217
+ # Automatically configure Vanity.
191
218
  if defined?(Rails)
192
- Rails.configuration.after_initialize do
193
- # Use Rails logger by default.
194
- Vanity.playground.logger ||= Rails.logger
195
- Vanity.playground.load_path = Rails.root + Vanity.playground.load_path
196
- Vanity.playground.collecting = Rails.env.production?
197
-
198
- # Do this at the very end of initialization, allowing you to change
199
- # connection adapter, turn collection on/off, etc.
219
+ if Rails.const_defined?(:Railtie) # Rails 3
220
+ class Plugin < Rails::Railtie # :nodoc:
221
+ initializer "vanity.require" do |app|
222
+ Vanity::Rails.load!
223
+ end
224
+ end
225
+ else
200
226
  Rails.configuration.after_initialize do
201
- Vanity.playground.load!
227
+ Vanity::Rails.load!
202
228
  end
203
229
  end
204
230
  end
@@ -206,13 +232,13 @@ end
206
232
 
207
233
  # Reconnect whenever we fork under Passenger.
208
234
  if defined?(PhusionPassenger)
209
- PhusionPassenger.on_event(:starting_worker_process) do |forked|
210
- if forked
235
+ PhusionPassenger.on_event(:starting_worker_process) do |forked|
236
+ if forked
211
237
  begin
212
238
  Vanity.playground.establish_connection if Vanity.playground.collecting?
213
239
  rescue Exception=>ex
214
- Rails.logger.error "Error reconnecting: #{ex.to_s}"
240
+ Rails.logger.error "Error reconnecting: #{ex.to_s}"
215
241
  end
216
- end
217
- end
242
+ end
243
+ end
218
244
  end
@@ -57,7 +57,7 @@ module Vanity
57
57
  def values(sdate, edate)
58
58
  query = { :conditions=>{ @ar_timestamp=>(sdate.to_time...(edate + 1).to_time) },
59
59
  :group=>"date(#{@ar_scoped.connection.quote_column_name @ar_timestamp})" }
60
- grouped = @ar_column ? @ar_scoped.calculate(@ar_aggregate, @ar_column, query) : @ar_scoped.count(query)
60
+ grouped = @ar_column ? @ar_scoped.send(@ar_aggregate, @ar_column, query) : @ar_scoped.count(query)
61
61
  (sdate..edate).inject([]) { |ordered, date| ordered << (grouped[date.to_s] || 0) }
62
62
  end
63
63
 
@@ -9,7 +9,7 @@ module Vanity
9
9
  # puts Vanity.playground.map(&:name)
10
10
  class Playground
11
11
 
12
- DEFAULTS = { :load_path=>"experiments" }
12
+ DEFAULTS = { :collecting => true, :load_path=>"experiments" }
13
13
 
14
14
  # Created new Playground. Unless you need to, use the global
15
15
  # Vanity.playground.
@@ -40,7 +40,7 @@ module Vanity
40
40
  @logger.level = Logger::ERROR
41
41
  end
42
42
  @loading = []
43
- @collecting = true
43
+ @collecting = @options[:collecting]
44
44
  end
45
45
 
46
46
  # Deprecated. Use redis.server instead.
@@ -141,7 +141,7 @@ module Vanity
141
141
  Dir[File.join(load_path, "metrics/*.rb")].each do |file|
142
142
  Metric.load self, @loading, file
143
143
  end
144
- if File.exist?("config/vanity.yml") && remote = YAML.load_file("config/vanity.yml")["metrics"]
144
+ if File.exist?("config/vanity.yml") && remote = YAML.load(ERB.new(File.read("config/vanity.yml")).result)["metrics"]
145
145
  remote.each do |id, url|
146
146
  fail "Metric #{id} already defined in playground" if metrics[id.to_sym]
147
147
  metric = Metric.new(self, id)
@@ -195,19 +195,19 @@ module Vanity
195
195
  when nil
196
196
  if File.exists?("config/vanity.yml")
197
197
  env = ENV["RACK_ENV"] || ENV["RAILS_ENV"] || "development"
198
- spec = YAML.load_file("config/vanity.yml")[env]
198
+ spec = YAML.load(ERB.new(File.read("config/vanity.yml")).result)[env]
199
199
  fail "No configuration for #{env}" unless spec
200
200
  establish_connection spec
201
201
  elsif File.exists?("config/redis.yml")
202
202
  env = ENV["RACK_ENV"] || ENV["RAILS_ENV"] || "development"
203
- redis = YAML.load_file("config/redis.yml")[env]
203
+ redis = YAML.load(ERB.new(File.read("config/redis.yml")).result)[env]
204
204
  fail "No configuration for #{env}" unless redis
205
205
  establish_connection "redis://" + redis
206
206
  else
207
207
  establish_connection :adapter=>"redis"
208
208
  end
209
209
  when Symbol
210
- spec = YAML.load_file("config/vanity.yml")[spec.to_s]
210
+ spec = YAML.load(ERB.new(File.read("config/vanity.yml")).result)[spec.to_s]
211
211
  establish_connection spec
212
212
  when String
213
213
  uri = URI.parse(spec)
@@ -283,7 +283,9 @@ module Vanity
283
283
 
284
284
  end
285
285
 
286
- @playground = Playground.new
286
+ # In the case of Rails, use the Rails logger and collect only for
287
+ # production environment by default.
288
+ @playground = Playground.new(defined?(Rails) ? { :logger => Rails.logger, :collecting => Rails.env.production? } : {})
287
289
  class << self
288
290
 
289
291
  # The playground instance.
@@ -5,7 +5,7 @@
5
5
  <% score.alts.each do |alt| %>
6
6
  <tr class="<%= "choice" if score.choice == alt %>">
7
7
  <td class="option"><%= alt.name.gsub(/^o/, "O") %>:</td>
8
- <td class="value"><code><%=h alt.value.to_s %></code></td>
8
+ <td class="value"><code><%=vanity_h alt.value.to_s %></code></td>
9
9
  <td>
10
10
  <%= "%.1f%%" % [alt.conversion_rate * 100] %>
11
11
  <%= "(%d%% better than %s)" % [alt.difference, score.least.name] if alt.difference && alt.difference >= 1 %>
@@ -1,5 +1,5 @@
1
- <h3><%=h experiment.name %> <span class="type">(<%= experiment.class.friendly_name %>)</span></h3>
2
- <%= experiment.description.to_s.split(/\n\s*\n/).map { |para| %{<p class="description">#{h para}</p>} }.join %>
1
+ <h3><%=vanity_h experiment.name %> <span class="type">(<%= experiment.class.friendly_name %>)</span></h3>
2
+ <%= experiment.description.to_s.split(/\n\s*\n/).map { |para| vanity_html_safe(%{<p class="description">#{vanity_h para}</p>}) }.join %>
3
3
  <%= render Vanity.template(experiment.type), :experiment=>experiment %>
4
4
  <p class="meta">Started <%= experiment.created_at.strftime("%a, %b %d") %>
5
5
  <%= " | Completed #{experiment.completed_at.strftime("%a, %b %d")}" unless experiment.active? %></p>
@@ -1,6 +1,6 @@
1
1
  <ul class="experiments">
2
2
  <% experiments.sort_by { |id, experiment| experiment.created_at }.reverse.each do |id, experiment| %>
3
- <li class="experiment <%= experiment.type %>" id="experiment_<%=h id.to_s %>">
3
+ <li class="experiment <%= experiment.type %>" id="experiment_<%=vanity_h id.to_s %>">
4
4
  <%= render Vanity.template("experiment"), :id=>id, :experiment=>experiment %>
5
5
  </li>
6
6
  <% end %>
@@ -1,14 +1,14 @@
1
- <h3><%=h metric.name %></h3>
2
- <%= simple_format h(Vanity::Metric.description(metric).to_s), :class=>"description" %>
1
+ <h3><%=vanity_h metric.name %></h3>
2
+ <%= vanity_simple_format vanity_h(Vanity::Metric.description(metric).to_s), :class=>"description" %>
3
3
  <%=
4
4
  begin
5
5
  data = Vanity::Metric.data(metric)
6
6
  min, max = data.map(&:last).minmax
7
7
  js = data.map { |date,value| "['#{date.to_time.httpdate}',#{value}]" }.join(",")
8
- %{<div class="chart"></div>
8
+ vanity_html_safe(%{<div class="chart"></div>
9
9
  <script type="text/javascript">
10
- $(function(){Vanity.metric("#{h id.to_s}").plot([{label:"#{h metric.name}", data: [#{js}]}])})
11
- </script>}
10
+ $(function(){Vanity.metric("#{vanity_h id.to_s}").plot([{label:"#{vanity_h metric.name}", data: [#{js}]}])})
11
+ </script>})
12
12
  rescue Exception=>ex
13
- %{<div class="error">#{h ex.message}</div>}
13
+ %{<div class="error">#{vanity_h ex.message}</div>}
14
14
  end %>
@@ -1,13 +1,14 @@
1
1
  <ul class="metrics">
2
2
  <% metrics.sort_by { |id, metric| metric.name }.each do |id, metric| %>
3
3
  <li class="metric" id="metric_<%= id %>">
4
- <%= render Vanity.template("metric"), :id=>id, :metric=>metric %>
4
+ <%= render :file=>Vanity.template("_metric"), :locals=>{:id=>id, :metric=>metric} %>
5
+ <%= render :file=>Vanity.template("_metric"), :locals=>{:id=>id, :metric=>metric} %>
5
6
  </li>
6
7
  <% end %>
7
8
  </ul>
8
9
  <form id="milestones">
9
10
  <% experiments.each do |id, experiment| %>
10
11
  <label><input type="checkbox" name="milestone" data-start="<%= experiment.created_at.httpdate %>"
11
- data-end="<%= (experiment.completed_at || Time.now).httpdate %>"><%=h experiment.name %></label>
12
+ data-end="<%= (experiment.completed_at || Time.now).httpdate %>"><%=vanity_h experiment.name %></label>
12
13
  <% end %>
13
14
  </form>
@@ -4,22 +4,22 @@
4
4
  <style>
5
5
  .vanity { margin: 2em auto; width: 40em; font-family: "Helvetica Neue", "Helvetica", "Verdana", sans-serif }
6
6
  .vanity h1 { margin: 1em 0; border-bottom: 3px solid #ccc }
7
- <%= File.read(Vanity.template("vanity.css")) %>
7
+ <%= vanity_html_safe(File.read(Vanity.template("vanity.css"))) %>
8
8
  </style>
9
- <script type="text/javascript"><%= File.read(Vanity.template("jquery.min.js")) %></script>
10
- <script type="text/javascript"><%= File.read(Vanity.template("flot.min.js")) %></script>
11
- <script type="text/javascript"><%= File.read(Vanity.template("vanity.js")) %></script>
9
+ <script type="text/javascript"><%= vanity_html_safe(File.read(Vanity.template("jquery.min.js"))) %></script>
10
+ <script type="text/javascript"><%= vanity_html_safe(File.read(Vanity.template("flot.min.js"))) %></script>
11
+ <script type="text/javascript"><%= vanity_html_safe(File.read(Vanity.template("vanity.js"))) %></script>
12
12
  <% if respond_to?(:form_authenticity_token) %><script type="text/javascript">document.auth_token = "<%= form_authenticity_token %>"</script><% end %>
13
13
  </head>
14
14
  <body>
15
15
  <div class="vanity">
16
16
  <% experiments = Vanity.playground.experiments ; unless experiments.empty? %>
17
17
  <h2>Experiments</h2>
18
- <%= render Vanity.template("experiments"), :experiments=>experiments %>
18
+ <%= render :file=>Vanity.template("_experiments"), :locals=>{:experiments=>experiments} %>
19
19
  <% end %>
20
20
  <% metrics = Vanity.playground.metrics ; unless metrics.empty? %>
21
21
  <h2>Metrics</h2>
22
- <%= render Vanity.template("metrics"), :metrics=>metrics, :experiments=>experiments %>
22
+ <%= render :file=>Vanity.template("_metrics"), :locals=>{:metrics=>metrics, :experiments=>experiments} %>
23
23
  <% end %>
24
24
  <p class="footer">Generated by <a href="http://vanity.labnotes.org">Vanity</a></p>
25
25
  </div>
@@ -0,0 +1,11 @@
1
+ module Vanity
2
+ VERSION = "1.5.0.beta"
3
+
4
+ module Version
5
+ version = VERSION.to_s.split(".").map { |i| i.to_i }
6
+ MAJOR = version[0]
7
+ MINOR = version[1]
8
+ PATCH = version[2]
9
+ STRING = "#{MAJOR}.#{MINOR}.#{PATCH}"
10
+ end
11
+ end
@@ -42,7 +42,6 @@ context "ActiveRecord Metric" do
42
42
  end
43
43
 
44
44
  test "record average" do
45
- Sky.aggregates
46
45
  File.open "tmp/experiments/metrics/sky_is_limit.rb", "w" do |f|
47
46
  f.write <<-RUBY
48
47
  metric "Sky is limit" do
@@ -51,13 +50,12 @@ context "ActiveRecord Metric" do
51
50
  RUBY
52
51
  end
53
52
  Vanity.playground.metrics
54
- Sky.create! :height=>4
53
+ Sky.create! :height=>8
55
54
  Sky.create! :height=>2
56
- assert_equal 3, Vanity::Metric.data(metric(:sky_is_limit)).last.last
55
+ assert_equal 5, Vanity::Metric.data(metric(:sky_is_limit)).last.last
57
56
  end
58
57
 
59
58
  test "record minimum" do
60
- Sky.aggregates
61
59
  File.open "tmp/experiments/metrics/sky_is_limit.rb", "w" do |f|
62
60
  f.write <<-RUBY
63
61
  metric "Sky is limit" do
@@ -72,7 +70,6 @@ context "ActiveRecord Metric" do
72
70
  end
73
71
 
74
72
  test "record maximum" do
75
- Sky.aggregates
76
73
  File.open "tmp/experiments/metrics/sky_is_limit.rb", "w" do |f|
77
74
  f.write <<-RUBY
78
75
  metric "Sky is limit" do
@@ -108,7 +105,6 @@ context "ActiveRecord Metric" do
108
105
  end
109
106
 
110
107
  test "with scope" do
111
- Sky.aggregates
112
108
  File.open "tmp/experiments/metrics/sky_is_limit.rb", "w" do |f|
113
109
  f.write <<-RUBY
114
110
  metric "Sky is limit" do