vanity 1.2.0 → 1.3.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.
Files changed (69) hide show
  1. data/CHANGELOG +34 -0
  2. data/Gemfile +16 -0
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +10 -5
  5. data/Rakefile +119 -0
  6. data/bin/vanity +23 -18
  7. data/lib/vanity.rb +12 -4
  8. data/lib/vanity/commands.rb +1 -0
  9. data/lib/vanity/commands/list.rb +21 -0
  10. data/lib/vanity/experiment/ab_test.rb +8 -1
  11. data/lib/vanity/experiment/base.rb +40 -30
  12. data/lib/vanity/frameworks/rails.rb +222 -0
  13. data/lib/vanity/metric/active_record.rb +77 -0
  14. data/lib/vanity/{metric.rb → metric/base.rb} +6 -71
  15. data/lib/vanity/metric/google_analytics.rb +76 -0
  16. data/lib/vanity/playground.rb +93 -44
  17. data/lib/vanity/templates/_metric.erb +12 -7
  18. data/lib/vanity/templates/vanity.css +1 -0
  19. data/test/ab_test_test.rb +69 -48
  20. data/test/experiment_test.rb +29 -15
  21. data/test/metric_test.rb +104 -0
  22. data/test/myapp/app/controllers/application_controller.rb +2 -0
  23. data/test/myapp/app/controllers/main_controller.rb +7 -0
  24. data/test/myapp/config/boot.rb +110 -0
  25. data/test/myapp/config/environment.rb +10 -0
  26. data/test/myapp/config/environments/production.rb +0 -0
  27. data/test/myapp/config/routes.rb +3 -0
  28. data/test/myapp/log/production.log +80 -0
  29. data/test/passenger_test.rb +34 -0
  30. data/test/rails_test.rb +129 -1
  31. data/test/test_helper.rb +12 -4
  32. data/vanity.gemspec +2 -2
  33. data/vendor/cache/RedCloth-4.2.2.gem +0 -0
  34. data/vendor/cache/actionmailer-2.3.5.gem +0 -0
  35. data/vendor/cache/actionpack-2.3.5.gem +0 -0
  36. data/vendor/cache/activerecord-2.3.5.gem +0 -0
  37. data/vendor/cache/activeresource-2.3.5.gem +0 -0
  38. data/vendor/cache/activesupport-2.3.5.gem +0 -0
  39. data/vendor/cache/autotest-4.2.7.gem +0 -0
  40. data/vendor/cache/autotest-fsevent-0.2.1.gem +0 -0
  41. data/vendor/cache/autotest-growl-0.2.0.gem +0 -0
  42. data/vendor/cache/bundler-0.9.7.gem +0 -0
  43. data/vendor/cache/classifier-1.3.1.gem +0 -0
  44. data/vendor/cache/directory_watcher-1.3.1.gem +0 -0
  45. data/vendor/cache/fastthread-1.0.7.gem +0 -0
  46. data/vendor/cache/garb-0.7.0.gem +0 -0
  47. data/vendor/cache/happymapper-0.3.0.gem +0 -0
  48. data/vendor/cache/jekyll-0.5.7.gem +0 -0
  49. data/vendor/cache/libxml-ruby-1.1.3.gem +0 -0
  50. data/vendor/cache/liquid-2.0.0.gem +0 -0
  51. data/vendor/cache/maruku-0.6.0.gem +0 -0
  52. data/vendor/cache/mocha-0.9.8.gem +0 -0
  53. data/vendor/cache/open4-1.0.1.gem +0 -0
  54. data/vendor/cache/passenger-2.2.9.gem +0 -0
  55. data/vendor/cache/rack-1.0.1.gem +0 -0
  56. data/vendor/cache/rails-2.3.5.gem +0 -0
  57. data/vendor/cache/rake-0.8.7.gem +0 -0
  58. data/vendor/cache/rubygems-update-1.3.5.gem +0 -0
  59. data/vendor/cache/shoulda-2.10.3.gem +0 -0
  60. data/vendor/cache/sqlite3-ruby-1.2.5.gem +0 -0
  61. data/vendor/cache/stemmer-1.0.1.gem +0 -0
  62. data/vendor/cache/syntax-1.0.0.gem +0 -0
  63. data/vendor/cache/sys-uname-0.8.4.gem +0 -0
  64. data/vendor/cache/timecop-0.3.4.gem +0 -0
  65. metadata +60 -11
  66. data/lib/vanity/rails.rb +0 -22
  67. data/lib/vanity/rails/dashboard.rb +0 -24
  68. data/lib/vanity/rails/helpers.rb +0 -101
  69. data/lib/vanity/rails/testing.rb +0 -11
