vanity 1.1.1 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,3 +1,34 @@
1
+ == 1.2.0 (2009-12-14)
2
+ This release introduces metrics backed by ActiveRecord. Use them when your model is already tracking a metric, and you get instant historical data.
3
+
4
+ Example, track sign ups using User model:
5
+
6
+ metric "Signups" do
7
+ model Account
8
+ end
9
+
10
+ Example, track satisfaction using Survey model:
11
+ metric "Satisfaction" do
12
+ model Survey, :average=>:rating
13
+ end
14
+
15
+ Example, track only high ratings:
16
+ metric "High ratings" do
17
+ model Rating, :conditions=>["stars >= 4"]
18
+ end
19
+
20
+ There's no need to call track! on these metrics.
21
+
22
+ * Added: Metrics backed by ActiveRecord.
23
+ * Added: track! and ab_test methods now available from Object (i.e. everywhere).
24
+ * Added: Playground.load!. Now loading all metrics and experiments from Rails initializer.
25
+ * Changed: Decoupled metric name from identifier. You can now define a metric with more descriptive name, e.g. "Cheers per second (user satisfaction)" and keep their ID simple. Identifier is matched against the file name (for metrics loaded from experiments/metrics).
26
+ * Changed: Metrics no longer defined on-demand, i.e. calling playground.metric either returns existing metric or raises exception.
27
+ * Changed: Playground.experiments returns hash instead of array.
28
+ * Changed: All dates in report are UTC, since we don't know which locale to use.
29
+ * Removed: Object.experiment is deprecated, please call Vanity.playground.experiment directly.
30
+ * Fixed: Playground no longer changes logging level on supplied logger.
31
+
1
32
  == 1.1.1 (2009-12-4)
2
33
  * Fixed: Binding issue that shows up on 1.8.6/7.
3
34
 
@@ -1,3 +1,5 @@
1
+ require "digest/md5"
2
+
1
3
  module Vanity
2
4
  module Experiment
3
5
 
@@ -400,12 +402,14 @@ module Vanity
400
402
  # Called when tracking associated metric.
401
403
  def track!(metric_id, timestamp, count, *args)
402
404
  return unless active?
403
- identity = identity()
404
- return if redis[key("participants:#{identity}:show")]
405
- index = alternative_for(identity)
406
- redis.sadd key("alts:#{index}:converted"), identity if redis.sismember(key("alts:#{index}:participants"), identity)
407
- redis.incrby key("alts:#{index}:conversions"), count
408
- check_completion!
405
+ identity = identity() rescue nil
406
+ if identity
407
+ return if redis[key("participants:#{identity}:show")]
408
+ index = alternative_for(identity)
409
+ redis.sadd key("alts:#{index}:converted"), identity if redis.sismember(key("alts:#{index}:participants"), identity)
410
+ redis.incrby key("alts:#{index}:conversions"), count
411
+ check_completion!
412
+ end
409
413
  end
410
414
 
411
415
  # If you are not embarrassed by the first version of your product, you’ve
@@ -70,6 +70,7 @@ module Vanity
70
70
  # Human readable experiment name (first argument you pass when creating a
71
71
  # new experiment).
72
72
  attr_reader :name
73
+ alias :to_s :name
73
74
 
74
75
  # Unique identifier, derived from name experiment name, e.g. "Green
75
76
  # Button" becomes :green_button.
@@ -104,7 +105,7 @@ module Vanity
104
105
  end
105
106
 
106
107
  def identity
107
- @identify_block.call(Vanity.context) or fail "No identity found"
108
+ @identify_block.call(Vanity.context)
108
109
  end
109
110
  protected :identity
110
111
 
@@ -122,10 +123,6 @@ module Vanity
122
123
  @description
123
124
  end
124
125
 
125
- def report
126
- fail "Implement me"
127
- end
128
-
129
126
 
130
127
  # -- Experiment completion --
131
128
 
