vanity 1.8.4 → 1.9.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.
Files changed (53) hide show
  1. data/.travis.yml +3 -2
  2. data/CHANGELOG +12 -0
  3. data/Gemfile +6 -3
  4. data/Gemfile.lock +12 -10
  5. data/README.rdoc +45 -16
  6. data/Rakefile +14 -9
  7. data/doc/_layouts/page.html +4 -6
  8. data/doc/ab_testing.textile +1 -1
  9. data/doc/configuring.textile +2 -4
  10. data/doc/email.textile +1 -3
  11. data/doc/index.textile +3 -63
  12. data/doc/rails.textile +34 -8
  13. data/gemfiles/rails3.gemfile +12 -3
  14. data/gemfiles/rails3.gemfile.lock +37 -11
  15. data/gemfiles/rails31.gemfile +12 -3
  16. data/gemfiles/rails31.gemfile.lock +37 -11
  17. data/gemfiles/rails32.gemfile +12 -3
  18. data/gemfiles/rails32.gemfile.lock +37 -11
  19. data/gemfiles/rails4.gemfile +12 -3
  20. data/gemfiles/rails4.gemfile.lock +37 -11
  21. data/lib/vanity/adapters/abstract_adapter.rb +4 -0
  22. data/lib/vanity/adapters/active_record_adapter.rb +18 -10
  23. data/lib/vanity/adapters/mock_adapter.rb +8 -4
  24. data/lib/vanity/adapters/mongodb_adapter.rb +11 -7
  25. data/lib/vanity/adapters/redis_adapter.rb +88 -37
  26. data/lib/vanity/commands/report.rb +9 -9
  27. data/lib/vanity/experiment/ab_test.rb +120 -101
  28. data/lib/vanity/experiment/alternative.rb +21 -21
  29. data/lib/vanity/experiment/base.rb +5 -5
  30. data/lib/vanity/experiment/bayesian_bandit_score.rb +51 -51
  31. data/lib/vanity/experiment/definition.rb +10 -10
  32. data/lib/vanity/frameworks/rails.rb +39 -36
  33. data/lib/vanity/helpers.rb +6 -4
  34. data/lib/vanity/metric/active_record.rb +1 -1
  35. data/lib/vanity/metric/base.rb +23 -24
  36. data/lib/vanity/metric/google_analytics.rb +5 -5
  37. data/lib/vanity/playground.rb +118 -24
  38. data/lib/vanity/templates/_report.erb +20 -6
  39. data/lib/vanity/templates/vanity.css +2 -0
  40. data/lib/vanity/version.rb +1 -1
  41. data/test/adapters/redis_adapter_test.rb +106 -1
  42. data/test/dummy/config/database.yml +21 -4
  43. data/test/dummy/config/routes.rb +1 -1
  44. data/test/experiment/ab_test.rb +93 -13
  45. data/test/metric/active_record_test.rb +9 -4
  46. data/test/passenger_test.rb +43 -42
  47. data/test/playground_test.rb +50 -1
  48. data/test/rails_dashboard_test.rb +38 -1
  49. data/test/rails_helper_test.rb +5 -0
  50. data/test/rails_test.rb +66 -15
  51. data/test/test_helper.rb +24 -2
  52. data/vanity.gemspec +0 -2
  53. metadata +45 -57
@@ -24,3 +24,5 @@
24
24
  .vanity form#milestones label { margin-right: .5em }
25
25
  .vanity form#milestones input { vertical-align: bottom }
