vanity 1.0.0 → 1.1.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 +35 -0
- data/README.rdoc +33 -6
- data/lib/vanity.rb +13 -7
- data/lib/vanity/backport.rb +43 -0
- data/lib/vanity/commands/report.rb +13 -3
- data/lib/vanity/experiment/ab_test.rb +98 -66
- data/lib/vanity/experiment/base.rb +51 -5
- data/lib/vanity/metric.rb +213 -0
- data/lib/vanity/mock_redis.rb +76 -0
- data/lib/vanity/playground.rb +78 -61
- data/lib/vanity/rails/dashboard.rb +11 -2
- data/lib/vanity/rails/helpers.rb +3 -3
- data/lib/vanity/templates/_ab_test.erb +3 -4
- data/lib/vanity/templates/_experiment.erb +4 -4
- data/lib/vanity/templates/_experiments.erb +2 -2
- data/lib/vanity/templates/_metric.erb +9 -0
- data/lib/vanity/templates/_metrics.erb +13 -0
- data/lib/vanity/templates/_report.erb +14 -3
- data/lib/vanity/templates/flot.min.js +1 -0
- data/lib/vanity/templates/jquery.min.js +19 -0
- data/lib/vanity/templates/vanity.css +16 -4
- data/lib/vanity/templates/vanity.js +96 -0
- data/test/ab_test_test.rb +159 -96
- data/test/experiment_test.rb +99 -18
- data/test/experiments/age_and_zipcode.rb +1 -0
- data/test/experiments/metrics/cheers.rb +3 -0
- data/test/experiments/metrics/signups.rb +2 -0
- data/test/experiments/metrics/yawns.rb +3 -0
- data/test/experiments/null_abc.rb +1 -0
- data/test/metric_test.rb +287 -0
- data/test/playground_test.rb +1 -80
- data/test/rails_test.rb +9 -6
- data/test/test_helper.rb +37 -6
- data/vanity.gemspec +1 -1
- data/vendor/{redis-0.1 → redis-rb}/LICENSE +0 -0
- data/vendor/{redis-0.1 → redis-rb}/README.markdown +0 -0
- data/vendor/{redis-0.1 → redis-rb}/Rakefile +0 -0
- data/vendor/redis-rb/bench.rb +44 -0
- data/vendor/redis-rb/benchmarking/suite.rb +24 -0
- data/vendor/redis-rb/benchmarking/worker.rb +71 -0
- data/vendor/redis-rb/bin/distredis +33 -0
- data/vendor/redis-rb/examples/basic.rb +16 -0
- data/vendor/redis-rb/examples/incr-decr.rb +18 -0
- data/vendor/redis-rb/examples/list.rb +26 -0
- data/vendor/redis-rb/examples/sets.rb +36 -0
- data/vendor/{redis-0.1 → redis-rb}/lib/dist_redis.rb +0 -0
- data/vendor/{redis-0.1 → redis-rb}/lib/hash_ring.rb +0 -0
- data/vendor/{redis-0.1 → redis-rb}/lib/pipeline.rb +0 -2
- data/vendor/{redis-0.1 → redis-rb}/lib/redis.rb +25 -7
- data/vendor/{redis-0.1 → redis-rb}/lib/redis/raketasks.rb +0 -0
- data/vendor/redis-rb/profile.rb +22 -0
- data/vendor/redis-rb/redis-rb.gemspec +30 -0
- data/vendor/{redis-0.1 → redis-rb}/spec/redis_spec.rb +113 -0
- data/vendor/{redis-0.1 → redis-rb}/spec/spec_helper.rb +0 -0
- data/vendor/redis-rb/speed.rb +16 -0
- data/vendor/{redis-0.1 → redis-rb}/tasks/redis.tasks.rb +5 -1
- metadata +37 -14
@@ -1,6 +1,29 @@
|
|
1
1
|
module Vanity
|
2
2
|
module Experiment
|
3
3
|
|
4
|
+
# These methods are available from experiment definitions (files located in
|
5
|
+
# the experiments directory, automatically loaded by Vanity). Use these
|
6
|
+
# methods to define you experiments, for example:
|
7
|
+
# ab_test "New Banner" do
|
8
|
+
# alternatives :red, :green, :blue
|
9
|
+
# metrics :signup
|
10
|
+
# end
|
11
|
+
module Definition
|
12
|
+
|
13
|
+
# Defines a new experiment, given the experiment's name, type and
|
14
|
+
# definition block.
|
15
|
+
def define(name, type, options = nil, &block)
|
16
|
+
options ||= {}
|
17
|
+
@playground.define(name, type, options, &block)
|
18
|
+
end
|
19
|
+
|
20
|
+
def binding(playground)
|
21
|
+
@playground = playground
|
22
|
+
Kernel.binding
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
4
27
|
# Base class that all experiment types are derived from.
|
5
28
|
class Base
|
6
29
|
|
@@ -12,6 +35,27 @@ module Vanity
|
|
12
35
|
name.split("::").last.gsub(/([a-z])([A-Z])/) { "#{$1}_#{$2}" }.gsub(/([A-Z])([A-Z][a-z])/) { "#{$1}_#{$2}" }.downcase
|
13
36
|
end
|
14
37
|
|
38
|
+
# Playground uses this to load experiment definitions.
|
39
|
+
def load(playground, stack, path, id)
|
40
|
+
fn = File.join(path, "#{id}.rb")
|
41
|
+
fail "Circular dependency detected: #{stack.join('=>')}=>#{fn}" if stack.include?(fn)
|
42
|
+
source = File.read(fn)
|
43
|
+
stack.push fn
|
44
|
+
context = Object.new
|
45
|
+
context.instance_eval do
|
46
|
+
extend Definition
|
47
|
+
experiment = eval(source, context.binding(playground), fn)
|
48
|
+
fail NameError.new("Expected #{fn} to define experiment #{id}", id) unless experiment.id == id
|
49
|
+
experiment
|
50
|
+
end
|
51
|
+
rescue
|
52
|
+
error = NameError.exception($!.message, id)
|
53
|
+
error.set_backtrace $!.backtrace
|
54
|
+
raise error
|
55
|
+
ensure
|
56
|
+
stack.pop
|
57
|
+
end
|
58
|
+
|
15
59
|
end
|
16
60
|
|
17
61
|
def initialize(playground, id, name, options, &block)
|
@@ -19,7 +63,7 @@ module Vanity
|
|
19
63
|
@id, @name = id.to_sym, name
|
20
64
|
@options = options || {}
|
21
65
|
@namespace = "#{@playground.namespace}:#{@id}"
|
22
|
-
@identify_block =
|
66
|
+
@identify_block = lambda { |context| context.vanity_identity }
|
23
67
|
end
|
24
68
|
|
25
69
|
# Human readable experiment name (first argument you pass when creating a
|
@@ -110,18 +154,19 @@ module Vanity
|
|
110
154
|
# Force experiment to complete.
|
111
155
|
def complete!
|
112
156
|
redis.setnx key(:completed_at), Time.now.to_i
|
113
|
-
|
157
|
+
@completed_at = redis[key(:completed_at)]
|
158
|
+
@playground.logger.info "vanity: completed experiment #{id}"
|
114
159
|
end
|
115
160
|
|
116
161
|
# Time stamp when experiment was completed.
|
117
162
|
def completed_at
|
118
|
-
|
119
|
-
|
163
|
+
@completed_at ||= redis[key(:completed_at)]
|
164
|
+
@completed_at && Time.at(@completed_at.to_i)
|
120
165
|
end
|
121
166
|
|
122
167
|
# Returns true if experiment active, false if completed.
|
123
168
|
def active?
|
124
|
-
redis
|
169
|
+
!redis.exists(key(:completed_at))
|
125
170
|
end
|
126
171
|
|
127
172
|
# -- Store/validate --
|
@@ -130,6 +175,7 @@ module Vanity
|
|
130
175
|
def destroy
|
131
176
|
redis.del key(:created_at)
|
132
177
|
redis.del key(:completed_at)
|
178
|
+
@created_at = @completed_at = nil
|
133
179
|
end
|
134
180
|
|
135
181
|
# Called by Playground to save the experiment definition.
|
@@ -0,0 +1,213 @@
|
|
1
|
+
module Vanity
|
2
|
+
|
3
|
+
# A metric is an object that implements two methods: +name+ and +values+. It
|
4
|
+
# can also respond to addition methods (+track!+, +bounds+, etc), these are
|
5
|
+
# optional.
|
6
|
+
#
|
7
|
+
# This class implements a basic metric that tracks data and stores it in
|
8
|
+
# Redis. You can use this as the basis for your metric, or as reference for
|
9
|
+
# the methods your metric must and can implement.
|
10
|
+
#
|
11
|
+
# @since 1.1.0
|
12
|
+
class Metric
|
13
|
+
|
14
|
+
# These methods are available when defining a metric in a file loaded
|
15
|
+
# from the +experiments/metrics+ directory.
|
16
|
+
#
|
17
|
+
# For example:
|
18
|
+
# $ cat experiments/metrics/yawn_sec
|
19
|
+
# metric "Yawns/sec" do
|
20
|
+
# description "Most boring metric ever"
|
21
|
+
# end
|
22
|
+
module Definition
|
23
|
+
|
24
|
+
# Defines a new metric, using the class Vanity::Metric.
|
25
|
+
def metric(name, &block)
|
26
|
+
metric = Metric.new(@playground, name.to_s, name.to_s.downcase.gsub(/\W/, "_"))
|
27
|
+
metric.instance_eval &block
|
28
|
+
metric
|
29
|
+
end
|
30
|
+
|
31
|
+
def binding(playground)
|
32
|
+
@playground = playground
|
33
|
+
Kernel.binding
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
# Startup metrics for pirates. AARRR stands for:
|
39
|
+
# * Acquisition
|
40
|
+
# * Activation
|
41
|
+
# * Retention
|
42
|
+
# * Referral
|
43
|
+
# * Revenue
|
44
|
+
# Read more: http://500hats.typepad.com/500blogs/2007/09/startup-metrics.html
|
45
|
+
|
46
|
+
class << self
|
47
|
+
|
48
|
+
# Helper method to return description for a metric.
|
49
|
+
#
|
50
|
+
# A metric object may have a +description+ method that returns a detailed
|
51
|
+
# description. It may also have no description, or no +description+
|
52
|
+
# method, in which case return +nil+.
|
53
|
+
#
|
54
|
+
# @example
|
55
|
+
# puts Vanity::Metric.description(metric)
|
56
|
+
def description(metric)
|
57
|
+
metric.description if metric.respond_to?(:description)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Helper method to return bounds for a metric.
|
61
|
+
#
|
62
|
+
# A metric object may have a +bounds+ method that returns lower and upper
|
63
|
+
# bounds. It may also have no bounds, or no +bounds+ # method, in which
|
64
|
+
# case we return +[nil, nil]+.
|
65
|
+
#
|
66
|
+
# @example
|
67
|
+
# upper = Vanity::Metric.bounds(metric).last
|
68
|
+
def bounds(metric)
|
69
|
+
metric.respond_to?(:bounds) && metric.bounds || [nil, nil]
|
70
|
+
end
|
71
|
+
|
72
|
+
# Returns data set for a given date range. The data set is an array of
|
73
|
+
# date, value pairs.
|
74
|
+
#
|
75
|
+
# First argument is the metric. Second argument is the start date, or
|
76
|
+
# number of days to go back in history, defaults to 90 days. Third
|
77
|
+
# argument is end date, defaults to today.
|
78
|
+
#
|
79
|
+
# @example These are all equivalent:
|
80
|
+
# Vanity::Metric.data(my_metric)
|
81
|
+
# Vanity::Metric.data(my_metric, 90)
|
82
|
+
# Vanity::Metric.data(my_metric, Date.today - 90)
|
83
|
+
# Vanity::Metric.data(my_metric, Date.today - 90, Date.today)
|
84
|
+
def data(metric, *args)
|
85
|
+
first = args.shift || 90
|
86
|
+
to = args.shift || Date.today
|
87
|
+
from = first.respond_to?(:to_date) ? first.to_date : to - first
|
88
|
+
(from..to).zip(metric.values(from, to))
|
89
|
+
end
|
90
|
+
|
91
|
+
# Playground uses this to load metric definitions.
|
92
|
+
def load(playground, stack, path, id)
|
93
|
+
fn = File.join(path, "#{id}.rb")
|
94
|
+
fail "Circular dependency detected: #{stack.join('=>')}=>#{fn}" if stack.include?(fn)
|
95
|
+
source = File.read(fn)
|
96
|
+
stack.push fn
|
97
|
+
context = Object.new
|
98
|
+
context.instance_eval do
|
99
|
+
extend Definition
|
100
|
+
metric = eval(source, context.binding(playground), fn)
|
101
|
+
fail NameError.new("Expected #{fn} to define metric #{id}", id) unless metric.name.downcase.gsub(/\W+/, '_').to_sym == id
|
102
|
+
metric
|
103
|
+
end
|
104
|
+
rescue
|
105
|
+
error = NameError.exception($!.message, id)
|
106
|
+
error.set_backtrace $!.backtrace
|
107
|
+
raise error
|
108
|
+
ensure
|
109
|
+
stack.pop
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
|
114
|
+
|
115
|
+
# Takes playground (need this to access Redis), friendly name and optional
|
116
|
+
# id (can infer from name).
|
117
|
+
def initialize(playground, name, id = nil)
|
118
|
+
id ||= name.to_s.downcase.gsub(/\W+/, '_')
|
119
|
+
@playground, @name, @id = playground, name.to_s, id.to_sym
|
120
|
+
@hooks = []
|
121
|
+
redis.setnx key(:created_at), Time.now.to_i
|
122
|
+
@created_at = Time.at(redis[key(:created_at)].to_i)
|
123
|
+
end
|
124
|
+
|
125
|
+
|
126
|
+
# -- Tracking --
|
127
|
+
|
128
|
+
# Called to track an action associated with this metric.
|
129
|
+
def track!(count = 1)
|
130
|
+
timestamp = Time.now
|
131
|
+
if count > 0
|
132
|
+
redis.incrby key(timestamp.to_date, "count"), count
|
133
|
+
@playground.logger.info "vanity: #{@id} with count #{count}"
|
134
|
+
@hooks.each do |hook|
|
135
|
+
hook.call @id, timestamp, count
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# Metric definitions use this to introduce tracking hook. The hook is
|
141
|
+
# called with metric identifier, timestamp, count and possibly additional
|
142
|
+
# arguments.
|
143
|
+
#
|
144
|
+
# For example:
|
145
|
+
# hook do |metric_id, timestamp, count|
|
146
|
+
# syslog.info metric_id
|
147
|
+
# end
|
148
|
+
def hook(&block)
|
149
|
+
@hooks << block
|
150
|
+
end
|
151
|
+
|
152
|
+
# This method returns the acceptable bounds of a metric as an array with
|
153
|
+
# two values: low and high. Use nil for unbounded.
|
154
|
+
#
|
155
|
+
# Alerts are created when metric values exceed their bounds. For example,
|
156
|
+
# a metric of user registration can use historical data to calculate
|
157
|
+
# expected range of new registration for the next day. If actual metric
|
158
|
+
# falls below the expected range, it could indicate registration process is
|
159
|
+
# broken. Going above higher bound could trigger opening a Champagne
|
160
|
+
# bottle.
|
161
|
+
#
|
162
|
+
# The default implementation returns +nil+.
|
163
|
+
def bounds
|
164
|
+
end
|
165
|
+
|
166
|
+
|
167
|
+
# -- Reporting --
|
168
|
+
|
169
|
+
# Human readable metric name. All metrics must implement this method.
|
170
|
+
def name
|
171
|
+
@name
|
172
|
+
end
|
173
|
+
|
174
|
+
# Time stamp when metric was created.
|
175
|
+
attr_reader :created_at
|
176
|
+
|
177
|
+
# Human readable description. Use two newlines to break paragraphs.
|
178
|
+
attr_accessor :description
|
179
|
+
|
180
|
+
# Sets or returns description. For example
|
181
|
+
# metric "Yawns/sec" do
|
182
|
+
# description "Most boring metric ever"
|
183
|
+
# end
|
184
|
+
#
|
185
|
+
# puts "Just defined: " + metric(:boring).description
|
186
|
+
def description(text = nil)
|
187
|
+
@description = text if text
|
188
|
+
@description
|
189
|
+
end
|
190
|
+
|
191
|
+
# Given two arguments, a start date and an end date (inclusive), returns an
|
192
|
+
# array of measurements. All metrics must implement this method.
|
193
|
+
def values(from, to)
|
194
|
+
redis.mget((from.to_date..to.to_date).map { |date| key(date, "count") }).map(&:to_i)
|
195
|
+
end
|
196
|
+
|
197
|
+
|
198
|
+
# -- Storage --
|
199
|
+
|
200
|
+
def destroy!
|
201
|
+
redis.del redis.keys(key("*"))
|
202
|
+
end
|
203
|
+
|
204
|
+
def redis
|
205
|
+
@playground.redis
|
206
|
+
end
|
207
|
+
|
208
|
+
def key(*args)
|
209
|
+
"metrics:#{@id}:#{args.join(':')}"
|
210
|
+
end
|
211
|
+
|
212
|
+
end
|
213
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module Vanity
|
2
|
+
# The Redis you should never use in production.
|
3
|
+
class MockRedis
|
4
|
+
@@hash = {}
|
5
|
+
|
6
|
+
def initialize(options = {})
|
7
|
+
end
|
8
|
+
|
9
|
+
def [](key)
|
10
|
+
@@hash[key]
|
11
|
+
end
|
12
|
+
|
13
|
+
def []=(key, value)
|
14
|
+
@@hash[key] = value.to_s
|
15
|
+
end
|
16
|
+
|
17
|
+
def del(*keys)
|
18
|
+
keys.flatten.each do |key|
|
19
|
+
@@hash.delete key
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def setnx(key, value)
|
24
|
+
@@hash[key] = value.to_s unless @@hash.has_key?(key)
|
25
|
+
end
|
26
|
+
|
27
|
+
def incr(key)
|
28
|
+
@@hash[key] = (@@hash[key].to_i + 1).to_s
|
29
|
+
end
|
30
|
+
|
31
|
+
def incrby(key, value)
|
32
|
+
@@hash[key] = (@@hash[key].to_i + value).to_s
|
33
|
+
end
|
34
|
+
|
35
|
+
def mget(keys)
|
36
|
+
@@hash.values_at(*keys)
|
37
|
+
end
|
38
|
+
|
39
|
+
def exists(key)
|
40
|
+
@@hash.has_key?(key)
|
41
|
+
end
|
42
|
+
|
43
|
+
def keys(pattern)
|
44
|
+
regexp = Regexp.new(pattern.split("*").map { |r| Regexp.escape(r) }.join(".*"))
|
45
|
+
@@hash.keys.select { |key| key =~ regexp }
|
46
|
+
end
|
47
|
+
|
48
|
+
def flushdb
|
49
|
+
@@hash.clear
|
50
|
+
end
|
51
|
+
|
52
|
+
def sismember(key, value)
|
53
|
+
case set = @@hash[key]
|
54
|
+
when nil ; false
|
55
|
+
when Set ; set.member?(value)
|
56
|
+
else fail "Not a set"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def sadd(key, value)
|
61
|
+
case set = @@hash[key]
|
62
|
+
when nil ; @@hash[key] = Set.new([value])
|
63
|
+
when Set ; set.add value
|
64
|
+
else fail "Not a set"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def scard(key)
|
69
|
+
case set = @@hash[key]
|
70
|
+
when nil ; 0
|
71
|
+
when Set ; set.size
|
72
|
+
else fail "Not a set"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
data/lib/vanity/playground.rb
CHANGED
@@ -1,35 +1,23 @@
|
|
1
1
|
module Vanity
|
2
2
|
|
3
|
-
# These methods are available from experiment definitions (files located in
|
4
|
-
# the experiments directory, automatically loaded by Vanity). Use these
|
5
|
-
# methods to define you experiments, for example:
|
6
|
-
# ab_test "New Banner" do
|
7
|
-
# alternatives :red, :green, :blue
|
8
|
-
# end
|
9
|
-
module Definition
|
10
|
-
|
11
|
-
protected
|
12
|
-
# Defines a new experiment, given the experiment's name, type and
|
13
|
-
# definition block.
|
14
|
-
def define(name, type, options = nil, &block)
|
15
|
-
options ||= {}
|
16
|
-
Vanity.playground.define(name, type, options, &block)
|
17
|
-
end
|
18
|
-
|
19
|
-
end
|
20
|
-
|
21
3
|
# Playground catalogs all your experiments, holds the Vanity configuration.
|
22
|
-
#
|
4
|
+
#
|
5
|
+
# @example
|
23
6
|
# Vanity.playground.logger = my_logger
|
24
7
|
# puts Vanity.playground.map(&:name)
|
25
8
|
class Playground
|
26
9
|
|
10
|
+
DEFAULTS = { :host=>"127.0.0.1", :port=>6379, :db=>0, :load_path=>"experiments" }
|
11
|
+
|
27
12
|
# Created new Playground. Unless you need to, use the global Vanity.playground.
|
28
|
-
def initialize
|
29
|
-
@
|
30
|
-
@host, @port, @db = "127.0.0.1", 6379, 0
|
13
|
+
def initialize(options = {})
|
14
|
+
@host, @port, @db, @load_path = DEFAULTS.merge(options).values_at(:host, :port, :db, :load_path)
|
31
15
|
@namespace = "vanity:#{Vanity::Version::MAJOR}"
|
32
|
-
@
|
16
|
+
@logger = options[:logger] || Logger.new(STDOUT)
|
17
|
+
@logger.level = Logger::ERROR
|
18
|
+
@redis = options[:redis]
|
19
|
+
@experiments = {}
|
20
|
+
@loading = []
|
33
21
|
end
|
34
22
|
|
35
23
|
# Redis host name. Default is 127.0.0.1
|
@@ -56,7 +44,7 @@ module Vanity
|
|
56
44
|
# Defines a new experiment. Generally, do not call this directly,
|
57
45
|
# use one of the definition methods (ab_test, measure, etc).
|
58
46
|
def define(name, type, options = {}, &block)
|
59
|
-
id = name.to_s.downcase.gsub(/\W/, "_")
|
47
|
+
id = name.to_s.downcase.gsub(/\W/, "_").to_sym
|
60
48
|
raise "Experiment #{id} already defined once" if @experiments[id]
|
61
49
|
klass = Experiment.const_get(type.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase })
|
62
50
|
experiment = klass.new(self, id, name, options)
|
@@ -65,41 +53,19 @@ module Vanity
|
|
65
53
|
@experiments[id] = experiment
|
66
54
|
end
|
67
55
|
|
68
|
-
# Returns the
|
69
|
-
#
|
70
|
-
#
|
71
|
-
# Experiment names are always mapped by downcasing them and replacing
|
72
|
-
# non-word characters with underscores, so "Green call to action" becomes
|
73
|
-
# "green_call_to_action". You can also use a symbol if you feel like it.
|
56
|
+
# Returns the experiment. You may not have guessed, but this method raises
|
57
|
+
# an exception if it cannot load the experiment's definition.
|
74
58
|
def experiment(name)
|
75
|
-
id = name.to_s.downcase.gsub(/\W/, "_")
|
76
|
-
unless
|
77
|
-
|
78
|
-
fail "Circular dependency detected: #{@loading.join('=>')}=>#{id}" if @loading.include?(id)
|
79
|
-
begin
|
80
|
-
@loading.push id
|
81
|
-
source = File.read(File.expand_path("#{id}.rb", load_path))
|
82
|
-
context = Object.new
|
83
|
-
context.instance_eval do
|
84
|
-
extend Definition
|
85
|
-
eval source
|
86
|
-
end
|
87
|
-
rescue
|
88
|
-
error = LoadError.exception($!.message)
|
89
|
-
error.set_backtrace $!.backtrace
|
90
|
-
raise error
|
91
|
-
ensure
|
92
|
-
@loading.pop
|
93
|
-
end
|
94
|
-
end
|
95
|
-
@experiments[id] or fail LoadError, "Expected experiments/#{id}.rb to define experiment #{name}"
|
59
|
+
id = name.to_s.downcase.gsub(/\W/, "_").to_sym
|
60
|
+
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)
|
96
62
|
end
|
97
63
|
|
98
64
|
# Returns list of all loaded experiments.
|
99
65
|
def experiments
|
100
66
|
Dir[File.join(load_path, "*.rb")].each do |file|
|
101
67
|
id = File.basename(file).gsub(/.rb$/, "")
|
102
|
-
experiment id
|
68
|
+
experiment id.to_sym
|
103
69
|
end
|
104
70
|
@experiments.values
|
105
71
|
end
|
@@ -107,25 +73,72 @@ module Vanity
|
|
107
73
|
# Reloads all experiments.
|
108
74
|
def reload!
|
109
75
|
@experiments.clear
|
76
|
+
@metrics = nil
|
110
77
|
end
|
111
78
|
|
112
79
|
# Use this instance to access the Redis database.
|
113
80
|
def redis
|
114
|
-
redis
|
115
|
-
|
116
|
-
class << self ; self ; end.send(:define_method, :redis) { redis }
|
117
|
-
redis
|
81
|
+
@redis ||= Redis.new(:host=>self.host, :port=>self.port, :db=>self.db,
|
82
|
+
:password=>self.password, :logger=>self.logger)
|
83
|
+
class << self ; self ; end.send(:define_method, :redis) { @redis }
|
84
|
+
@redis
|
85
|
+
end
|
86
|
+
|
87
|
+
# Switches playground to use MockRedis instead of a live server.
|
88
|
+
# Particularly useful for testing, e.g. if you can't access Redis on your CI
|
89
|
+
# server. This method has no affect after playground accesses live Redis
|
90
|
+
# server.
|
91
|
+
#
|
92
|
+
# @example Put this in config/environments/test.rb
|
93
|
+
# config.after_initialize { Vanity.playground.mock! }
|
94
|
+
def mock!
|
95
|
+
@redis ||= MockRedis.new
|
118
96
|
end
|
119
97
|
|
98
|
+
# Returns a metric (creating one if doesn't already exist).
|
99
|
+
#
|
100
|
+
# @since 1.1.0
|
101
|
+
def metric(id)
|
102
|
+
id = id.to_sym
|
103
|
+
metrics[id] ||= Metric.load(self, @loading, File.expand_path("metrics", load_path), id)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Returns hash of metrics (key is metric id).
|
107
|
+
#
|
108
|
+
# @since 1.1.0
|
109
|
+
def metrics
|
110
|
+
unless @metrics
|
111
|
+
@metrics = {}
|
112
|
+
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
|
119
|
+
end
|
120
|
+
end
|
121
|
+
@metrics
|
122
|
+
end
|
123
|
+
|
124
|
+
# Tracks an action associated with a metric.
|
125
|
+
#
|
126
|
+
# @example
|
127
|
+
# Vanity.playground.track! :uploaded_video
|
128
|
+
#
|
129
|
+
# @since 1.1.0
|
130
|
+
def track!(id, count = 1)
|
131
|
+
metric(id).track! count
|
132
|
+
end
|
120
133
|
end
|
121
134
|
|
122
135
|
@playground = Playground.new
|
123
136
|
class << self
|
124
137
|
|
125
|
-
#
|
126
|
-
|
127
|
-
|
128
|
-
|
138
|
+
# The playground instance.
|
139
|
+
#
|
140
|
+
# @see Vanity::Playground
|
141
|
+
attr_accessor :playground
|
129
142
|
|
130
143
|
# Returns the Vanity context. For example, when using Rails this would be
|
131
144
|
# the current controller, which can be used to get/set the vanity identity.
|
@@ -152,8 +165,12 @@ end
|
|
152
165
|
|
153
166
|
class Object
|
154
167
|
|
155
|
-
# Use this method to access an experiment by name.
|
168
|
+
# Use this method to access an experiment by name.
|
169
|
+
#
|
170
|
+
# @example
|
156
171
|
# puts experiment(:text_size).alternatives
|
172
|
+
#
|
173
|
+
# @see Vanity::Playground#experiment
|
157
174
|
def experiment(name)
|
158
175
|
Vanity.playground.experiment(name)
|
159
176
|
end
|