@@ -0,0 +1,59 @@
1
+ module Vanity
2
+ # Helper methods available on Object.
3
+ #
4
+ # @example From ERB template
5
+ # <%= ab_test(:greeting) %> <%= current_user.name %>
6
+ # @example From Rails controller
7
+ # class AccountController < ApplicationController
8
+ # def create
9
+ # track! :signup
10
+ # Acccount.create! params[:account]
11
+ # end
12
+ # end
13
+ # @example From ActiveRecord
14
+ # class Posts < ActiveRecord::Base
15
+ # after_create do |post|
16
+ # track! :images if post.type == :image
17
+ # end
18
+ # end
19
+ module Helpers
20
+
21
+ # This method returns one of the alternative values in the named A/B test.
22
+ #
23
+ # @example A/B two alternatives for a page
24
+ # def index
25
+ # if ab_test(:new_page) # true/false test
26
+ # render action: "new_page"
27
+ # else
28
+ # render action: "index"
29
+ # end
30
+ # end
31
+ # @example Similar, alternative value is page name
32
+ # def index
33
+ # render action: ab_test(:new_page)
34
+ # end
35
+ # @since 1.2.0
36
+ def ab_test(name, &block)
37
+ value = Vanity.playground.experiment(name).choose
38
+ if block
39
+ content = capture(value, &block)
40
+ block_called_from_erb?(block) ? concat(content) : content
41
+ else
42
+ value
43
+ end
44
+ end
45
+
46
+ # Tracks an action associated with a metric.
47
+ #
48
+ # @example
49
+ # track! :invitation
50
+ # @since 1.2.0
51
+ def track!(name, count = 1)
52
+ Vanity.playground.track! name, count
53
+ end
54
+ end
55
+ end
56
+
57
+ Object.class_eval do
58
+ include Vanity::Helpers
59
+ end
data/lib/vanity/metric.rb CHANGED
@@ -25,9 +25,11 @@ module Vanity
25
25
 
26
26
  # Defines a new metric, using the class Vanity::Metric.
27
27
  def metric(name, &block)
28
- metric = Metric.new(playground, name.to_s, name.to_s.downcase.gsub(/\W/, "_"))
28
+ id = File.basename(caller.first.split(":").first, ".rb").downcase.gsub(/\W/, "_").to_sym
29
+ fail "Metric #{id} already defined in playground" if playground.metrics[id]
30
+ metric = Metric.new(playground, name.to_s, id)
29
31
  metric.instance_eval &block
30
- metric
32
+ playground.metrics[id] = metric
31
33
  end
32
34
 
33
35
  def binding_with(playground)
@@ -81,26 +83,26 @@ module Vanity
81
83
  # @example These are all equivalent:
82
84
  # Vanity::Metric.data(my_metric)
83
85
  # Vanity::Metric.data(my_metric, 90)
84
- # Vanity::Metric.data(my_metric, Date.today - 90)
85
- # Vanity::Metric.data(my_metric, Date.today - 90, Date.today)
86
+ # Vanity::Metric.data(my_metric, Date.today - 89)
87
+ # Vanity::Metric.data(my_metric, Date.today - 89, Date.today)
86
88
  def data(metric, *args)
87
89
  first = args.shift || 90
88
90
  to = args.shift || Date.today
89
- from = first.respond_to?(:to_date) ? first.to_date : to - first
91
+ from = first.respond_to?(:to_date) ? first.to_date : to - (first - 1)
90
92
  (from..to).zip(metric.values(from, to))
91
93
  end
92
94
 
93
95
  # Playground uses this to load metric definitions.
94
- def load(playground, stack, path, id)
95
- fn = File.join(path, "#{id}.rb")
96
- fail "Circular dependency detected: #{stack.join('=>')}=>#{fn}" if stack.include?(fn)
97
- source = File.read(fn)
98
- stack.push fn
96
+ def load(playground, stack, file)
97
+ fail "Circular dependency detected: #{stack.join('=>')}=>#{file}" if stack.include?(file)
98
+ source = File.read(file)
99
+ stack.push file
100
+ id = File.basename(file, ".rb").downcase.gsub(/\W/, "_").to_sym
99
101
  context = Object.new
100
102
  context.instance_eval do
101
103
  extend Definition
