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 +31 -0
- data/lib/vanity/experiment/ab_test.rb +10 -6
- data/lib/vanity/experiment/base.rb +2 -5
- data/lib/vanity/helpers.rb +59 -0
- data/lib/vanity/metric.rb +92 -21
- data/lib/vanity/playground.rb +42 -22
- data/lib/vanity/rails/helpers.rb +7 -16
- data/lib/vanity/rails.rb +12 -5
- data/lib/vanity/templates/_experiments.erb +3 -3
- data/lib/vanity/templates/_metric.erb +1 -1
- data/lib/vanity/templates/_metrics.erb +3 -3
- data/lib/vanity/templates/vanity.js +7 -21
- data/lib/vanity.rb +3 -1
- data/test/metric_test.rb +442 -211
- data/test/test_helper.rb +64 -4
- data/vanity.gemspec +1 -1
- metadata +4 -3
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
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
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)
|
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
|
-
|
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 -
|
85
|
-
# Vanity::Metric.data(my_metric, 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,
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
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),
|
103
|
-
fail NameError.new("Expected #{
|
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
|
-
|
121
|
-
@
|
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
|
-
|
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
|
-
|
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
|
-
|
173
|
-
|
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
|
data/lib/vanity/playground.rb
CHANGED
@@ -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]
|
17
|
-
@logger
|
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
|
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
|
-
|
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
|
-
|
67
|
+
experiments[id] ||= Experiment::Base.load(self, @loading, File.expand_path(load_path), id)
|
62
68
|
end
|
63
69
|
|
64
|
-
# Returns
|
70
|
+
# Returns hash of experiments (key is experiment id).
|
71
|
+
#
|
72
|
+
# @see Vanity::Experiment
|
65
73
|
def experiments
|
66
|
-
|
67
|
-
|
68
|
-
|
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
|
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
|
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 (
|
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
|
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
|
-
|
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
|
data/lib/vanity/rails/helpers.rb
CHANGED
@@ -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"] ||
|
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
|
-
#
|
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)
|
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
|
3
|
-
<li class="experiment <%= experiment.type %>" id="experiment_<%=h
|
4
|
-
|
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.
|
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.
|
11
|
-
data-end="<%= (experiment.completed_at || Time.now).
|
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
|
8
|
-
|
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
|
-
|
40
|
-
|
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 =
|
66
|
-
var end =
|
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)
|