@@ -10,19 +10,28 @@ class ExperimentTest < Test::Unit::TestCase
10
10
  # -- Defining experiment --
11
11
 
12
12
  def test_can_access_experiment_by_id
13
- exp = Vanity.playground.define(:ice_cream_flavor, :ab_test) { metrics :happiness }
13
+ exp = new_ab_test(:ice_cream_flavor) { metrics :happiness }
14
14
  assert_equal exp, experiment(:ice_cream_flavor)
15
15
  end
16
16
 
17
17
  def test_fail_when_defining_same_experiment_twice
18
- Vanity.playground.define("Ice Cream Flavor", :ab_test) { metrics :happiness }
19
- assert_raises RuntimeError do
20
- Vanity.playground.define("Ice Cream Flavor", :ab_test) { metrics :happiness }
18
+ File.open "tmp/experiments/ice_cream_flavor.rb", "w" do |f|
19
+ f.write <<-RUBY
20
+ ab_test "Ice Cream Flavor" do
21
+ metrics :happiness
22
+ end
23
+ ab_test "Ice Cream Flavor" do
24
+ metrics :happiness
25
+ end
26
+ RUBY
27
+ end
28
+ assert_raises NameError do
29
+ experiment(:ice_cream_flavor)
21
30
  end
22
31
  end
23
32
 
24
33
  def test_uses_playground_namespace_for_experiment
25
- Vanity.playground.define(:ice_cream_flavor, :ab_test) { metrics :happiness }
34
+ new_ab_test(:ice_cream_flavor) { metrics :happiness }
26
35
  assert_equal "vanity:#{Vanity::Version::MAJOR}:ice_cream_flavor", experiment(:ice_cream_flavor).send(:key)
27
36
  assert_equal "vanity:#{Vanity::Version::MAJOR}:ice_cream_flavor:participants", experiment(:ice_cream_flavor).send(:key, "participants")
28
37
  end
@@ -69,8 +78,8 @@ class ExperimentTest < Test::Unit::TestCase
69
78
  end
70
79
 
71
80
  def test_reloading_experiments
72
- Vanity.playground.define(:ab, :ab_test) { metrics :happiness }
73
- Vanity.playground.define(:cd, :ab_test) { metrics :happiness }
81
+ new_ab_test(:ab) { metrics :happiness }
82
+ new_ab_test(:cd) { metrics :happiness }
74
83
  assert 2, Vanity.playground.experiments.size
75
84
  Vanity.playground.reload!
76
85
  assert Vanity.playground.experiments.empty?
@@ -80,20 +89,25 @@ class ExperimentTest < Test::Unit::TestCase
80
89
  # -- Attributes --
81
90
 
82
91
  def test_experiment_mapping_name_to_id
83
- experiment = Vanity.playground.define("Ice Cream Flavor/Tastes", :ab_test) { metrics :happiness }
92
+ experiment = new_ab_test("Ice Cream Flavor/Tastes") { metrics :happiness }
84
93
  assert_equal "Ice Cream Flavor/Tastes", experiment.name
85
94
  assert_equal :ice_cream_flavor_tastes, experiment.id
86
95
  end
87
96
 
88
97
  def test_saving_experiment_after_definition
89
- Vanity.playground.define :ice_cream_flavor, :ab_test do
90
- metrics :happiness
91
- expects(:save)
98
+ File.open "tmp/experiments/ice_cream_flavor.rb", "w" do |f|
99
+ f.write <<-RUBY
100
+ ab_test "Ice Cream Flavor" do
101
+ metrics :happiness
102
+ expects(:save)
103
+ end
104
+ RUBY
92
105
  end