102
- metric = eval(source, context.binding_with(playground), fn)
103
- fail NameError.new("Expected #{fn} to define metric #{id}", id) unless metric.name.downcase.gsub(/\W+/, '_').to_sym == id
104
+ metric = eval(source, context.binding_with(playground), file)
105
+ fail NameError.new("Expected #{file} to define metric #{id}", id) unless playground.metrics[id]
104
106
  metric
105
107
  end
106
108
  rescue
@@ -117,8 +119,8 @@ module Vanity
117
119
  # Takes playground (need this to access Redis), friendly name and optional
118
120
  # id (can infer from name).
119
121
  def initialize(playground, name, id = nil)
120
- id ||= name.to_s.downcase.gsub(/\W+/, '_')
121
- @playground, @name, @id = playground, name.to_s, id.to_sym
122
+ @playground, @name = playground, name.to_s
123
+ @id = (id || name.to_s.downcase.gsub(/\W+/, '_')).to_sym
122
124
  @hooks = []
123
125
  redis.setnx key(:created_at), Time.now.to_i
124
126
  @created_at = Time.at(redis[key(:created_at)].to_i)
@@ -129,13 +131,12 @@ module Vanity
129
131
 
130
132
  # Called to track an action associated with this metric.
131
133
  def track!(count = 1)
132
- timestamp = Time.now
134
+ count ||= 1
133
135
  if count > 0
136
+ timestamp = Time.now
134
137
  redis.incrby key(timestamp.to_date, "count"), count
135
138
  @playground.logger.info "vanity: #{@id} with count #{count}"
136
- @hooks.each do |hook|
137
- hook.call @id, timestamp, count
138
- end
139
+ call_hooks timestamp, count
139
140
  end
140
141
  end
141
142
 
@@ -169,9 +170,8 @@ module Vanity
169
170
  # -- Reporting --
170
171
 
171
172
  # Human readable metric name. All metrics must implement this method.
172
- def name
173
- @name
174
- end
173
+ attr_reader :name
174
+ alias :to_s :name
175
175
 
176
176
  # Time stamp when metric was created.
177
177
  attr_reader :created_at
@@ -197,6 +197,70 @@ module Vanity
197
197
  end
198
198
 
199
199
 
200
+ # -- ActiveRecord support --
201
+
202
+ AGGREGATES = [:average, :minimum, :maximum, :sum]
203
+
204
+ # Use an ActiveRecord model to get metric data from database table. Also
205
+ # forwards @after_create@ callbacks to hooks (updating experiments).
206
+ #
207
+ # Supported options:
208
+ # :conditions -- Only select records that match this condition
209
+ # :average -- Metric value is average of this column
210
+ # :minimum -- Metric value is minimum of this column
211
+ # :maximum -- Metric value is maximum of this column
212
+ # :sum -- Metric value is sum of this column
213
+ # :timestamp -- Use this column to filter/group records (defaults to
214
+ # +created_at+)
215
+ #
216
+ # @example Track sign ups using User model
217
+ # metric "Signups" do
218
+ # model Account
219
+ # end
220
+ # @example Track satisfaction using Survey model
221
+ # metric "Satisfaction" do
222
+ # model Survey, :average=>:rating
223
+ # end
224
+ # @example Track only high ratings
225
+ # metric "High ratings" do
226
+ # model Rating, :conditions=>["stars >= 4"]
227
+ # end
228
+ # @example Track only high ratings (using scope)
229
+ # metric "High ratings" do
230
+ # model Rating.high
231
+ # end
232
+ #
233
+ # @since 1.2.0
234
+ def model(class_or_scope, options = nil)
235
+ options = (options || {}).clone
236
+ conditions = options.delete(:conditions)
237
+ scoped = conditions ? class_or_scope.scoped(:conditions=>conditions) : class_or_scope
238
+ aggregate = AGGREGATES.find { |key| options.has_key?(key) }
239
+ column = options.delete(aggregate)
240
+ fail "Cannot use multiple aggregates in a single metric" if AGGREGATES.find { |key| options.has_key?(key) }
241
+ timestamp = options.delete(:timestamp) || :created_at
242
+ fail "Unrecognized options: #{options.keys * ", "}" unless options.empty?
243
+
244
+ # Hook into model's after_create
245
+ scoped.after_create do |record|
246
+ count = column ? (record.send(column) || 0) : 1
247
+ call_hooks record.send(timestamp), count if count > 0 && scoped.exists?(record)
248
+ end
249
+ # Redefine values method to perform query
250
+ eigenclass = class << self ; self ; end
251
+ eigenclass.send :define_method, :values do |sdate, edate|
252
+ query = { :conditions=>{ timestamp=>(sdate.to_time...(edate + 1).to_time) }, :group=>"date(#{scoped.connection.quote_column_name timestamp})" }
253
+ grouped = column ? scoped.calculate(aggregate, column, query) : scoped.count(query)
254
+ (sdate..edate).inject([]) { |ordered, date| ordered << (grouped[date.to_s] || 0) }
255
+ end
256
+ # Redefine track! method to call on hooks
257
+ eigenclass.send :define_method, :track! do |*args|
258
+ count = args.first || 1
259
+ call_hooks Time.now, count if count > 0
260
+ end
261
+ end
262
+
263
+
200
264
  # -- Storage --
