vanity 1.3.0 → 1.4.0.beta
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 +61 -3
- data/Gemfile +22 -14
- data/README.rdoc +9 -4
- data/Rakefile +72 -12
- data/bin/vanity +16 -4
- data/lib/vanity.rb +7 -5
- data/lib/vanity/adapters/abstract_adapter.rb +135 -0
- data/lib/vanity/adapters/mock_adapter.rb +157 -0
- data/lib/vanity/adapters/mongo_adapter.rb +162 -0
- data/lib/vanity/adapters/redis_adapter.rb +154 -0
- data/lib/vanity/backport.rb +0 -17
- data/lib/vanity/commands/upgrade.rb +34 -0
- data/lib/vanity/experiment/ab_test.rb +46 -41
- data/lib/vanity/experiment/base.rb +13 -15
- data/lib/vanity/frameworks/rails.rb +5 -9
- data/lib/vanity/metric/active_record.rb +10 -4
- data/lib/vanity/metric/base.rb +46 -23
- data/lib/vanity/metric/google_analytics.rb +7 -0
- data/lib/vanity/metric/remote.rb +53 -0
- data/lib/vanity/playground.rb +133 -49
- data/test/{ab_test_test.rb → experiment/ab_test.rb} +47 -3
- data/test/{experiment_test.rb → experiment/base_test.rb} +8 -8
- data/test/metric/active_record_test.rb +253 -0
- data/test/metric/base_test.rb +293 -0
- data/test/metric/google_analytics_test.rb +104 -0
- data/test/metric/remote_test.rb +108 -0
- data/test/myapp/app/controllers/application_controller.rbc +66 -0
- data/test/myapp/app/controllers/main_controller.rb +3 -3
- data/test/myapp/app/controllers/main_controller.rbc +347 -0
- data/test/myapp/config/boot.rbc +2534 -0
- data/test/myapp/config/environment.rbc +403 -0
- data/test/myapp/config/routes.rbc +174 -0
- data/test/myapp/log/production.log +2601 -0
- data/test/passenger_test.rb +14 -5
- data/test/passenger_test.rbc +0 -0
- data/test/playground_test.rbc +256 -0
- data/test/rails_test.rb +75 -22
- data/test/rails_test.rbc +4086 -0
- data/test/test_helper.rb +30 -7
- data/test/test_helper.rbc +4297 -0
- data/vanity.gemspec +6 -2
- metadata +74 -73
- data/lib/vanity/commands.rb +0 -2
- data/lib/vanity/mock_redis.rb +0 -76
- data/test/metric_test.rb +0 -622
- data/vendor/cache/RedCloth-4.2.2.gem +0 -0
- data/vendor/cache/actionmailer-2.3.5.gem +0 -0
- data/vendor/cache/actionpack-2.3.5.gem +0 -0
- data/vendor/cache/activerecord-2.3.5.gem +0 -0
- data/vendor/cache/activeresource-2.3.5.gem +0 -0
- data/vendor/cache/activesupport-2.3.5.gem +0 -0
- data/vendor/cache/autotest-4.2.7.gem +0 -0
- data/vendor/cache/autotest-fsevent-0.2.1.gem +0 -0
- data/vendor/cache/autotest-growl-0.2.0.gem +0 -0
- data/vendor/cache/bundler-0.9.7.gem +0 -0
- data/vendor/cache/classifier-1.3.1.gem +0 -0
- data/vendor/cache/directory_watcher-1.3.1.gem +0 -0
- data/vendor/cache/fastthread-1.0.7.gem +0 -0
- data/vendor/cache/garb-0.7.0.gem +0 -0
- data/vendor/cache/happymapper-0.3.0.gem +0 -0
- data/vendor/cache/jekyll-0.5.7.gem +0 -0
- data/vendor/cache/libxml-ruby-1.1.3.gem +0 -0
- data/vendor/cache/liquid-2.0.0.gem +0 -0
- data/vendor/cache/maruku-0.6.0.gem +0 -0
- data/vendor/cache/mocha-0.9.8.gem +0 -0
- data/vendor/cache/open4-1.0.1.gem +0 -0
- data/vendor/cache/passenger-2.2.9.gem +0 -0
- data/vendor/cache/rack-1.0.1.gem +0 -0
- data/vendor/cache/rails-2.3.5.gem +0 -0
- data/vendor/cache/rake-0.8.7.gem +0 -0
- data/vendor/cache/rubygems-update-1.3.5.gem +0 -0
- data/vendor/cache/shoulda-2.10.3.gem +0 -0
- data/vendor/cache/sqlite3-ruby-1.2.5.gem +0 -0
- data/vendor/cache/stemmer-1.0.1.gem +0 -0
- data/vendor/cache/syntax-1.0.0.gem +0 -0
- data/vendor/cache/sys-uname-0.8.4.gem +0 -0
- data/vendor/cache/timecop-0.3.4.gem +0 -0
- data/vendor/redis-rb/LICENSE +0 -20
- data/vendor/redis-rb/README.markdown +0 -36
- data/vendor/redis-rb/Rakefile +0 -62
- data/vendor/redis-rb/bench.rb +0 -44
- data/vendor/redis-rb/benchmarking/suite.rb +0 -24
- data/vendor/redis-rb/benchmarking/worker.rb +0 -71
- data/vendor/redis-rb/bin/distredis +0 -33
- data/vendor/redis-rb/examples/basic.rb +0 -16
- data/vendor/redis-rb/examples/incr-decr.rb +0 -18
- data/vendor/redis-rb/examples/list.rb +0 -26
- data/vendor/redis-rb/examples/sets.rb +0 -36
- data/vendor/redis-rb/lib/dist_redis.rb +0 -124
- data/vendor/redis-rb/lib/hash_ring.rb +0 -128
- data/vendor/redis-rb/lib/pipeline.rb +0 -21
- data/vendor/redis-rb/lib/redis.rb +0 -370
- data/vendor/redis-rb/lib/redis/raketasks.rb +0 -1
- data/vendor/redis-rb/profile.rb +0 -22
- data/vendor/redis-rb/redis-rb.gemspec +0 -30
- data/vendor/redis-rb/spec/redis_spec.rb +0 -637
- data/vendor/redis-rb/spec/spec_helper.rb +0 -4
- data/vendor/redis-rb/speed.rb +0 -16
- data/vendor/redis-rb/tasks/redis.tasks.rb +0 -140
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
require "net/http"
|
|
2
|
+
require "cgi"
|
|
3
|
+
|
|
4
|
+
module Vanity
|
|
5
|
+
class Metric
|
|
6
|
+
|
|
7
|
+
# Specifies the base URL to use for a remote metric. For example:
|
|
8
|
+
# metric :sandbox do
|
|
9
|
+
# remote "http://api.vanitydash.com/metrics/sandbox"
|
|
10
|
+
# end
|
|
11
|
+
def remote(url = nil)
|
|
12
|
+
@remote_url = URI.parse(url) if url
|
|
13
|
+
@mutex ||= Mutex.new
|
|
14
|
+
extend Remote
|
|
15
|
+
@remote_url
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# To update a remote metric, make a POST request to the metric URL with the
|
|
19
|
+
# content type "application/x-www-form-urlencoded" and the following
|
|
20
|
+
# fields:
|
|
21
|
+
# - The +metric+ identifier,
|
|
22
|
+
# - The +timestamp+ must be RFC 2616 formatted (in Ruby just call +httpdate+
|
|
23
|
+
# on the Time object),
|
|
24
|
+
# - The +identity+ (optional),
|
|
25
|
+
# - Pass consecutive values using the field +values[]+, or
|
|
26
|
+
# - Set values by their index using +values[0]+, +values[1]+, etc or
|
|
27
|
+
# - Set values by series name using +values[foo]+, +values[bar]+, etc.
|
|
28
|
+
module Remote
|
|
29
|
+
|
|
30
|
+
def track!(args = nil)
|
|
31
|
+
return unless @playground.collecting?
|
|
32
|
+
timestamp, identity, values = track_args(args)
|
|
33
|
+
params = ["metric=#{CGI.escape @id.to_s}", "timestamp=#{CGI.escape timestamp.httpdate}"]
|
|
34
|
+
params << "identity=#{CGI.escape identity.to_s}" if identity
|
|
35
|
+
params.concat values.map { |v| "values[]=#{v.to_i}" }
|
|
36
|
+
params << @remote_url.query if @remote_url.query
|
|
37
|
+
@mutex.synchronize do
|
|
38
|
+
@http ||= Net::HTTP.start(@remote_url.host, @remote_url.port)
|
|
39
|
+
@http.request Net::HTTP::Post.new(@remote_url.path, "Content-Type"=>"application/x-www-form-urlencoded"), params.join("&")
|
|
40
|
+
end
|
|
41
|
+
rescue Timeout::Error, StandardError
|
|
42
|
+
@playground.logger.error "Error sending data for metric #{name}: #{$!}"
|
|
43
|
+
@http = nil
|
|
44
|
+
ensure
|
|
45
|
+
call_hooks timestamp, identity, values
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# "Don't worry, be crappy. Revolutionary means you ship and then test."
|
|
49
|
+
# -- Guy Kawazaki
|
|
50
|
+
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
data/lib/vanity/playground.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require "uri"
|
|
2
|
+
|
|
1
3
|
module Vanity
|
|
2
4
|
|
|
3
5
|
# Playground catalogs all your experiments, holds the Vanity configuration.
|
|
@@ -7,11 +9,7 @@ module Vanity
|
|
|
7
9
|
# puts Vanity.playground.map(&:name)
|
|
8
10
|
class Playground
|
|
9
11
|
|
|
10
|
-
DEFAULTS = {
|
|
11
|
-
:connection=>"localhost:6379",
|
|
12
|
-
:load_path=>"experiments",
|
|
13
|
-
:namespace=>"vanity:#{Vanity::Version::MAJOR}"
|
|
14
|
-
}
|
|
12
|
+
DEFAULTS = { :load_path=>"experiments" }
|
|
15
13
|
|
|
16
14
|
# Created new Playground. Unless you need to, use the global
|
|
17
15
|
# Vanity.playground.
|
|
@@ -26,22 +24,23 @@ module Vanity
|
|
|
26
24
|
options = args.pop if Hash === args.last
|
|
27
25
|
@options = DEFAULTS.merge(options || {})
|
|
28
26
|
if @options.values_at(:host, :port, :db).any?
|
|
29
|
-
warn "Deprecated: please specify Redis connection as
|
|
30
|
-
|
|
27
|
+
warn "Deprecated: please specify Redis connection as URL (\"redis://host:port/db\")"
|
|
28
|
+
establish_connection :adapter=>"redis", :host=>options[:host], :port=>options[:port], :database=>options[:db]
|
|
31
29
|
elsif @options[:redis]
|
|
32
|
-
@
|
|
30
|
+
@adapter = RedisAdapter.new(:redis=>@options[:redis])
|
|
33
31
|
else
|
|
34
|
-
|
|
32
|
+
connection_spec = args.shift || @options[:connection]
|
|
33
|
+
establish_connection "redis://" + connection_spec if connection_spec
|
|
35
34
|
end
|
|
36
35
|
|
|
37
|
-
|
|
36
|
+
warn "Deprecated: namespace option no longer supported directly" if @options[:namespace]
|
|
38
37
|
@load_path = @options[:load_path] || DEFAULTS[:load_path]
|
|
39
|
-
@logger = @options[:logger]
|
|
40
|
-
unless @logger
|
|
38
|
+
unless @logger = @options[:logger]
|
|
41
39
|
@logger = Logger.new(STDOUT)
|
|
42
40
|
@logger.level = Logger::ERROR
|
|
43
41
|
end
|
|
44
42
|
@loading = []
|
|
43
|
+
@collecting = true
|
|
45
44
|
end
|
|
46
45
|
|
|
47
46
|
# Deprecated. Use redis.server instead.
|
|
@@ -115,6 +114,22 @@ module Vanity
|
|
|
115
114
|
metrics[id.to_sym] or raise NameError, "No metric #{id}"
|
|
116
115
|
end
|
|
117
116
|
|
|
117
|
+
# True if collection data (metrics and experiments). You only want to
|
|
118
|
+
# collect data in production environment, everywhere else run with
|
|
119
|
+
# collection off.
|
|
120
|
+
#
|
|
121
|
+
# @since 1.4.0
|
|
122
|
+
def collecting?
|
|
123
|
+
@collecting
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Turns data collection on and off.
|
|
127
|
+
#
|
|
128
|
+
# @since 1.4.0
|
|
129
|
+
def collecting=(enabled)
|
|
130
|
+
@collecting = !!enabled
|
|
131
|
+
end
|
|
132
|
+
|
|
118
133
|
# Returns hash of metrics (key is metric id).
|
|
119
134
|
#
|
|
120
135
|
# @see Vanity::Metric
|
|
@@ -126,6 +141,14 @@ module Vanity
|
|
|
126
141
|
Dir[File.join(load_path, "metrics/*.rb")].each do |file|
|
|
127
142
|
Metric.load self, @loading, file
|
|
128
143
|
end
|
|
144
|
+
if File.exist?("config/vanity.yml") && remote = YAML.load_file("config/vanity.yml")["metrics"]
|
|
145
|
+
remote.each do |id, url|
|
|
146
|
+
fail "Metric #{id} already defined in playground" if metrics[id.to_sym]
|
|
147
|
+
metric = Metric.new(self, id)
|
|
148
|
+
metric.remote url
|
|
149
|
+
metrics[id.to_sym] = metric
|
|
150
|
+
end
|
|
151
|
+
end
|
|
129
152
|
end
|
|
130
153
|
@metrics
|
|
131
154
|
end
|
|
@@ -142,61 +165,122 @@ module Vanity
|
|
|
142
165
|
|
|
143
166
|
|
|
144
167
|
# -- Connection management --
|
|
145
|
-
|
|
146
|
-
#
|
|
147
|
-
#
|
|
148
|
-
#
|
|
149
|
-
#
|
|
150
|
-
#
|
|
151
|
-
|
|
152
|
-
|
|
168
|
+
|
|
169
|
+
# This is the preferred way to programmatically create a new connection (or
|
|
170
|
+
# switch to a new connection). If no connection was established, the
|
|
171
|
+
# playground will create a new one by calling this method with no arguments.
|
|
172
|
+
#
|
|
173
|
+
# With no argument, uses the connection specified in config/vanity.yml file
|
|
174
|
+
# for the current environment (RACK_ENV, RAILS_ENV or development). If there
|
|
175
|
+
# is no config/vanity.yml file, picks the configuration from
|
|
176
|
+
# config/redis.yml, or defaults to Redis on localhost, port 6379.
|
|
177
|
+
#
|
|
178
|
+
# If the argument is a symbol, uses the connection specified in
|
|
179
|
+
# config/vanity.yml for that environment. For example:
|
|
180
|
+
# Vanity.playground.establish_connection :production
|
|
181
|
+
#
|
|
182
|
+
# If the argument is a string, it is processed as a URL. For example:
|
|
183
|
+
# Vanity.playground.establish_connection "redis://redis.local/5"
|
|
184
|
+
#
|
|
185
|
+
# Otherwise, the argument is a hash and specifies the adapter name and any
|
|
186
|
+
# additional options understood by that adapter (as with config/vanity.yml).
|
|
187
|
+
# For example:
|
|
188
|
+
# Vanity.playground.establish_connection :adapter=>:redis,
|
|
189
|
+
# :host=>"redis.local"
|
|
190
|
+
#
|
|
191
|
+
# @since 1.4.0
|
|
192
|
+
def establish_connection(spec = nil)
|
|
193
|
+
disconnect! if @adapter
|
|
194
|
+
case spec
|
|
195
|
+
when nil
|
|
196
|
+
if File.exists?("config/vanity.yml")
|
|
197
|
+
env = ENV["RACK_ENV"] || ENV["RAILS_ENV"] || "development"
|
|
198
|
+
spec = YAML.load_file("config/vanity.yml")[env]
|
|
199
|
+
fail "No configuration for #{env}" unless spec
|
|
200
|
+
establish_connection spec
|
|
201
|
+
elsif File.exists?("config/redis.yml")
|
|
202
|
+
env = ENV["RACK_ENV"] || ENV["RAILS_ENV"] || "development"
|
|
203
|
+
redis = YAML.load_file("config/redis.yml")[env]
|
|
204
|
+
fail "No configuration for #{env}" unless redis
|
|
205
|
+
establish_connection "redis://" + redis
|
|
206
|
+
else
|
|
207
|
+
establish_connection :adapter=>"redis"
|
|
208
|
+
end
|
|
209
|
+
when Symbol
|
|
210
|
+
spec = YAML.load_file("config/vanity.yml")[spec.to_s]
|
|
211
|
+
establish_connection spec
|
|
153
212
|
when String
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
@connection_spec = nil
|
|
159
|
-
@redis = spec_or_connection
|
|
160
|
-
when :mock
|
|
161
|
-
@connection_spec = nil
|
|
162
|
-
@redis = MockRedis.new
|
|
213
|
+
uri = URI.parse(spec)
|
|
214
|
+
params = CGI.parse(uri.query) if uri.query
|
|
215
|
+
establish_connection :adapter=>uri.scheme, :username=>uri.user, :password=>uri.password,
|
|
216
|
+
:host=>uri.host, :port=>uri.port, :path=>uri.path, :params=>params
|
|
163
217
|
else
|
|
164
|
-
|
|
218
|
+
spec = spec.inject({}) { |hash,(k,v)| hash[k.to_sym] = v ; hash }
|
|
219
|
+
begin
|
|
220
|
+
require "vanity/adapters/#{spec[:adapter]}_adapter"
|
|
221
|
+
rescue LoadError
|
|
222
|
+
raise "Could not find #{spec[:adapter]} in your load path"
|
|
223
|
+
end
|
|
224
|
+
@adapter = Adapters.establish_connection(spec)
|
|
165
225
|
end
|
|
166
226
|
end
|
|
167
227
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
228
|
+
# Returns the current connection. Establishes new connection is necessary.
|
|
229
|
+
#
|
|
230
|
+
# @since 1.4.0
|
|
231
|
+
def connection
|
|
232
|
+
@adapter || establish_connection
|
|
171
233
|
end
|
|
172
234
|
|
|
235
|
+
# Returns true if connection is open.
|
|
236
|
+
#
|
|
237
|
+
# @since 1.4.0
|
|
173
238
|
def connected?
|
|
174
|
-
|
|
239
|
+
@adapter && @adapter.active?
|
|
175
240
|
end
|
|
176
241
|
|
|
242
|
+
# Closes the current connection.
|
|
243
|
+
#
|
|
244
|
+
# @since 1.4.0
|
|
177
245
|
def disconnect!
|
|
178
|
-
@
|
|
179
|
-
@redis = nil
|
|
246
|
+
@adapter.disconnect! if @adapter
|
|
180
247
|
end
|
|
181
248
|
|
|
249
|
+
# Closes the current connection and establishes a new one.
|
|
250
|
+
#
|
|
251
|
+
# @since 1.3.0
|
|
182
252
|
def reconnect!
|
|
183
|
-
|
|
184
|
-
disconnect! rescue nil
|
|
253
|
+
establish_connection
|
|
185
254
|
end
|
|
186
255
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
# Use this when testing to disable Redis (e.g. if your CI server doesn't
|
|
193
|
-
# have Redis).
|
|
194
|
-
#
|
|
195
|
-
# @example Put this in config/environments/test.rb
|
|
196
|
-
# config.after_initialize { Vanity.playground.test! }
|
|
256
|
+
# Deprecated. Use Vanity.playground.collecting = true/false instead. Under
|
|
257
|
+
# Rails, collecting is true in production environment, false in all other
|
|
258
|
+
# environments, which is exactly what you want.
|
|
197
259
|
def test!
|
|
198
|
-
|
|
260
|
+
warn "Deprecated: use collecting = false instead"
|
|
261
|
+
self.collecting = false
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Deprecated. Use establish_connection or configuration file instead.
|
|
265
|
+
def redis=(spec_or_connection)
|
|
266
|
+
warn "Deprecated: use establish_connection method instead"
|
|
267
|
+
case spec_or_connection
|
|
268
|
+
when String
|
|
269
|
+
establish_connection "redis://" + spec_or_connection
|
|
270
|
+
when ::Redis
|
|
271
|
+
@connection = Adapters::RedisAdapter.new(spec_or_connection)
|
|
272
|
+
when :mock
|
|
273
|
+
establish_connection :adapter=>:mock
|
|
274
|
+
else
|
|
275
|
+
raise "I don't know what to do with #{spec_or_connection.inspect}"
|
|
276
|
+
end
|
|
199
277
|
end
|
|
278
|
+
|
|
279
|
+
def redis
|
|
280
|
+
warn "Deprecated: use connection method instead"
|
|
281
|
+
connection
|
|
282
|
+
end
|
|
283
|
+
|
|
200
284
|
end
|
|
201
285
|
|
|
202
286
|
@playground = Playground.new
|
|
@@ -359,10 +359,10 @@ class AbTestTest < ActionController::TestCase
|
|
|
359
359
|
assert experiment(:abcd).score.alts.all? { |alt| alt.z_score.nan? }
|
|
360
360
|
assert experiment(:abcd).score.alts.all? { |alt| alt.probability == 0 }
|
|
361
361
|
assert experiment(:abcd).score.alts.all? { |alt| alt.difference.nil? }
|
|
362
|
-
|
|
362
|
+
assert_equal 1, experiment(:abcd).score.best.id
|
|
363
363
|
assert_nil experiment(:abcd).score.choice
|
|
364
|
-
|
|
365
|
-
|
|
364
|
+
assert_equal 2, experiment(:abcd).score.base.id
|
|
365
|
+
assert_equal 1, experiment(:abcd).score.least.id
|
|
366
366
|
end
|
|
367
367
|
|
|
368
368
|
def test_scoring_with_some_performers
|
|
@@ -647,6 +647,50 @@ This experiment did not run long enough to find a clear winner.
|
|
|
647
647
|
end
|
|
648
648
|
|
|
649
649
|
|
|
650
|
+
# -- No collection --
|
|
651
|
+
|
|
652
|
+
def test_no_collection_does_not_track
|
|
653
|
+
not_collecting!
|
|
654
|
+
metric "Coolness"
|
|
655
|
+
new_ab_test :abcd do
|
|
656
|
+
metrics :coolness
|
|
657
|
+
end
|
|
658
|
+
Vanity.playground.track! :coolness
|
|
659
|
+
assert_equal 0, experiment(:abcd).alternatives.sum(&:conversions)
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
def test_no_collection_and_completion
|
|
663
|
+
not_collecting!
|
|
664
|
+
new_ab_test :quick do
|
|
665
|
+
outcome_is { alternatives[1] }
|
|
666
|
+
metrics :coolness
|
|
667
|
+
end
|
|
668
|
+
experiment(:quick).complete!
|
|
669
|
+
assert_nil experiment(:quick).outcome
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
def test_no_collection_and_chooses
|
|
673
|
+
not_collecting!
|
|
674
|
+
new_ab_test :simple do
|
|
675
|
+
alternatives :a, :b, :c
|
|
676
|
+
end
|
|
677
|
+
assert !experiment(:simple).showing?(experiment(:simple).alternatives[1])
|
|
678
|
+
experiment(:simple).chooses(:b)
|
|
679
|
+
assert experiment(:simple).showing?(experiment(:simple).alternatives[1])
|
|
680
|
+
assert !experiment(:simple).showing?(experiment(:simple).alternatives[2])
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
def test_no_collection_chooses_without_database
|
|
684
|
+
not_collecting!
|
|
685
|
+
new_ab_test :simple do
|
|
686
|
+
alternatives :a, :b, :c
|
|
687
|
+
end
|
|
688
|
+
choice = experiment(:simple).choose
|
|
689
|
+
assert [:a, :b, :c].include?(choice)
|
|
690
|
+
assert_equal choice, experiment(:simple).choose
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
|
|
650
694
|
# -- Helper methods --
|
|
651
695
|
|
|
652
696
|
def fake(name, args)
|
|
@@ -30,12 +30,6 @@ class ExperimentTest < Test::Unit::TestCase
|
|
|
30
30
|
end
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
def test_uses_playground_namespace_for_experiment
|
|
34
|
-
new_ab_test(:ice_cream_flavor) { metrics :happiness }
|
|
35
|
-
assert_equal "vanity:#{Vanity::Version::MAJOR}:ice_cream_flavor", experiment(:ice_cream_flavor).send(:key)
|
|
36
|
-
assert_equal "vanity:#{Vanity::Version::MAJOR}:ice_cream_flavor:participants", experiment(:ice_cream_flavor).send(:key, "participants")
|
|
37
|
-
end
|
|
38
|
-
|
|
39
33
|
|
|
40
34
|
# -- Loading experiments --
|
|
41
35
|
|
|
@@ -80,7 +74,7 @@ class ExperimentTest < Test::Unit::TestCase
|
|
|
80
74
|
def test_reloading_experiments
|
|
81
75
|
new_ab_test(:ab) { metrics :happiness }
|
|
82
76
|
new_ab_test(:cd) { metrics :happiness }
|
|
83
|
-
|
|
77
|
+
assert_equal 2, Vanity.playground.experiments.size
|
|
84
78
|
Vanity.playground.reload!
|
|
85
79
|
assert Vanity.playground.experiments.empty?
|
|
86
80
|
end
|
|
@@ -114,7 +108,7 @@ class ExperimentTest < Test::Unit::TestCase
|
|
|
114
108
|
|
|
115
109
|
def test_experiment_keeps_created_timestamp_across_definitions
|
|
116
110
|
past = Date.today - 1
|
|
117
|
-
Timecop.
|
|
111
|
+
Timecop.freeze past do
|
|
118
112
|
new_ab_test(:ice_cream_flavor) { metrics :happiness }
|
|
119
113
|
assert_equal past.to_time.to_i, experiment(:ice_cream_flavor).created_at.to_i
|
|
120
114
|
end
|
|
@@ -133,4 +127,10 @@ class ExperimentTest < Test::Unit::TestCase
|
|
|
133
127
|
assert_equal "Because 31 is not enough ...", experiment(:ice_cream_flavor).description
|
|
134
128
|
end
|
|
135
129
|
|
|
130
|
+
def test_experiment_stores_nothing_when_collection_disabled
|
|
131
|
+
not_collecting!
|
|
132
|
+
new_ab_test(:ice_cream_flavor) { metrics :happiness }
|
|
133
|
+
experiment(:ice_cream_flavor).complete!
|
|
134
|
+
end
|
|
135
|
+
|
|
136
136
|
end
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
require "test/test_helper"
|
|
2
|
+
|
|
3
|
+
class Sky < ActiveRecord::Base
|
|
4
|
+
connection.drop_table :skies if table_exists?
|
|
5
|
+
connection.create_table :skies do |t|
|
|
6
|
+
t.integer :height
|
|
7
|
+
t.timestamps
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
named_scope :high, lambda { { :conditions=>"height >= 4" } }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
context "ActiveRecord Metric" do
|
|
15
|
+
|
|
16
|
+
test "record count" do
|
|
17
|
+
File.open "tmp/experiments/metrics/sky_is_limit.rb", "w" do |f|
|
|
18
|
+
f.write <<-RUBY
|
|
19
|
+
metric "Sky is limit" do
|
|
20
|
+
model Sky
|
|
21
|
+
end
|
|
22
|
+
RUBY
|
|
23
|
+
end
|
|
24
|
+
Vanity.playground.metrics
|
|
25
|
+
Sky.create!
|
|
26
|
+
assert_equal 1, Sky.count
|
|
27
|
+
assert_equal 1, Vanity::Metric.data(metric(:sky_is_limit)).last.last
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
test "record sum" do
|
|
31
|
+
File.open "tmp/experiments/metrics/sky_is_limit.rb", "w" do |f|
|
|
32
|
+
f.write <<-RUBY
|
|
33
|
+
metric "Sky is limit" do
|
|
34
|
+
model Sky, :sum=>:height
|
|
35
|
+
end
|
|
36
|
+
RUBY
|
|
37
|
+
end
|
|
38
|
+
Vanity.playground.metrics
|
|
39
|
+
Sky.create! :height=>4
|
|
40
|
+
Sky.create! :height=>2
|
|
41
|
+
assert_equal 6, Vanity::Metric.data(metric(:sky_is_limit)).last.last
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
test "record average" do
|
|
45
|
+
Sky.aggregates
|
|
46
|
+
File.open "tmp/experiments/metrics/sky_is_limit.rb", "w" do |f|
|
|
47
|
+
f.write <<-RUBY
|
|
48
|
+
metric "Sky is limit" do
|
|
49
|
+
model Sky, :average=>:height
|
|
50
|
+
end
|
|
51
|
+
RUBY
|
|
52
|
+
end
|
|
53
|
+
Vanity.playground.metrics
|
|
54
|
+
Sky.create! :height=>4
|
|
55
|
+
Sky.create! :height=>2
|
|
56
|
+
assert_equal 3, Vanity::Metric.data(metric(:sky_is_limit)).last.last
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
test "record minimum" do
|
|
60
|
+
Sky.aggregates
|
|
61
|
+
File.open "tmp/experiments/metrics/sky_is_limit.rb", "w" do |f|
|
|
62
|
+
f.write <<-RUBY
|
|
63
|
+
metric "Sky is limit" do
|
|
64
|
+
model Sky, :minimum=>:height
|
|
65
|
+
end
|
|
66
|
+
RUBY
|
|
67
|
+
end
|
|
68
|
+
Vanity.playground.metrics
|
|
69
|
+
Sky.create! :height=>4
|
|
70
|
+
Sky.create! :height=>2
|
|
71
|
+
assert_equal 2, Vanity::Metric.data(metric(:sky_is_limit)).last.last
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
test "record maximum" do
|
|
75
|
+
Sky.aggregates
|
|
76
|
+
File.open "tmp/experiments/metrics/sky_is_limit.rb", "w" do |f|
|
|
77
|
+
f.write <<-RUBY
|
|
78
|
+
metric "Sky is limit" do
|
|
79
|
+
model Sky, :maximum=>:height
|
|
80
|
+
end
|
|
81
|
+
RUBY
|
|
82
|
+
end
|
|
83
|
+
Vanity.playground.metrics
|
|
84
|
+
Sky.create! :height=>4
|
|
85
|
+
Sky.create! :height=>2
|
|
86
|
+
assert_equal 4, Vanity::Metric.data(metric(:sky_is_limit)).last.last
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
test "with conditions" do
|
|
90
|
+
File.open "tmp/experiments/metrics/sky_is_limit.rb", "w" do |f|
|
|
91
|
+
f.write <<-RUBY
|
|
92
|
+
metric "Sky is limit" do
|
|
93
|
+
model Sky, :sum=>:height, :conditions=>["height > 4"]
|
|
94
|
+
end
|
|
95
|
+
RUBY
|
|
96
|
+
end
|
|
97
|
+
Vanity.playground.metrics
|
|
98
|
+
high_skies = 0
|
|
99
|
+
metric(:sky_is_limit).hook do |metric_id, timestamp, height|
|
|
100
|
+
assert height > 4
|
|
101
|
+
high_skies += height
|
|
102
|
+
end
|
|
103
|
+
[nil,5,3,6].each do |height|
|
|
104
|
+
Sky.create! :height=>height
|
|
105
|
+
end
|
|
106
|
+
assert_equal 11, Vanity::Metric.data(metric(:sky_is_limit)).sum(&:last)
|
|
107
|
+
assert_equal 11, high_skies
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
test "with scope" do
|
|
111
|
+
Sky.aggregates
|
|
112
|
+
File.open "tmp/experiments/metrics/sky_is_limit.rb", "w" do |f|
|
|
113
|
+
f.write <<-RUBY
|
|
114
|
+
metric "Sky is limit" do
|
|
115
|
+
model Sky.high
|
|
116
|
+
end
|
|
117
|
+
RUBY
|
|
118
|
+
end
|
|
119
|
+
Vanity.playground.metrics
|
|
120
|
+
total = 0
|
|
121
|
+
metric(:sky_is_limit).hook do |metric_id, timestamp, count|
|
|
122
|
+
total += count
|
|
123
|
+
end
|
|
124
|
+
Sky.create! :height=>4
|
|
125
|
+
Sky.create! :height=>2
|
|
126
|
+
assert_equal 1, Vanity::Metric.data(metric(:sky_is_limit)).last.last
|
|
127
|
+
assert_equal 1, total
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
test "hooks" do
|
|
131
|
+
File.open "tmp/experiments/metrics/sky_is_limit.rb", "w" do |f|
|
|
132
|
+
f.write <<-RUBY
|
|
133
|
+
metric "Sky is limit" do
|
|
134
|
+
model Sky, :sum=>:height
|
|
135
|
+
end
|
|
136
|
+
RUBY
|
|
137
|
+
end
|
|
138
|
+
Vanity.playground.metrics
|
|
139
|
+
total = 0
|
|
140
|
+
metric(:sky_is_limit).hook do |metric_id, timestamp, count|
|
|
141
|
+
assert_equal :sky_is_limit, metric_id
|
|
142
|
+
assert_in_delta Time.now.to_i, timestamp.to_i, 1
|
|
143
|
+
total += count
|
|
144
|
+
end
|
|
145
|
+
Sky.create! :height=>4
|
|
146
|
+
assert_equal 4, total
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
test "no hooks when metrics disabled" do
|
|
150
|
+
not_collecting!
|
|
151
|
+
File.open "tmp/experiments/metrics/sky_is_limit.rb", "w" do |f|
|
|
152
|
+
f.write <<-RUBY
|
|
153
|
+
metric "Sky is limit" do
|
|
154
|
+
model Sky, :sum=>:height
|
|
155
|
+
end
|
|
156
|
+
RUBY
|
|
157
|
+
end
|
|
158
|
+
Vanity.playground.metrics
|
|
159
|
+
total = 0
|
|
160
|
+
metric(:sky_is_limit).hook do |metric_id, timestamp, count|
|
|
161
|
+
total += count
|
|
162
|
+
end
|
|
163
|
+
Sky.create! :height=>4
|
|
164
|
+
assert_equal 0, total
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
test "after_create not after_save" do
|
|
168
|
+
File.open "tmp/experiments/metrics/sky_is_limit.rb", "w" do |f|
|
|
169
|
+
f.write <<-RUBY
|
|
170
|
+
metric "Sky is limit" do
|
|
171
|
+
model Sky
|
|
172
|
+
end
|
|
173
|
+
RUBY
|
|
174
|
+
end
|
|
175
|
+
Vanity.playground.metrics
|
|
176
|
+
once = nil
|
|
177
|
+
metric(:sky_is_limit).hook do
|
|
178
|
+
fail "Metric tracked twice" if once
|
|
179
|
+
once = true
|
|
180
|
+
end
|
|
181
|
+
Sky.create!
|
|
182
|
+
Sky.last.update_attributes :height=>4
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
test "with after_save" do
|
|
186
|
+
File.open "tmp/experiments/metrics/sky_is_limit.rb", "w" do |f|
|
|
187
|
+
f.write <<-RUBY
|
|
188
|
+
metric "Sky is limit" do
|
|
189
|
+
model Sky, :conditions=>["height > 3"]
|
|
190
|
+
Sky.after_save { |sky| track! if sky.height_changed? && sky.height > 3 }
|
|
191
|
+
end
|
|
192
|
+
RUBY
|
|
193
|
+
end
|
|
194
|
+
Vanity.playground.metrics
|
|
195
|
+
times = 0
|
|
196
|
+
metric(:sky_is_limit).hook do
|
|
197
|
+
times += 1
|
|
198
|
+
end
|
|
199
|
+
Sky.create!
|
|
200
|
+
(1..5).each do |height|
|
|
201
|
+
Sky.last.update_attributes! :height=>height
|
|
202
|
+
end
|
|
203
|
+
assert_equal 2, times
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
test "do it youself" do
|
|
207
|
+
File.open "tmp/experiments/metrics/sky_is_limit.rb", "w" do |f|
|
|
208
|
+
f.write <<-RUBY
|
|
209
|
+
metric "Sky is limit" do
|
|
210
|
+
Sky.after_save { |sky| track! if sky.height_changed? && sky.height > 3 }
|
|
211
|
+
end
|
|
212
|
+
RUBY
|
|
213
|
+
end
|
|
214
|
+
Vanity.playground.metrics
|
|
215
|
+
(1..5).each do |height|
|
|
216
|
+
Sky.create! :height=>height
|
|
217
|
+
end
|
|
218
|
+
Sky.first.update_attributes! :height=>4
|
|
219
|
+
assert_equal 3, Vanity::Metric.data(metric(:sky_is_limit)).last.last
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
test "last update for new metric" do
|
|
223
|
+
File.open "tmp/experiments/metrics/sky_is_limit.rb", "w" do |f|
|
|
224
|
+
f.write <<-RUBY
|
|
225
|
+
metric "Sky is limit" do
|
|
226
|
+
model Sky
|
|
227
|
+
end
|
|
228
|
+
RUBY
|
|
229
|
+
end
|
|
230
|
+
assert_nil metric(:sky_is_limit).last_update_at
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
test "last update with records" do
|
|
234
|
+
File.open "tmp/experiments/metrics/sky_is_limit.rb", "w" do |f|
|
|
235
|
+
f.write <<-RUBY
|
|
236
|
+
metric "Sky is limit" do
|
|
237
|
+
model Sky
|
|
238
|
+
end
|
|
239
|
+
RUBY
|
|
240
|
+
end
|
|
241
|
+
Sky.create! :height=>1
|
|
242
|
+
Timecop.freeze Time.now + 1.day do
|
|
243
|
+
Sky.create! :height=>1
|
|
244
|
+
end
|
|
245
|
+
assert_in_delta metric(:sky_is_limit).last_update_at.to_i, (Time.now + 1.day).to_i, 1
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
teardown do
|
|
249
|
+
Sky.delete_all
|
|
250
|
+
Sky.after_create.clear
|
|
251
|
+
Sky.after_save.clear
|
|
252
|
+
end
|
|
253
|
+
end
|