26
26
  .vanity .metric .marking.label { position: absolute; bottom: 2em; color: #c66; font-size: 80% }
27
+
28
+ .vanity .alert { padding: 8px 35px 8px 14px; margin-bottom: 20px; text-shadow: 0px 1px 0px rgba(255, 255, 255, 0.5); background-color: rgb(252, 248, 227); border: 1px solid rgb(251, 238, 213); border-radius: 4px 4px 4px 4px; }
@@ -1,5 +1,5 @@
1
1
  module Vanity
2
- VERSION = "1.8.4"
2
+ VERSION = "1.9.0.beta"
3
3
 
4
4
  module Version
5
5
  version = VERSION.to_s.split(".").map { |i| i.to_i }
@@ -1,6 +1,12 @@
1
1
  require 'test/test_helper'
2
2
 
3
3
  class RedisAdapterTest < Test::Unit::TestCase
4
+ def setup
5
+ require "vanity/adapters/redis_adapter"
6
+ require "redis"
7
+ require "redis/namespace"
8
+ end
9
+
4
10
  def test_warn_on_disconnect_error
5
11
  if defined?(Redis)
6
12
  assert_nothing_raised do
@@ -8,10 +14,109 @@ class RedisAdapterTest < Test::Unit::TestCase
8
14
  mocked_redis = stub("Redis")
9
15
  mocked_redis.expects(:client).raises(RuntimeError)
10
16
  redis_adapter = Vanity::Adapters::RedisAdapter.new({})
11
- redis_adapter.expects(:warn).with("Error while disconnecting from redis: RuntimeError")
12
17
  redis_adapter.stubs(:redis).returns(mocked_redis)
18
+ redis_adapter.expects(:warn).with("Error while disconnecting from redis: RuntimeError")
13
19
  redis_adapter.disconnect!
14
20
  end
15
21
  end
16
22
  end
23
+
24
+ def stub_redis
25
+ Vanity.playground.failover_on_datastore_error!
26
+ mocked_redis = stub("Redis")
27
+ redis_adapter = Vanity::Adapters::RedisAdapter.new(:redis => mocked_redis)
28
+
29
+ [redis_adapter, mocked_redis]
30
+ end
31
+
32
+ def test_graceful_failure_metric_track
33
+ redis_adapter, mocked_redis = stub_redis
34
+ mocked_redis.stubs(:incrby).raises(RuntimeError)
35
+
36
+ assert_nothing_raised do
37
+ redis_adapter.metric_track("metric", Time.now.to_s, "3ff62e2fb51f0b22646a342a2d357aec", [1])
38
+ end
39
+ end
40
+
41
+ def test_graceful_failure_set_experiment_created_at
42
+ redis_adapter, mocked_redis = stub_redis
43
+ mocked_redis.stubs(:setnx).raises(RuntimeError)
44
+
45
+ assert_nothing_raised do
46
+ redis_adapter.set_experiment_created_at("price_options", Time.now)
47
+ end
48
+ end
49
+
50
+ def test_graceful_failure_is_experiment_completed?
51
+ redis_adapter, mocked_redis = stub_redis
52
+ mocked_redis.stubs(:exists).raises(RuntimeError)
53
+
54
+ assert_nothing_raised do
55
+ redis_adapter.is_experiment_completed?("price_options")
56
+ end
57
+ end
58
+
59
+ def test_graceful_failure_ab_show
60
+ redis_adapter, mocked_redis = stub_redis
61
+ mocked_redis.stubs(:[]=).raises(RuntimeError)
62
+
63
+ assert_nothing_raised do
64
+ redis_adapter.ab_show("price_options", "3ff62e2fb51f0b22646a342a2d357aec", 0)
65
+ end
66
+ end
67
+
68
+ def test_graceful_failure_ab_showing
69
+ redis_adapter, mocked_redis = stub_redis
70
+ mocked_redis.stubs(:[]).raises(RuntimeError)
71
+
72
+ assert_nothing_raised do
73
+ redis_adapter.ab_showing("price_options", "3ff62e2fb51f0b22646a342a2d357aec")
74
+ end
75
+ end
76
+
77
+ def test_graceful_failure_ab_not_showing
78
+ redis_adapter, mocked_redis = stub_redis
79
+ mocked_redis.stubs(:del).raises(RuntimeError)
80
+
81
+ assert_nothing_raised do
82
+ redis_adapter.ab_not_showing("price_options", "3ff62e2fb51f0b22646a342a2d357aec")
83
+ end
84
+ end
85
+
86
+ def test_graceful_failure_ab_add_participant
87
+ redis_adapter, mocked_redis = stub_redis
88
+ mocked_redis.stubs(:sadd).raises(RuntimeError)
89
+
90
+ assert_nothing_raised do
91
+ redis_adapter.ab_add_participant("price_options", "3ff62e2fb51f0b22646a342a2d357aec", 0)
92
+ end
93
+ end
94
+
95
+ def test_graceful_failure_ab_seen
96
+ redis_adapter, mocked_redis = stub_redis
97
+ mocked_redis.stubs(:sismember).raises(RuntimeError)
98
+
99
+ assert_nothing_raised do
100
+ redis_adapter.ab_seen("price_options", "3ff62e2fb51f0b22646a342a2d357aec", 0)
101
+ end
102
+ end
103
+
104
+ def test_graceful_failure_ab_assigned
105
+ redis_adapter, mocked_redis = stub_redis
106
+ mocked_redis.stubs(:sismember).raises(RuntimeError)
107
+
108
+ assert_nothing_raised do
109
+ redis_adapter.ab_assigned("price_options", "3ff62e2fb51f0b22646a342a2d357aec")
110
+ end
111
+ end
112
+
113
+ def test_graceful_failure_ab_add_conversion
114
+ redis_adapter, mocked_redis = stub_redis
115
+ mocked_redis.stubs(:sismember).raises(RuntimeError)
116
+
117
+ assert_nothing_raised do
118
+ redis_adapter.ab_add_conversion("price_options", 0, "3ff62e2fb51f0b22646a342a2d357aec")
119
+ end
120
+ end
121
+
17
122
  end
@@ -1,5 +1,22 @@
1
+ # Based on https://github.com/plataformatec/devise/blob/master/test/rails_app/config/database.yml
2
+
3
+ # SQLite version 3.x
4
+ # gem install sqlite3
5
+ #
6
+ # Ensure the SQLite 3 gem is defined in your Gemfile
7
+ # gem 'sqlite3'
8
+
9
+ # Set databases in all environments since we may test loading Rails in non-
10
+ # test environments
11
+
12
+ development:
13
+ adapter: sqlite3
14
+ database: ":memory:"
15
+
1
16
  test:
2
- adapter: mysql
3
- database: vanity_test
4
- pool: 5
5
- timeout: 5000
17
+ adapter: sqlite3
18
+ database: ":memory:"
19
+
20
+ production:
21
+ adapter: sqlite3
22
+ database: ":memory:"
@@ -54,5 +54,5 @@ Dummy::Application.routes.draw do
54
54
 
55
55
  # This is a legacy wild controller route that's not recommended for RESTful applications.
56
56
  # Note: This route will make all actions in every controller accessible via GET requests.
57
- match ':controller(/:action(/:id(.:format)))', :via => [:get]
57
+ match ':controller(/:action(/:id(.:format)))', :via => [:get, :post]
58
58
  end
@@ -133,7 +133,7 @@ class AbTestTest < ActionController::TestCase
133
133
 
134
134
  # -- use_js! --
135
135
 
136
- def test_does_not_record_participant_when_using_js
136
+ def test_choose_does_not_record_participant_when_using_js
137
137
  Vanity.playground.use_js!
138
138
  ids = (0...10).to_a
139
139
  new_ab_test :foobar do
@@ -160,6 +160,44 @@ class AbTestTest < ActionController::TestCase
160
160
  assert_equal 1, on_assignment_called_times
161
161
  end
162
162
 
163
+ def test_calls_on_assignment_when_given_valid_request
164
+ on_assignment_called_times = 0
165
+ new_ab_test :foobar do
166
+ alternatives "foo", "bar"
167
+ identify { "6e98ec" }
168
+ metrics :coolness
169
+ on_assignment { on_assignment_called_times = on_assignment_called_times+1 }
170
+ end
171
+ experiment(:foobar).choose(dummy_request)
172
+ assert_equal 1, on_assignment_called_times
173
+ end
174
+
175
+ def test_does_not_call_on_assignment_when_given_invalid_request
176
+ on_assignment_called_times = 0
177
+ new_ab_test :foobar do
178
+ alternatives "foo", "bar"
179
+ identify { "6e98ec" }
180
+ metrics :coolness
181
+ on_assignment { on_assignment_called_times = on_assignment_called_times+1 }
182
+ end
183
+ request = dummy_request
184
+ request.user_agent = "Googlebot/2.1 ( http://www.google.com/bot.html)"
185
+ experiment(:foobar).choose(request)
186
+ assert_equal 0, on_assignment_called_times
187
+ end
188
+
189
+ def test_calls_on_assignment_on_new_assignment_via_chooses
190
+ on_assignment_called_times = 0
191
+ new_ab_test :foobar do
192
+ alternatives "foo", "bar"
193
+ identify { "6e98ec" }
194
+ metrics :coolness
195
+ on_assignment { on_assignment_called_times = on_assignment_called_times+1 }
196
+ end
197
+ 2.times { experiment(:foobar).chooses("foo") }
198
+ assert_equal 1, on_assignment_called_times
199
+ end
200
+
163
201
  def test_returns_the_same_alternative_consistently_when_on_assignment_is_set
164
202
  new_ab_test :foobar do
165
203
  alternatives "foo", "bar"
@@ -344,6 +382,27 @@ class AbTestTest < ActionController::TestCase
344
382
  assert_equal 100, alts.map(&:converted).sum
345
383
  end
346
384
 
385
+ def test_choose_records_participants_given_a_valid_request
386
+ new_ab_test :foobar do
387
+ alternatives "foo", "bar"
388
+ identify { "me" }
389
+ metrics :coolness
390
+ end
391
+ experiment(:foobar).choose(dummy_request)
392
+ assert_equal 1, experiment(:foobar).alternatives.map(&:participants).sum
393
+ end
394
+
395
+ def test_choose_ignores_participants_given_an_invalid_request
396
+ new_ab_test :foobar do
397
+ alternatives "foo", "bar"
398
+ identify { "me" }
399
+ metrics :coolness
400
+ end
401
+ request = dummy_request
402
+ request.user_agent = "Googlebot/2.1 ( http://www.google.com/bot.html)"
403
+ experiment(:foobar).choose(request)
404
+ assert_equal 0, experiment(:foobar).alternatives.map(&:participants).sum
405
+ end
347
406
 
348
407
  def test_destroy_experiment
349
408
  new_ab_test :simple do
@@ -465,6 +524,7 @@ class AbTestTest < ActionController::TestCase
465
524
 
466
525
 
467
526
  # -- Scoring --
527
+
468
528
  def test_calculate_score
469
529
  new_ab_test :abcd do
470
530
  alternatives :a, :b, :c, :d
@@ -488,10 +548,10 @@ class AbTestTest < ActionController::TestCase
488
548
  metrics :coolness
489
549
  end
490
550
  # participating, conversions, rate, z-score
491
- # Control: 182 35 19.23% N/A
492
- # Treatment A: 180 45 25.00% 1.33
493
- # treatment B: 189 28 14.81% -1.13
494
- # treatment C: 188 61 32.45% 2.94
551
+ # Control: 182 35 19.23% N/A
552
+ # Treatment A: 180 45 25.00% 1.33
553
+ # treatment B: 189 28 14.81% -1.13
554
+ # treatment C: 188 61 32.45% 2.94
495
555
  fake :abcd, :a=>[182, 35], :b=>[180, 45], :c=>[189,28], :d=>[188, 61]
496
556
 
497
557
  z_scores = experiment(:abcd).score.alts.map { |alt| "%.2f" % alt.z_score }
@@ -514,10 +574,10 @@ class AbTestTest < ActionController::TestCase
514
574
  metrics :coolness
515
575
  end
516
576
  # participating, conversions, rate, z-score
517
- # Control: 182 35 19.23% N/A
518
- # Treatment A: 180 45 25.00% 1.33
519
- # treatment B: 189 28 14.81% -1.13
520
- # treatment C: 188 61 32.45% 2.94
577
+ # Control: 182 35 19.23% N/A
578
+ # Treatment A: 180 45 25.00% 1.33
579
+ # treatment B: 189 28 14.81% -1.13
580
+ # treatment C: 188 61 32.45% 2.94
521
581
  fake :abcd, :a=>[182, 35], :b=>[180, 45], :c=>[189,28], :d=>[188, 61]
522
582
 
523
583
  score_result = experiment(:abcd).bayes_bandit_score
@@ -593,10 +653,10 @@ class AbTestTest < ActionController::TestCase
593
653
  metrics :coolness
594
654
  end
595
655
  # participating, conversions, rate, z-score
596
- # Control: 182 35 19.23% N/A
597
- # Treatment A: 180 45 25.00% 1.33
598
- # treatment B: 189 28 14.81% -1.13
599
- # treatment C: 188 61 32.45% 2.94
656
+ # Control: 182 35 19.23% N/A
657
+ # Treatment A: 180 45 25.00% 1.33
658
+ # treatment B: 189 28 14.81% -1.13
659
+ # treatment C: 188 61 32.45% 2.94
600
660
  fake :abcd, :a=>[182, 35], :b=>[180, 45], :c=>[189,28], :d=>[188, 61]
601
661
 
602
662
  assert_equal <<-TEXT, experiment(:abcd).conclusion.join("\n") << "\n"
@@ -919,6 +979,26 @@ This experiment did not run long enough to find a clear winner.
919
979
  assert_equal experiment(:simple).alternatives[2].participants, 1
920
980
  end
921
981
 
982
+ def test_chooses_records_participants_given_a_valid_request
983
+ new_ab_test :simple do
984
+ alternatives :a, :b, :c
985
+ metrics :coolness
986
+ end
987
+ experiment(:simple).chooses(:a, dummy_request)
988
+ assert_equal 1, experiment(:simple).alternatives[0].participants
989
+ end
990
+
991
+ def test_chooses_ignores_participants_given_an_invalid_request
992
+ new_ab_test :simple do
993
+ alternatives :a, :b, :c
994
+ metrics :coolness
995
+ end
996
+ request = dummy_request
997
+ request.user_agent = "Googlebot/2.1 ( http://www.google.com/bot.html)"
998
+ experiment(:simple).chooses(:a, request)
999
+ assert_equal 0, experiment(:simple).alternatives[0].participants
1000
+ end
1001
+
922
1002
  def test_no_collection_and_chooses
923
1003
  not_collecting!
924
1004
  new_ab_test :simple do
@@ -1,10 +1,12 @@
1
1
  require "test/test_helper"
2
2
 
3
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
4
+ if connected?
5
+ connection.drop_table :skies if table_exists?
6
+ connection.create_table :skies do |t|
7
+ t.integer :height
8
+ t.timestamps
9
+ end
8
10
  end
9
11
 
10
12
  if defined?(Rails::Railtie)
@@ -14,6 +16,7 @@ class Sky < ActiveRecord::Base
14
16
  end
15
17
  end
16
18
 
19
+ if ActiveRecord::Base.connected?
17
20
 
18
21
  context "ActiveRecord Metric" do
19
22
 
@@ -305,3 +308,5 @@ context "ActiveRecord Metric" do
305
308
  end
306
309
  end
307
310
  end
311
+
312
+ end
@@ -1,51 +1,52 @@
1
1
  require "test/test_helper"
2
2
 
3
- #not supported for rails3
4
- unless defined?(Rails::Railtie)
5
- require "phusion_passenger/spawn_manager"
6
- class PassengerTest < Test::Unit::TestCase
7
- def setup
8
- super
9
- ActiveRecord::Base.connection.disconnect! # Otherwise AR metric tests fail
10
- @original = Vanity.playground.connection
11
- File.unlink "test/myapp/config/vanity.yml" rescue nil
12
- File.open("test/myapp/config/vanity.yml", "w") do |io|
13
- io.write YAML.dump({ "production"=>DATABASE })
3
+ # Not supported for rails3
4
+ if !defined?(Rails::Railtie) && ActiveRecord::Base.connected?
5
+ require "phusion_passenger/spawn_manager"
6
+
7
+ class PassengerTest < Test::Unit::TestCase
8
+ def setup
9
+ super
10
+ ActiveRecord::Base.connection.disconnect! # Otherwise AR metric tests fail
11
+ @original = Vanity.playground.connection
12
+ File.unlink "test/myapp/config/vanity.yml" rescue nil
13
+ File.open("test/myapp/config/vanity.yml", "w") do |io|
14
+ io.write YAML.dump({ "production"=>DATABASE })
15
+ end
16
+ @server = PhusionPassenger::SpawnManager.new
17
+ @server.start
18
+ Thread.pass until @server.started?
19
+ app_root = File.expand_path("myapp", File.dirname(__FILE__))
20
+ @app = @server.spawn_application "app_root"=>app_root, "spawn_method"=>"smart"
14
21
  end
15
- @server = PhusionPassenger::SpawnManager.new
16
- @server.start
17
- Thread.pass until @server.started?
18
- app_root = File.expand_path("myapp", File.dirname(__FILE__))
19
- @app = @server.spawn_application "app_root"=>app_root, "spawn_method"=>"smart"
20
- end
21
22
 
22
- def test_reconnect
23
- # When using AR adapter, we're not responsible to reconnect, and we're going
24
- # to get the same "connect" (AR connection handler) either way.
25
- # return if defined?(Vanity::Adapters::ActiveRecordAdapter) && Vanity::Adapters::ActiveRecordAdapter === Vanity.playground.connection
23
+ def test_reconnect
24
+ # When using AR adapter, we're not responsible to reconnect, and we're going
25
+ # to get the same "connect" (AR connection handler) either way.
26
+ # return if defined?(Vanity::Adapters::ActiveRecordAdapter) && Vanity::Adapters::ActiveRecordAdapter === Vanity.playground.connection
26
27
 
27
- sleep 0.1
28
- case @app.listen_socket_type
29
- when "tcp" ; socket = TCPSocket.new(*@app.listen_socket_name.split(":"))
30
- when "unix"; socket = UNIXSocket.new(@app.listen_socket_name)
31
- else fail
28
+ sleep 0.1
29
+ case @app.listen_socket_type
30
+ when "tcp" ; socket = TCPSocket.new(*@app.listen_socket_name.split(":"))
31
+ when "unix"; socket = UNIXSocket.new(@app.listen_socket_name)
32
+ else fail
33
+ end
34
+ channel = PhusionPassenger::MessageChannel.new(socket)
35
+ request = {"REQUEST_PATH"=>"/", "REQUEST_METHOD"=>"GET", "QUERY_STRING"=>" "}
36
+ channel.write_scalar request.to_a.join("\0")
37
+ response = socket.read.split("\r\n\r\n").last
38
+ socket.close
39
+ conn, obj_id = response.split("\n")
40
+ assert_equal @original.to_s, conn
41
+ assert_not_equal @original.object_id.to_s, obj_id
32
42
  end
33
- channel = PhusionPassenger::MessageChannel.new(socket)
34
- request = {"REQUEST_PATH"=>"/", "REQUEST_METHOD"=>"GET", "QUERY_STRING"=>" "}
35
- channel.write_scalar request.to_a.join("\0")
36
- response = socket.read.split("\r\n\r\n").last
37
- socket.close
38
- conn, obj_id = response.split("\n")
39
- assert_equal @original.to_s, conn
40
- assert_not_equal @original.object_id.to_s, obj_id
41
- end
42
43
 
43
- def teardown
44
- super
45
- @server.cleanup
46
- @server.stop
47
- Process.kill('SIGKILL', @app.pid.to_i) # Just in case...KIDS, GET OUT OF THE POOL!
48
- File.unlink "test/myapp/config/vanity.yml"
44
+ def teardown
45
+ super
46
+ @server.cleanup
47
+ @server.stop
48
+ Process.kill('SIGKILL', @app.pid.to_i) # Just in case...KIDS, GET OUT OF THE POOL!
49
+ File.unlink "test/myapp/config/vanity.yml"
50
+ end
49
51
  end
50
52
  end
51
- end