201
265
 
202
266
  def destroy!
@@ -211,5 +275,12 @@ module Vanity
211
275
  "metrics:#{@id}:#{args.join(':')}"
212
276
  end
213
277
 
278
+ def call_hooks(timestamp, count)
279
+ count ||= 1
280
+ @hooks.each do |hook|
281
+ hook.call @id, timestamp, count
282
+ end
283
+ end
284
+
214
285
  end
215
286
  end
@@ -13,10 +13,12 @@ module Vanity
13
13
  def initialize(options = {})
14
14
  @host, @port, @db, @load_path = DEFAULTS.merge(options).values_at(:host, :port, :db, :load_path)
15
15
  @namespace = "vanity:#{Vanity::Version::MAJOR}"
16
- @logger = options[:logger] || Logger.new(STDOUT)
17
- @logger.level = Logger::ERROR
16
+ @logger = options[:logger]
17
+ unless @logger
18
+ @logger = Logger.new(STDOUT)
19
+ @logger.level = Logger::ERROR
20
+ end
18
21
  @redis = options[:redis]
19
- @experiments = {}
20
22
  @loading = []
21
23
  end
22
24
 
@@ -43,37 +45,56 @@ module Vanity
43
45
 
44
46
  # Defines a new experiment. Generally, do not call this directly,
45
47
  # use one of the definition methods (ab_test, measure, etc).
48
+ #
49
+ # @see Vanity::Experiment
46
50
  def define(name, type, options = {}, &block)
47
51
  id = name.to_s.downcase.gsub(/\W/, "_").to_sym
48
- raise "Experiment #{id} already defined once" if @experiments[id]
52
+ raise "Experiment #{id} already defined once" if experiments[id]
49
53
  klass = Experiment.const_get(type.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase })
50
54
  experiment = klass.new(self, id, name, options)
51
55
  experiment.instance_eval &block
52
56
  experiment.save
53
- @experiments[id] = experiment
57
+ experiments[id] = experiment
54
58
  end
55
59
 
56
60
  # Returns the experiment. You may not have guessed, but this method raises
57
61
  # an exception if it cannot load the experiment's definition.
62
+ #
63
+ # @see Vanity::Experiment
58
64
  def experiment(name)
59
65
  id = name.to_s.downcase.gsub(/\W/, "_").to_sym
60
66
  warn "Deprecated: pleae call experiment method with experiment identifier (a Ruby symbol)" unless id == name
61
- @experiments[id] ||= Experiment::Base.load(self, @loading, File.expand_path(load_path), id)
67
+ experiments[id] ||= Experiment::Base.load(self, @loading, File.expand_path(load_path), id)
62
68
  end
63
69
 
64
- # Returns list of all loaded experiments.
70
+ # Returns hash of experiments (key is experiment id).
71
+ #
72
+ # @see Vanity::Experiment
65
73
  def experiments
66
- Dir[File.join(load_path, "*.rb")].each do |file|
67
- id = File.basename(file).gsub(/.rb$/, "")
68
- experiment id.to_sym
74
+ unless @experiments
75
+ @experiments = {}
76
+ @logger.info "Vanity: loading experiments from #{load_path}"
77
+ Dir[File.join(load_path, "*.rb")].each do |file|
78
+ id = File.basename(file).gsub(/.rb$/, "")
79
+ experiment id.to_sym
80
+ end
69
81
  end