106
+ Vanity.playground.experiment(:ice_cream_flavor)
93
107
  end
94
108
 
95
109
  def test_experiment_has_created_timestamp
96
- Vanity.playground.define(:ice_cream_flavor, :ab_test) { metrics :happiness }
110
+ new_ab_test(:ice_cream_flavor) { metrics :happiness }
97
111
  assert_instance_of Time, experiment(:ice_cream_flavor).created_at
98
112
  assert_in_delta experiment(:ice_cream_flavor).created_at.to_i, Time.now.to_i, 1
99
113
  end
@@ -101,18 +115,18 @@ class ExperimentTest < Test::Unit::TestCase
101
115
  def test_experiment_keeps_created_timestamp_across_definitions
102
116
  past = Date.today - 1
103
117
  Timecop.travel past do
104
- Vanity.playground.define(:ice_cream_flavor, :ab_test) { metrics :happiness }
118
+ new_ab_test(:ice_cream_flavor) { metrics :happiness }
105
119
  assert_equal past.to_time.to_i, experiment(:ice_cream_flavor).created_at.to_i
106
120
  end
107
121
 
108
122
  new_playground
109
123
  metric :happiness
110
- Vanity.playground.define(:ice_cream_flavor, :ab_test) { metrics :happiness }
124
+ new_ab_test(:ice_cream_flavor) { metrics :happiness }
111
125
  assert_equal past.to_time.to_i, experiment(:ice_cream_flavor).created_at.to_i
112
126
  end
113
127
 
114
128
  def test_experiment_has_description
115
- Vanity.playground.define :ice_cream_flavor, :ab_test do
129
+ new_ab_test :ice_cream_flavor do
116
130
  description "Because 31 is not enough ..."
117
131
  metrics :happiness
118
132
  end
@@ -502,7 +502,111 @@ context "Metric" do
502
502
 
503
503
  end
504
504
 
505
+ # -- Google Analytics --
505
506
 
507
+ context "Google Analytics" do
508
+
509
+ setup do
510
+ File.open "tmp/experiments/metrics/ga.rb", "w" do |f|
511
+ f.write <<-RUBY
512
+ metric "GA" do
513
+ google_analytics "UA2"
514
+ end
515
+ RUBY
516
+ end
517
+ end
518
+
519
+ GA_RESULT = Struct.new(:date, :pageviews, :visits)
520
+ GA_PROFILE = Struct.new(:web_property_id)
521
+
522
+ test "fail if Garb not available" do
523
+ File.open "tmp/experiments/metrics/ga.rb", "w" do |f|
524
+ f.write <<-RUBY
525
+ metric "GA" do
526
+ expects(:require).raises LoadError
527
+ google_analytics "UA2"
528
+ end
529
+ RUBY
530
+ end
531
+ assert_raise LoadError do
532
+ Vanity.playground.metrics
533
+ end
534
+ end
535
+
536
+ test "constructs a report" do
537
+ Vanity.playground.metrics
538
+ assert metric(:ga).report
539
+ end
540
+
541
+ test "default to pageviews metric" do
542
+ Vanity.playground.metrics
543
+ assert_equal [:pageviews], metric(:ga).report.metrics.elements
544
+ end
545
+
546
+ test "apply data dimension and sort" do
547
+ Vanity.playground.metrics
548
+ assert_equal [:date], metric(:ga).report.dimensions.elements
549
+ assert_equal [:date], metric(:ga).report.sort.elements
550
+ end
551
+
552
+ test "accept other metrics" do
553
+ File.open "tmp/experiments/metrics/ga.rb", "w" do |f|
554
+ f.write <<-RUBY
555
+ metric "GA" do
556
+ google_analytics "UA2", :visitors
557
+ end
558
+ RUBY
559
+ end
560
+ Vanity.playground.metrics
561
+ assert_equal [:visitors], metric(:ga).report.metrics.elements
562
+ end
563
+
564
+ test "does not support hooks" do
565
+ Vanity.playground.metrics
566
+ assert_raises RuntimeError do
567
+ metric(:ga).hook
568
+ end
569
+ end
570
+
571
+ test "should find matching profile" do
572
+ Vanity.playground.metrics
573
+ Garb::Profile.expects(:all).returns(Array.new(3) { |i| GA_PROFILE.new("UA#{i + 1}") })
574
+ metric(:ga).report.stubs(:send_request_for_body).returns(nil)
575
+ Garb::ReportResponse.stubs(:new).returns(mock(:results=>[]))
576
+ metric(:ga).values(Date.parse("2010-02-10"), Date.parse("2010-02-12"))
577
+ assert_equal "UA2", metric(:ga).report.profile.web_property_id
578
+ end
579
+
580
+ test "should map results from report" do
581
+ Vanity.playground.metrics
582
+ today = Date.today
583
+ response = mock(:results=>Array.new(3) { |i| GA_RESULT.new("2010021#{i}", i + 1) })
584
+ Garb::Profile.stubs(:all).returns([])
585
+ Garb::ReportResponse.expects(:new).returns(response)
586
+ metric(:ga).report.stubs(:send_request_for_body).returns(nil)
587
+ assert_equal [1,2,3], metric(:ga).values(Date.parse("2010-02-10"), Date.parse("2010-02-12"))
588
+ end
589
+
590
+ test "mapping GA metrics to single value" do
591
+ File.open "tmp/experiments/metrics/ga.rb", "w" do |f|
592
+ f.write <<-RUBY
593
+ metric "GA" do
594
+ google_analytics "UA2", :mapper=>lambda { |e| e.pageviews * e.visits }
595
+ end
596
+ RUBY
597
+ end
598
+ Vanity.playground.metrics
599
+ today = Date.today
600
+ response = mock(:results=>Array.new(3) { |i| GA_RESULT.new("2010021#{i}", i + 1, i + 1) })
601
+ Garb::Profile.stubs(:all).returns([])
602
+ Garb::ReportResponse.expects(:new).returns(response)
603
+ metric(:ga).report.stubs(:send_request_for_body).returns(nil)
604
+ assert_equal [1,4,9], metric(:ga).values(Date.parse("2010-02-10"), Date.parse("2010-02-12"))
605
+ end
606
+
607
+ end
608
+
609
+
506
610
  # -- Helper methods --
