mikeg-vanity 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. data/CHANGELOG +153 -0
  2. data/MIT-LICENSE +21 -0
  3. data/README.rdoc +83 -0
  4. data/bin/vanity +53 -0
  5. data/lib/vanity.rb +38 -0
  6. data/lib/vanity/backport.rb +43 -0
  7. data/lib/vanity/commands.rb +2 -0
  8. data/lib/vanity/commands/list.rb +21 -0
  9. data/lib/vanity/commands/report.rb +60 -0
  10. data/lib/vanity/experiment/ab_test.rb +477 -0
  11. data/lib/vanity/experiment/base.rb +212 -0
  12. data/lib/vanity/helpers.rb +59 -0
  13. data/lib/vanity/metric/active_record.rb +77 -0
  14. data/lib/vanity/metric/base.rb +221 -0
  15. data/lib/vanity/metric/google_analytics.rb +70 -0
  16. data/lib/vanity/mock_redis.rb +76 -0
  17. data/lib/vanity/playground.rb +197 -0
  18. data/lib/vanity/rails.rb +22 -0
  19. data/lib/vanity/rails/dashboard.rb +24 -0
  20. data/lib/vanity/rails/helpers.rb +158 -0
  21. data/lib/vanity/rails/testing.rb +11 -0
  22. data/lib/vanity/templates/_ab_test.erb +26 -0
  23. data/lib/vanity/templates/_experiment.erb +5 -0
  24. data/lib/vanity/templates/_experiments.erb +7 -0
  25. data/lib/vanity/templates/_metric.erb +14 -0
  26. data/lib/vanity/templates/_metrics.erb +13 -0
  27. data/lib/vanity/templates/_report.erb +27 -0
  28. data/lib/vanity/templates/flot.min.js +1 -0
  29. data/lib/vanity/templates/jquery.min.js +19 -0
  30. data/lib/vanity/templates/vanity.css +26 -0
  31. data/lib/vanity/templates/vanity.js +82 -0
  32. data/test/ab_test_test.rb +656 -0
  33. data/test/experiment_test.rb +136 -0
  34. data/test/experiments/age_and_zipcode.rb +19 -0
  35. data/test/experiments/metrics/cheers.rb +3 -0
  36. data/test/experiments/metrics/signups.rb +2 -0
  37. data/test/experiments/metrics/yawns.rb +3 -0
  38. data/test/experiments/null_abc.rb +5 -0
  39. data/test/metric_test.rb +518 -0
  40. data/test/playground_test.rb +10 -0
  41. data/test/rails_test.rb +104 -0
  42. data/test/test_helper.rb +135 -0
  43. data/vanity.gemspec +18 -0
  44. data/vendor/redis-rb/LICENSE +20 -0
  45. data/vendor/redis-rb/README.markdown +36 -0
  46. data/vendor/redis-rb/Rakefile +62 -0
  47. data/vendor/redis-rb/bench.rb +44 -0
  48. data/vendor/redis-rb/benchmarking/suite.rb +24 -0
  49. data/vendor/redis-rb/benchmarking/worker.rb +71 -0
  50. data/vendor/redis-rb/bin/distredis +33 -0
  51. data/vendor/redis-rb/examples/basic.rb +16 -0
  52. data/vendor/redis-rb/examples/incr-decr.rb +18 -0
  53. data/vendor/redis-rb/examples/list.rb +26 -0
  54. data/vendor/redis-rb/examples/sets.rb +36 -0
  55. data/vendor/redis-rb/lib/dist_redis.rb +124 -0
  56. data/vendor/redis-rb/lib/hash_ring.rb +128 -0
  57. data/vendor/redis-rb/lib/pipeline.rb +21 -0
  58. data/vendor/redis-rb/lib/redis.rb +370 -0
  59. data/vendor/redis-rb/lib/redis/raketasks.rb +1 -0
  60. data/vendor/redis-rb/profile.rb +22 -0
  61. data/vendor/redis-rb/redis-rb.gemspec +30 -0
  62. data/vendor/redis-rb/spec/redis_spec.rb +637 -0
  63. data/vendor/redis-rb/spec/spec_helper.rb +4 -0
  64. data/vendor/redis-rb/speed.rb +16 -0
  65. data/vendor/redis-rb/tasks/redis.tasks.rb +140 -0
  66. metadata +125 -0