70
- @experiments.values
82
+ @experiments
71
83
  end
72
84
 
73
- # Reloads all experiments.
85
+ # Reloads all metrics and experiments. Rails calls this for each request in
86
+ # development mode.
74
87
  def reload!
75
- @experiments.clear
88
+ @experiments = nil
76
89
  @metrics = nil
90
+ load!
91
+ end
92
+
93
+ # Loads all metrics and experiments. Rails calls this during
94
+ # initialization.
95
+ def load!
96
+ experiments
97
+ metrics
77
98
  end
78
99
 
79
100
  # Use this instance to access the Redis database.
@@ -95,27 +116,24 @@ module Vanity
95
116
  @redis ||= MockRedis.new
96
117
  end
97
118
 
98
- # Returns a metric (creating one if doesn't already exist).
119
+ # Returns a metric (raises NameError if no metric with that identifier).
99
120
  #
121
+ # @see Vanity::Metric
100
122
  # @since 1.1.0
101
123
  def metric(id)
102
- id = id.to_sym
103
- metrics[id] ||= Metric.load(self, @loading, File.expand_path("metrics", load_path), id)
124
+ metrics[id.to_sym] or raise NameError, "No metric #{id}"
104
125
  end
105
126
 
106
127
  # Returns hash of metrics (key is metric id).
107
128
  #
129
+ # @see Vanity::Metric
108
130
  # @since 1.1.0
109
131
  def metrics
110
132
  unless @metrics
111
133
  @metrics = {}
134
+ @logger.info "Vanity: loading metrics from #{load_path}/metrics"
112
135
  Dir[File.join(load_path, "metrics/*.rb")].each do |file|
113
- begin
114
- id = File.basename(file).gsub(/.rb$/, "")
115
- metric id
116
- rescue NameError
117
- @logger.error "Could not load metric #{$!.name}: #{$!}"
118
- end
136
+ Metric.load self, @loading, file
119
137
  end
120
138
  end
121
139
  @metrics
@@ -171,7 +189,9 @@ class Object
171
189
  # puts experiment(:text_size).alternatives
172
190
  #
173
191
  # @see Vanity::Playground#experiment
192
+ # @deprecated
174
193
  def experiment(name)
194
+ warn "Deprecated. Please call Vanity.playground.experiment directly."
175
195
  Vanity.playground.experiment(name)
176
196
  end
177
197
  end
@@ -50,7 +50,7 @@ module Vanity
50
50
  elsif symbol && object = send(symbol)
51
51
  @vanity_identity = object.id
52
52
  elsif response # everyday use
53
- @vanity_identity = cookies["vanity_id"] || OpenSSL::Random.random_bytes(16).unpack("H*")[0]
53
+ @vanity_identity = cookies["vanity_id"] || ActiveSupport::SecureRandom.hex(16)
54
54
  cookies["vanity_id"] = { :value=>@vanity_identity, :expires=>1.month.from_now }
55
55
  @vanity_identity
56
56
  else # during functional testing
@@ -67,7 +67,7 @@ module Vanity
67
67
 
68
68
  # This method returns one of the alternative values in the named A/B test.
69
69
  #
70
- # Examples using ab_test inside controller:
70
+ # @example A/B two alternatives for a page
71
71
  # def index
72
72
  # if ab_test(:new_page) # true/false test
73
73
  # render action: "new_page"
@@ -75,16 +75,15 @@ module Vanity
75
75
  # render action: "index"
76
76
  # end
77
77
  # end
78
- #
78
+ # @example Similar, alternative value is page name
79
79
  # def index
80
- # render action: ab_test(:new_page) # alternatives are page names
80
+ # render action: ab_test(:new_page)
81
81
  # end
82
- #
83
- # Examples using ab_test inside view:
82
+ # @example A/B test inside ERB template (condition)
84
83
  # <%= if ab_test(:banner) %>100% less complexity!<% end %>