507
611
 
508
612
  def today
@@ -0,0 +1,2 @@
1
+ class ApplicationController < ActionController::Base
2
+ end
@@ -0,0 +1,7 @@
1
+ class MainController < ApplicationController
2
+ def index
3
+ render :text=>"#{Vanity.playground.redis.server}\n#{Vanity.playground.redis.object_id}"
4
+ rescue Error=>ex
5
+ puts $!
6
+ end
7
+ end
@@ -0,0 +1,110 @@
1
+ # Don't change this file!
2
+ # Configure your app in config/environment.rb and config/environments/*.rb
3
+
4
+ RAILS_ROOT = "#{File.dirname(__FILE__)}/.." unless defined?(RAILS_ROOT)
5
+
6
+ module Rails
7
+ class << self
8
+ def boot!
9
+ unless booted?
10
+ preinitialize
11
+ pick_boot.run
12
+ end
13
+ end
14
+
15
+ def booted?
16
+ defined? Rails::Initializer
17
+ end
18
+
19
+ def pick_boot
20
+ (vendor_rails? ? VendorBoot : GemBoot).new
21
+ end
22
+
23
+ def vendor_rails?
24
+ File.exist?("#{RAILS_ROOT}/vendor/rails")
25
+ end
26
+
27
+ def preinitialize
28
+ load(preinitializer_path) if File.exist?(preinitializer_path)
29
+ end
30
+
31
+ def preinitializer_path
32
+ "#{RAILS_ROOT}/config/preinitializer.rb"
33
+ end
34
+ end
35
+
36
+ class Boot
37
+ def run
38
+ load_initializer
39
+ Rails::Initializer.run(:set_load_path)
40
+ end
41
+ end
42
+
43
+ class VendorBoot < Boot
44
+ def load_initializer
45
+ require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer"
46
+ Rails::Initializer.run(:install_gem_spec_stubs)
47
+ Rails::GemDependency.add_frozen_gem_path
48
+ end
49
+ end
50
+
51
+ class GemBoot < Boot
52
+ def load_initializer
53
+ self.class.load_rubygems
54
+ load_rails_gem
55
+ require 'initializer'
56
+ end
57
+
58
+ def load_rails_gem
59
+ if version = self.class.gem_version
60
+ gem 'rails', version
61
+ else
62
+ gem 'rails'
63
+ end
64
+ rescue Gem::LoadError => load_error
65
+ $stderr.puts %(Missing the Rails #{version} gem. Please `gem install -v=#{version} rails`, update your RAILS_GEM_VERSION setting in config/environment.rb for the Rails version you do have installed, or comment out RAILS_GEM_VERSION to use the latest version installed.)
66
+ exit 1
67
+ end
68
+
69
+ class << self
70
+ def rubygems_version
71
+ Gem::RubyGemsVersion rescue nil
72
+ end
73
+
74
+ def gem_version
75
+ if defined? RAILS_GEM_VERSION
76
+ RAILS_GEM_VERSION
77
+ elsif ENV.include?('RAILS_GEM_VERSION')
78
+ ENV['RAILS_GEM_VERSION']
79
+ else
80
+ parse_gem_version(read_environment_rb)
81
+ end
82
+ end
83
+
84
+ def load_rubygems
85
+ min_version = '1.3.1'
86
+ require 'rubygems'
87
+ unless rubygems_version >= min_version
88
+ $stderr.puts %Q(Rails requires RubyGems >= #{min_version} (you have #{rubygems_version}). Please `gem update --system` and try again.)
89
+ exit 1
90
+ end
91
+
92
+ rescue LoadError
93
+ $stderr.puts %Q(Rails requires RubyGems >= #{min_version}. Please install RubyGems and try again: http://rubygems.rubyforge.org)
94
+ exit 1
95
+ end
96
+
97
+ def parse_gem_version(text)
98
+ $1 if text =~ /^[^#]*RAILS_GEM_VERSION\s*=\s*["']([!~<>=]*\s*[\d.]+)["']/
99
+ end
100
+
101
+ private
102
+ def read_environment_rb
103
+ File.read("#{RAILS_ROOT}/config/environment.rb")
104
+ end
105
+ end
106
+ end
107
+ end
108
+
109
+ # All that for this:
110
+ Rails.boot!
@@ -0,0 +1,10 @@
1
+ require File.join(File.dirname(__FILE__), 'boot')
2
+
3
+ Rails::Initializer.run do |config|
4
+ config.frameworks -= [ :active_record, :active_resource, :action_mailer ]
5
+ config.action_controller.session = { :key=>"_myapp_session", :secret=>"Stay hungry. Stay foolish. -- Steve Jobs" }
6
+ config.after_initialize do
7
+ $:.unshift File.dirname(__FILE__) + "/../../../lib/"
8
+ require "vanity"
9
+ end
10
+ end
@@ -0,0 +1,3 @@
1
+ ActionController::Routing::Routes.draw do |map|
2
+ map.connect "/", :controller=>:main, :action=>:index
3
+ end
@@ -0,0 +1,80 @@
1
+ # Logfile created on 2010-02-22 15:10:48 -0800
2
+
3
+ Processing MainController#index (for at 2010-02-22 15:10:48) [GET]
4
+ Parameters: {" "=>nil}
5
+ Completed in 5ms (View: 1 | 200 OK [http://:? ]
6
+
7
+
8
+ Processing MainController#index (for at 2010-02-22 15:17:09) [GET]
9
+ Parameters: {" "=>nil}
10
+ Completed in 6ms (View: 1 | 200 OK [http://:? ]
11
+
12
+
13
+ Processing MainController#index (for at 2010-02-22 15:21:35) [GET]
14
+ Parameters: {" "=>nil}
15
+ Completed in 167ms (View: 94 | 200 OK [http://:? ]
16
+
17
+
18
+ Processing MainController#index (for at 2010-02-22 17:01:14) [GET]
19
+ Parameters: {" "=>nil}
20
+ Completed in 8ms (View: 1 | 200 OK [http://:? ]
21
+
22
+
23
+ Processing MainController#index (for at 2010-02-24 09:16:46) [GET]
24
+ Parameters: {" "=>nil}
25
+ Completed in 5ms (View: 1 | 200 OK [http://:? ]
26
+
27
+
28
+ Processing MainController#index (for at 2010-02-24 09:36:17) [GET]
29
+ Parameters: {" "=>nil}
30
+ Completed in 6ms (View: 1 | 200 OK [http://:? ]
31
+
32
+
33
+ Processing MainController#index (for at 2010-02-24 09:49:36) [GET]
34
+ Parameters: {" "=>nil}
35
+ Completed in 4ms (View: 1 | 200 OK [http://:? ]
36
+
37
+
38
+ Processing MainController#index (for at 2010-02-24 11:41:39) [GET]
39
+ Parameters: {" "=>nil}
40
+ Completed in 6ms (View: 1 | 200 OK [http://:? ]
41
+
42
+
43
+ Processing MainController#index (for at 2010-02-24 11:42:19) [GET]
44
+ Parameters: {" "=>nil}
45
+ Completed in 5ms (View: 1 | 200 OK [http://:? ]
46
+
47
+
48
+ Processing MainController#index (for at 2010-02-24 12:14:54) [GET]
49
+ Parameters: {" "=>nil}
50
+ Completed in 10ms (View: 2 | 200 OK [http://:? ]
51
+
52
+
53
+ Processing MainController#index (for at 2010-02-24 13:40:28) [GET]
54
+ Parameters: {" "=>nil}
55
+ Completed in 8ms (View: 2 | 200 OK [http://:? ]
56
+
57
+
58
+ Processing MainController#index (for at 2010-02-24 13:54:19) [GET]
59
+ Parameters: {" "=>nil}
60
+ Completed in 5ms (View: 1 | 200 OK [http://:? ]
61
+
62
+
63
+ Processing MainController#index (for at 2010-02-24 14:39:14) [GET]
64
+ Parameters: {" "=>nil}
65
+ Completed in 8ms (View: 1 | 200 OK [http://:? ]
66
+
67
+
68
+ Processing MainController#index (for at 2010-02-27 00:03:56) [GET]
69
+ Parameters: {" "=>nil}
70
+ Completed in 5ms (View: 1 | 200 OK [http://:? ]
71
+
72
+
73
+ Processing MainController#index (for at 2010-03-01 19:13:46) [GET]
74
+ Parameters: {" "=>nil}
75
+ Completed in 5ms (View: 1 | 200 OK [http://:? ]
76
+
77
+
78
+ Processing MainController#index (for at 2010-03-01 20:01:19) [GET]
79
+ Parameters: {" "=>nil}
80
+ Completed in 5ms (View: 1 | 200 OK [http://:? ]
@@ -0,0 +1,34 @@
1
+ require "test/test_helper"
2
+ require "phusion_passenger/spawn_manager"
3
+
4
+ class PassengerTest < Test::Unit::TestCase
5
+ def setup
6
+ ActiveRecord::Base.connection.disconnect! # Otherwise AR metric tests fail
7
+ @original = Vanity.playground.redis
8
+ @server = PhusionPassenger::SpawnManager.new
9
+ @server.start
10
+ Thread.pass until @server.started?
11
+ app_root = File.expand_path("myapp", File.dirname(__FILE__))
12
+ @app = @server.spawn_application "app_root"=>app_root, "spawn_method"=>"smart-lv2"
13
+ end
14
+
15
+ def test_reconnect
16
+ sleep 0.1
17
+ socket = TCPSocket.new(*@app.listen_socket_name.split(":"))
18
+ channel = PhusionPassenger::MessageChannel.new(socket)
19
+ request = {"REQUEST_PATH"=>"/", "REQUEST_METHOD"=>"GET", "QUERY_STRING"=>" "}
20
+ channel.write_scalar request.to_a.join("\0")
21
+ response = socket.read.split("\r\n\r\n").last
22
+ socket.close
23
+
24
+ server, obj_id = response.split("\n")
25
+ assert_equal @original.server, server
26
+ assert_not_equal @original.object_id.to_s, obj_id
27
+ end
28
+
29
+ def teardown
30
+ super
31
+ @server.stop
32
+ end
33
+
34
+ end