@@ -0,0 +1,153 @@
1
+ == 1.3.0
2
+ This release adds support for Google Analytics and AdWords.
3
+
4
+ To view Google Analytics metrics from within Vanity, first make sure you are using Garb. For example, in your Gemfile:
5
+
6
+ gem "vanity", "1.3.0"
7
+ gem "garb", "0.5.0"
8
+
9
+ Next, authenticate using your account credentials. For example, in your config/environments/production.rb:
10
+
11
+ require "garb"
12
+ Garb::Session.login('..email..', '..password..', account_type: "GOOGLE") rescue nil
13
+
14
+ Last, define Vanity metrics that tap to Google Analytics metrics. For example:
15
+
16
+ metric "Acquisition: Visitors" do
17
+ description "Unique visitors on any given page, as tracked by Google Analytics"
18
+ google_analytics "UA-10087469-2", :visitors
19
+ end
20
+
21
+ * Added: Support for Google Analytics metrics, thanks to Tony Pitale's Garb and blog post: http://www.viget.com/extend/user-goal-tracking-in-rails-with-vanity-and-google-analytics/
22
+ * Added: Vanity query parameter that you can use to choose a particular alternative, e.g. to tie an advertisement banner with content of the site.
23
+ * Added: Command line "vanity list" catalogs all ongoing experiments, their alternatives (and fingerprints) and all metrics.
24
+ * Changed: Rails integration now separates use_vanity method, filters and helpers.
25
+ * Changed: Explicit vanity_context_filter and vanity_reload_filter so you can skip them, or order filters relative to them.
26
+ * Fixed: if metric cannot be loaded (e.g. offline, no db access) show error message for that metric but don't break dashboard.
27
+ * Removed: Vanity.playground.define is deprecated. Bad choice for a method name. If you need this feature, make a suggestion and let's create a better API.
28
+
29
+ == 1.2.0 (2009-12-14)
30
+ This release introduces metrics backed by ActiveRecord. Use them when your model is already tracking a metric, and you get instant historical data.
31
+
32
+ Example, track sign ups using User model:
33
+
34
+ metric "Signups" do
35
+ model Account
36
+ end
37
+
38
+ Example, track satisfaction using Survey model:
39
+ metric "Satisfaction" do
40
+ model Survey, :average=>:rating
41
+ end
42
+
43
+ Example, track only high ratings:
44
+ metric "High ratings" do
45
+ model Rating, :conditions=>["stars >= 4"]
46
+ end
47
+
48
+ There's no need to call track! on these metrics.
49
+
50
+ * Added: Metrics backed by ActiveRecord.
51
+ * Added: track! and ab_test methods now available from Object (i.e. everywhere).
52
+ * Added: Playground.load!. Now loading all metrics and experiments from Rails initializer.
53
+ * 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).
54
+ * Changed: Metrics no longer defined on-demand, i.e. calling playground.metric either returns existing metric or raises exception.
55
+ * Changed: Playground.experiments returns hash instead of array.
56
+ * Changed: All dates in report are UTC, since we don't know which locale to use.
57
+ * Removed: Object.experiment is deprecated, please call Vanity.playground.experiment directly.
58
+ * Fixed: Playground no longer changes logging level on supplied logger.
59
+
60
+ == 1.1.1 (2009-12-4)
61
+ * Fixed: Binding issue that shows up on 1.8.6/7.
62
+
63
+ == 1.1.0 (2009-12-4)
64
+ This release introduces metrics. Metrics are the gateway drug to better software.
65
+
66
+ It’s as simple as defining a metric:
67
+
68
+ metric "Cheers" do
69
+ description "They love us, don't they?"
70
+ end
71
+
72
+ Tracking it from your code:
73
+
74
+ track! :cheers
75
+
76
+ And watching the graph from the Dashboard.
77
+
78
+ You can (should) also use metrics with your A/B tests, for example:
79
+
80
+ ab_test "Pricing options" do
81
+ metrics :signup
82
+ alternatives 15, 25, 29
83
+ end
84
+
85
+ This new usage may become requirement in a future release.
86
+
87
+ Much thanks to Ian Sefferman for fixing issues with Ruby 1.8.7 and Rails support.
88
+
89
+ * Added: Metrics.
90
+ * Added: Use Vanity.playground.mock! when running tests and you'd rather not access a live Redis server.
91
+ * Changed: A/B tests now using metrics for tracking.
92
+ * Changed: Now throwing NameError instead of LoadError when failing to load experiment/metric. NameError can be rescued on same line.
93
+ * Changed: New, easier URL mapping for Dashboard: map.vanity "/vanity", :controller=>:vanity.
94
+ * Changed: All tests are green on Ruby 1.8.6, 1.8.7 and 1.9.1.
95
+ * Changed: Switched to redis-rb from http://github.com/ezmobius/redis-rb.
96
+ * Deprecated: Please call experiment method with experiment identifier (a symbol) and not experiment name.
97
+
98
+ == 1.0.0 (2009-11-19)
99
+ This release changes the way you define a new experiment. You can use a method suitable for the type of experiment you want to define, or call the generic define method (previously: experiment method). For example:
100
+
101
+ ab_test "My A/B test" do
102
+ alternatives :a, :b
103
+ end
104
+
105
+ The experiment method is no longer overloaded: it looks up an experiment (loading its definition if necessary), but does not define an experiment. The ab_test method is overloaded, though this may change in the future.
106
+
107
+ In addition, the ab_goal! method is now track!. This method may be used for other tests in the future.
108
+
109
+ * Added: A/B test report now showing number of participants.
110
+ * Added: AbTest.score method accepts minimum probability (default 90), and
111
+ * Removed: Experiment.reset! method. Destroy and save have the same effect.
112
+ * Changed: Playground.define now requires an experiment type, ab_test is not the default any more.
113
+ * Changed: When you run Vanity in development mode (configuration.cache_classes = false), it will reload experiments on each request. You can also Vanity.playground.reload!.
114
+ * Changed: Fancy AJAX trickery in Rails console.
115
+ * Changed: You can break long experiment descriptions into multiple paragraphs using two consecutive newlines.
116
+ * Changed: AbTest confidence becomes probability; only returns choice alternative with probability equal or higher than that.
117
+ * Changed: ab_goal! becomes track!.
118
+ * Changed: Console becomes Dashboard, which is less confusing with rails console (script/console).
119
+
120
+ == 0.3.1 (2009-11-13)
121
+ * Changed: Redis 1.0 is now vendored into Vanity. This means one less dependecy ... actually two, since Redis brings with it RSpec.
122
+
123
+ == 0.3.0 (2009-11-13)
124
+ * Added: score now includes least performing alternatives, names and values.
125
+ * Added: shiny reports.
126
+ * Added: Rails console shows current experiments status and also allows you to choose which alternative you want to see.
127
+ * Changed: letters instead of numbers for options (option 1 => option A).
128
+ * Changed: experiment.alternatives is now an immutable snapshot.
129
+ * Changed: experiment.score returns populated alternative objects instead of structs.
130
+ * Changed: experiment.chooses uses Redis to store state, better for (when we get to) browser integration.
131
+ * Changed: experiment.chooses skips recording participant or conversion.
132
+ * Changed: to MIT license.
133
+
134
+ == 0.2.2 (2009-11-12)
135
+ * Added: vanity binary, with single command for generating a report.
136
+ * Added: return alternative by value from experiment.alternative(val) method.
137
+ * Added: reset an experiment by calling reset!.
138
+ * Added: experiment alternative name (option 1, option 2, etc).
139
+ * Added: new scoring algorithm: use experiment.score instead of alternative.z_score/confidence.
140
+ * Added: experiment.conclusion for plain English results.
141
+
142
+ == 0.2.1 (2009-11-11)
143
+ * Added: z-score and confidence level for A/B test alternatives.
144
+ * Added: test auto-completion and auto-outcome (complete_it, outcome_is).
145
+ * Changed: default alternatives are now false/true, so if can't decide outcome, fall back on false.
146
+
147
+ == 0.2.0 (2009-11-10)
148
+ * Added: experiment method on object, used to define and access experiments.
149
+ * Added: playground configuration (Vanity.playground.namespace = , etc).
150
+ * Added: use_vanity now accepts block instead of symbol.
151
+ * Changed: Vanity::Helpers becomes Vanity::Rails.
152
+ * Changed: A/B test experiments alternatives now handled using Alternatives object.
153
+ * Removed: A/B test measure method no longer in use.
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2009 Assaf Arkin
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
@@ -0,0 +1,83 @@
1
+ Vanity is an Experiment Driven Development framework for Rails.
2
+
3
+ * All about Vanity: http://vanity.labnotes.org
4
+ * On github: http://github.com/assaf/vanity
5
+ * Vanity requires Redis 1.0 or later.
6
+
7
+ http://farm3.static.flickr.com/2540/4099665871_497f274f68_o.jpg
8
+
9
+
10
+ == A/B Testing With Rails (In 5 Easy Steps)
11
+
12
+ <b>Step 1:</b> Start using Vanity in your Rails application:
13
+
14
+ gem.config "vanity"
15
+
16
+ And:
17
+
18
+ class ApplicationController < ActionController::Base
19
+ use_vanity :current_user
20
+ end
21
+
22
+ <b>Step 2:</b> Define your first A/B test. This experiment goes in the file <code>experiments/price_options.rb</code>:
23
+
24
+ ab_test "Price options" do
25
+ description "Mirror, mirror on the wall, who's the better price of all?"
26
+ alternatives 19, 25, 29
27
+ metrics :signups
28
+ end
29
+
30
+ <b>Step 3:</b> Present the different options to your users:
31
+
32
+ <h2>Get started for only $<%= ab_test :price_options %> a month!</h2>
33
+
34
+ <b>Step 4:</b> Measure conversion:
35
+
36
+ class SignupController < ApplicationController
37
+ def signup
38
+ @account = Account.new(params[:account])
39
+ if @account.save
40
+ track! :signups
41
+ redirect_to @acccount
42
+ else
43
+ render action: :offer
44
+ end
45
+ end
46
+ end
47
+
48
+ <b>Step 5:</b> Check the report:
49
+
50
+ vanity --output vanity.html
51
+
52
+
53
+ == Building From Source
54
+
55
+ To run the test suite for the first time:
56
+
57
+ $ gem install rails mocha timecop
58
+ $ rake
59
+
60
+ You can also +rake test+ if you insist on being explicit.
61
+
62
+ To build the documentation:
63
+
64
+ $ gem install yardoc jekyll
65
+ $ rake docs
66
+ $ open html/index.html
67
+
68
+ To clean up after yourself:
69
+
70
+ $ rake clobber
71
+
72
+ To package Vanity as a gem and install on your machine:
73
+
74
+ $ rake install
75
+
76
+
77
+ == Credits/License
78
+
79
+ Original code, copyright of Assaf Arkin, released under the MIT license.
80
+
81
+ Documentation available under the Creative Commons Attribution license.
82
+
83
+ For full list of credits and licenses: http://vanity.labnotes.org/credits.html.
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env ruby
2
+ path = File.expand_path("../lib", File.dirname(__FILE__))
3
+ $LOAD_PATH.unshift path unless $LOAD_PATH.include?(path)
4
+
5
+ require "vanity"
6
+ require "optparse"
7
+
8
+ playground = Vanity.playground
9
+ options = Struct.new(:output).new
10
+ opts = OptionParser.new("", 24, " ") do |opts|
11
+ opts.banner = "Usage: #{File.basename($0)} [options] command\n"
12
+ opts.banner << "Commands:\n"
13
+ opts.banner << " report Report on all running experiments/metrics"
14
+ opts.banner << " list List all experiments and metrics"
15
+
16
+ opts.separator ""
17
+ opts.separator "General options:"
18
+ opts.on("--path PATH", "Path to experiments directory (default: #{playground.load_path})") { |v| playground.load_path = v }
19
+ opts.on("--output FILE", "Write report to this file (default: stdout)") { |v| options.output = v }
20
+
21
+ opts.separator ""
22
+ opts.separator "Redis options:"
23
+ opts.on("--host HOST", "Redis server host (default: #{playground.host})") { |v| playground.host = v }
24
+ opts.on("--port PORT", "Redis server port (default: #{playground.port})") { |v| playground.port = v }
25
+ opts.on("--db DB", "Redis database (default: #{playground.db})") { |v| playground.db = v }
26
+ opts.on("--password PWD", "Redis database password") { |v| playground.password = v }
27
+ opts.on("--namespace NS", "Redis namespace (default: #{playground.namespace})") { |v| playground.namespace = v }
28
+
29
+ opts.separator ""
30
+ opts.separator "Common options:"
31
+ opts.on_tail "-h", "-H", "--help", "Show this message" do
32
+ puts opts.to_s.gsub(/^.*DEPRECATED.*$/s, '')
33
+ exit
34
+ end
35
+ opts.on_tail "-v", "--version", "Show version" do
36
+ puts "Vanity #{Vanity::Version::STRING}"
37
+ exit
38
+ end
39
+ end
40
+
41
+ opts.parse!(ARGV)
42
+ if ARGV.empty?
43
+ puts opts.banner
44
+ exit
45
+ end
46
+
47
+ ARGV.each do |cmd|
48
+ case cmd
49
+ when "report"; Vanity::Commands.report options.output
50
+ when "list"; Vanity::Commands.list
51
+ else fail "No such command: #{cmd}"
52
+ end
53
+ end
@@ -0,0 +1,38 @@
1
+ require "date"
2
+ require "time"
3
+ require "logger"
4
+
5
+ $LOAD_PATH << File.join(File.dirname(__FILE__), "../vendor/redis-rb/lib")
6
+ autoload :Redis, "redis"
7
+
8
+ # All the cool stuff happens in other places.
9
+ # @see Vanity::Helper
10
+ # @see Vanity::Rails
11
+ # @see Vanity::Playground
12
+ # @see Vanity::Metric
13
+ # @see Vanity::Experiment
14
+ module Vanity
15
+ # Version number.
16
+ module Version
17
+ version = Gem::Specification.load(File.expand_path("../vanity.gemspec", File.dirname(__FILE__))).version.to_s.split(".").map { |i| i.to_i }
18
+ MAJOR = version[0]
19
+ MINOR = version[1]
20
+ PATCH = version[2]
21
+ STRING = "#{MAJOR}.#{MINOR}.#{PATCH}"
22
+ end
23
+ end
24
+
25
+ require "vanity/backport" if RUBY_VERSION < "1.9"
26
+ # Metrics.
27
+ require "vanity/metric/base"
28
+ require "vanity/metric/active_record"
29
+ require "vanity/metric/google_analytics" if defined?(Garb)
30
+ # Experiments.
31
+ require "vanity/experiment/base"
32
+ require "vanity/experiment/ab_test"
33
+ # Playground.
34
+ require "vanity/playground"
35
+ require "vanity/helpers"
36
+ Vanity.autoload :MockRedis, "vanity/mock_redis"
37
+ Vanity.autoload :Commands, "vanity/commands"
38
+ require "vanity/rails" if defined?(Rails)
@@ -0,0 +1,43 @@
1
+ class Time
2
+ unless method_defined?(:to_date)
3
+ # Backported from Ruby 1.9.
4
+ def to_date
5
+ jd = Date.__send__(:civil_to_jd, year, mon, mday, Date::ITALY)
6
+ Date.new!(Date.__send__(:jd_to_ajd, jd, 0, 0), 0, Date::ITALY)
7
+ end
8
+ end
9
+ end
10
+
11
+ class Date
12
+ unless method_defined?(:to_date)
13
+ # Backported from Ruby 1.9.
14
+ def to_date
15
+ self
16
+ end
17
+ end
18
+
19
+ unless method_defined?(:to_time)
20
+ # Backported from Ruby 1.9.
21
+ def to_time
22
+ Time.local(year, mon, mday)
23
+ end
24
+ end
25
+ end
26
+
27
+ class Symbol
28
+ unless method_defined?(:to_proc)
29
+ # Backported from Ruby 1.9.
30
+ def to_proc
31
+ Proc.new { |*args| args.shift.__send__(self, *args) }
32
+ end
33
+ end
34
+ end
35
+
36
+ class Array
37
+ unless method_defined?(:minmax)
38
+ # Backported from Ruby 1.9.
39
+ def minmax
40
+ [min, max]
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,2 @@
1
+ require "vanity/commands/report"
2
+ require "vanity/commands/list"
@@ -0,0 +1,21 @@
1
+ module Vanity
2
+ module Commands
3
+ class << self
4
+ # Lists all experiments and metrics.
5
+ def list
6
+ Vanity.playground.experiments.each do |id, experiment|
7
+ puts "experiment :%-.20s (%-.40s)" % [id, experiment.name]
8
+ if experiment.respond_to?(:alternatives)
9
+ experiment.alternatives.each do |alt|
10
+ hash = experiment.fingerprint(alt)
11
+ puts " %s: %-40.40s (%s)" % [alt.name, alt.value, hash]
12
+ end
13
+ end
14
+ end
15
+ Vanity.playground.metrics.each do |id, metric|
16
+ puts "metric :%-.20s (%-.40s)" % [id, metric.name]
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,60 @@
1
+ require "erb"
2
+ require "cgi"
3
+
4
+ module Vanity
5
+
6
+ # Render method available to templates (when used by Vanity command line,
7
+ # outside Rails).
8
+ module Render
9
+
10
+ # Render the named template. Used for reporting and the dashboard.
11
+ def render(path, locals = {})
12
+ locals[:playground] = self
13
+ keys = locals.keys
14
+ struct = Struct.new(*keys)
15
+ struct.send :include, Render
16
+ locals = struct.new(*locals.values_at(*keys))
17
+ dir, base = File.split(path)
18
+ path = File.join(dir, "_#{base}")
19
+ erb = ERB.new(File.read(path), nil, '<>')
20
+ erb.filename = path
21
+ erb.result(locals.instance_eval { binding })
22
+ end
23
+
24
+ # Escape HTML.
25
+ def h(html)
26
+ CGI.escapeHTML(html)
27
+ end
28
+
29
+ # Dumbed down from Rails' simple_format.
30
+ def simple_format(text, options={})
31
+ open = "<p #{options.map { |k,v| "#{k}=\"#{CGI.escapeHTML v}\"" }.join(" ")}>"
32
+ text = open + text.gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
33
+ gsub(/\n\n+/, "</p>\n\n#{open}"). # 2+ newline -> paragraph
34
+ gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') + # 1 newline -> br
35
+ "</p>"
36
+ end
37
+ end
38
+
39
+ # Commands available when running Vanity from the command line (see bin/vanity).
40
+ module Commands
41
+ class << self
42
+ include Render
43
+
44
+ # Generate an HTML report. Outputs to the named file, or stdout with no
45
+ # arguments.
46
+ def report(output = nil)
47
+ html = render(Vanity.template("report"))
48
+ if output
49
+ File.open output, 'w' do |file|
50
+ file.write html
51
+ end
52
+ puts "New report available in #{output}"
53
+ else
54
+ $stdout.write html
55
+ end
56
+ end
57
+
58
+ end
59
+ end
60
+ end