85
- #
84
+ # @example A/B test inside ERB template (value)
86
85
  # <%= ab_test(:greeting) %> <%= current_user.name %>
87
- #
86
+ # @example A/B test inside ERB template (capture)
88
87
  # <% ab_test :features do |count| %>
89
88
  # <%= count %> features to choose from!
90
89
  # <% end %>
@@ -98,13 +97,5 @@ module Vanity
98
97
  end
99
98
  end
100
99
 
101
- # This method records conversion on the named A/B test. For example:
102
- # def create
103
- # track! :call_to_action
104
- # Acccount.create! params[:account]
105
- # end
106
- def track!(name)
107
- Vanity.playground.track! name
108
- end
109
100
  end
110
101
  end
data/lib/vanity/rails.rb CHANGED
@@ -1,15 +1,22 @@
1
- require "vanity"
2
1
  require "vanity/rails/helpers"
3
2
  require "vanity/rails/testing"
4
3
  require "vanity/rails/dashboard"
5
4
 
6
- # Use Rails logger by default.
7
- Vanity.playground.logger ||= ActionController::Base.logger
8
- Vanity.playground.load_path = "#{RAILS_ROOT}/experiments"
9
-
10
5
  # Include in controller, add view helper methods.
11
6
  ActionController::Base.class_eval do
12
7
  extend Vanity::Rails::ClassMethods
13
8
  include Vanity::Rails
14
9
  helper Vanity::Rails
15
10
  end
11
+
12
+ Rails.configuration.after_initialize do
13
+ # Use Rails logger by default.
14
+ Vanity.playground.logger ||= ActionController::Base.logger
15
+ Vanity.playground.load_path = "#{RAILS_ROOT}/experiments"
16
+
17
+ # Do this at the very end of initialization, allowing test environment to do
18
+ # Vanity.playground.mock! before any database access takes place.
19
+ Rails.configuration.after_initialize do
20
+ Vanity.playground.load!
21
+ end
22
+ end
@@ -1,7 +1,7 @@
1
1
  <ul class="experiments">
2
- <% experiments.sort_by(&:created_at).reverse.each do |experiment| %>
3
- <li class="experiment <%= experiment.type %>" id="experiment_<%=h experiment.id.to_s %>">
4
- <%= render Vanity.template("experiment"), :experiment=>experiment %>
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 %>">
4
+ <%= render Vanity.template("experiment"), :id=>id, :experiment=>experiment %>
5
5
  </li>
6
6
  <% end %>
7
7
  </ul>
@@ -2,7 +2,7 @@
2
2
  <%= simple_format h(Vanity::Metric.description(metric).to_s), :class=>"description" %>
3
3
  <% data = Vanity::Metric.data(metric)
4
4
  min, max = data.map(&:last).minmax
5
- js = data.map { |date,value| "[#{date.to_time.to_i * 1000},#{value}]" }.join(",") %>
5
+ js = data.map { |date,value| "['#{date.to_time.httpdate}',#{value}]" }.join(",") %>
6
6
  <div class="chart"></div>
7
7
  <script type="text/javascript">
8
8
  $(function () { Vanity.metric("<%= id %>").plot([{ label: "<%=h metric.name %>", data: [<%= js %>] }]); });
@@ -6,8 +6,8 @@
6
6
  <% end %>
7
7
  </ul>
8
8
  <form id="milestones">
9
- <% experiments.each do |experiment| %>
10
- <label><input type="checkbox" name="milestone" data-start="<%= experiment.created_at.to_i %>"
11
- data-end="<%= (experiment.completed_at || Time.now).to_i %>"><%=h experiment.name %></label>
9
+ <% experiments.each do |id, experiment| %>
10
+ <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
12
  <% end %>
13
13
  </form>
