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