vanity 1.1.1 → 1.2.0

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