@@ -4,8 +4,9 @@ Vanity.tooltip = function(event, pos, item) {
4
4
  if (this.previousPoint != item.datapoint) {
5
5
  this.previousPoint = item.datapoint;
6
6
  $("#tooltip").remove();
7
- var x = item.datapoint[0].toFixed(2), y = item.datapoint[1].toFixed(2);
8
- $('<div id="tooltip">' + new Date(parseInt(x, 10)).toDateString() + "&mdash;" + y + '</div>').css( {
7
+ var y = item.datapoint[1].toFixed(2);
8
+ var dt = new Date(parseInt(item.datapoint[0], 10));
9
+ $('<div id="tooltip">' + dt.getUTCFullYear() + '-' + (dt.getUTCMonth() + 1) + '-' + dt.getUTCDate() + "<br>" + y + '</div>').css( {
9
10
  position: 'absolute', display: 'none',
10
11
  top: item.pageY + 5, left: item.pageX + 5,
11
12
  padding: '2px', border: '1px solid #ff8', 'background-color': '#ffe', opacity: 0.9
@@ -31,26 +32,11 @@ Vanity.metric = function(id) {
31
32
  legend: { position: 'sw', container: "#metric_" + id +" .legend", backgroundOpacity: 0.5 },
32
33
  grid: { markings: metric.markings, borderWidth: 1, borderColor: '#eee', hoverable: true, aboveData: true } };
33
34
 
34
- metric.mark = function(start, end, label) {
35
- metric.markings.push({color: "#f02020", xaxis: { from: start, to: start + 4 * 3600 * 1000 }, text: label});
36
- metric.markings.push({color: "#f02020", xaxis: { from: end, to: end + 4 * 3600 * 1000 }});
37
- }
38
35
  metric.plot = function(lines) {
39
- var min, max;
40
- $.each(lines[0].data, function(i, val) { var y = val[1];
41
- if (max == null || y > max) max = y;
42
- if (min == null || y < min) min = y;
36
+ $.each(lines, function(i, line) {
37
+ $.each(line.data, function(i, pair) { pair[0] = Date.parse(pair[0]) })
43
38
  });
44
- if (min == null) min = max = 0;
45
- metric.options.yaxis.ticks = [min, (min + max) / 2.0, max];
46
39
  var plot = $.plot(metric.chart, lines, metric.options);
47
- $.each(metric.markings, function(i, mark) {
48
- if (mark.xaxis && mark.xaxis.from && mark.label) {
49
- var o = plot.pointOffset({x:mark.xaxis.from, y:max / 10 - min});
50
- $('<div style="position:absolute;bottom:2em;color:#f02020;font-size:80%"></div>').
51
- css({left:o.left+4, top:o.top-4}).text(mark.text).appendTo(metric.chart);
52
- }
53
- });
54
40
  metric.chart.bind("plothover", Vanity.tooltip);
55
41
  metric.chart.data('plot', plot);
56
42
  }
@@ -62,8 +48,8 @@ $(function() {
62
48
  checkboxes.bind("change", function() {
63
49
  var markings = [];
64
50
  checkboxes.filter(":checked").each(function(i, checkbox) {
65
- var start = parseInt($(this).attr("data-start"), 10) * 1000;
66
- var end = parseInt($(this).attr("data-end"), 10) * 1000;
51
+ var start = Date.parse($(this).attr("data-start"));
52
+ var end = Date.parse($(this).attr("data-end"));
67
53
  var title = $(this).parent().text();
68
54
  markings.push({color: "#c66", xaxis: { from: start - 20000000, to: start }, label: title});
69
55
  if (end > start)
data/lib/vanity.rb CHANGED
@@ -1,10 +1,11 @@
1
1
  $LOAD_PATH.unshift File.join(File.dirname(__FILE__), "../vendor/redis-rb/lib")
2
2
  require "redis"
3
- require "openssl"
4
3
  require "date"
4
+ require "time"
5
5
  require "logger"
6
6
 
7
7
  # All the cool stuff happens in other places.
8
+ # @see Vanity::Helper
8
9
  # @see Vanity::Rails
9
10
  # @see Vanity::Playground
10
11
  # @see Vanity::Metric
@@ -25,6 +26,7 @@ require "vanity/metric"
25
26
  require "vanity/experiment/base"
26
27
  require "vanity/experiment/ab_test"
27
28
  require "vanity/playground"
29
+ require "vanity/helpers"
28
30
  Vanity.autoload :MockRedis, "vanity/mock_redis"
29
31
  Vanity.autoload :Commands, "vanity/commands"
30
32
  require "vanity/rails" if defined?(Rails)