moses-vanity 1.7.1

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 (98) hide show
  1. data/.autotest +22 -0
  2. data/.gitignore +7 -0
  3. data/.rvmrc +3 -0
  4. data/.travis.yml +13 -0
  5. data/CHANGELOG +374 -0
  6. data/Gemfile +28 -0
  7. data/MIT-LICENSE +21 -0
  8. data/README.rdoc +108 -0
  9. data/Rakefile +189 -0
  10. data/bin/vanity +16 -0
  11. data/doc/_config.yml +2 -0
  12. data/doc/_layouts/_header.html +34 -0
  13. data/doc/_layouts/page.html +47 -0
  14. data/doc/_metrics.textile +12 -0
  15. data/doc/ab_testing.textile +210 -0
  16. data/doc/configuring.textile +45 -0
  17. data/doc/contributing.textile +93 -0
  18. data/doc/credits.textile +23 -0
  19. data/doc/css/page.css +83 -0
  20. data/doc/css/print.css +43 -0
  21. data/doc/css/syntax.css +7 -0
  22. data/doc/email.textile +129 -0
  23. data/doc/experimental.textile +31 -0
  24. data/doc/faq.textile +8 -0
  25. data/doc/identity.textile +43 -0
  26. data/doc/images/ab_in_dashboard.png +0 -0
  27. data/doc/images/clear_winner.png +0 -0
  28. data/doc/images/price_options.png +0 -0
  29. data/doc/images/sidebar_test.png +0 -0
  30. data/doc/images/signup_metric.png +0 -0
  31. data/doc/images/vanity.png +0 -0
  32. data/doc/index.textile +91 -0
  33. data/doc/metrics.textile +231 -0
  34. data/doc/rails.textile +89 -0
  35. data/doc/site.js +27 -0
  36. data/generators/templates/vanity_migration.rb +53 -0
  37. data/generators/vanity_generator.rb +8 -0
  38. data/lib/generators/templates/vanity_migration.rb +53 -0
  39. data/lib/generators/vanity_generator.rb +15 -0
  40. data/lib/vanity.rb +36 -0
  41. data/lib/vanity/adapters/abstract_adapter.rb +140 -0
  42. data/lib/vanity/adapters/active_record_adapter.rb +248 -0
  43. data/lib/vanity/adapters/mock_adapter.rb +157 -0
  44. data/lib/vanity/adapters/mongodb_adapter.rb +178 -0
  45. data/lib/vanity/adapters/redis_adapter.rb +160 -0
  46. data/lib/vanity/backport.rb +26 -0
  47. data/lib/vanity/commands/list.rb +21 -0
  48. data/lib/vanity/commands/report.rb +64 -0
  49. data/lib/vanity/commands/upgrade.rb +34 -0
  50. data/lib/vanity/experiment/ab_test.rb +507 -0
  51. data/lib/vanity/experiment/base.rb +214 -0
  52. data/lib/vanity/frameworks.rb +16 -0
  53. data/lib/vanity/frameworks/rails.rb +318 -0
  54. data/lib/vanity/helpers.rb +66 -0
  55. data/lib/vanity/images/x.gif +0 -0
  56. data/lib/vanity/metric/active_record.rb +85 -0
  57. data/lib/vanity/metric/base.rb +244 -0
  58. data/lib/vanity/metric/google_analytics.rb +83 -0
  59. data/lib/vanity/metric/remote.rb +53 -0
  60. data/lib/vanity/playground.rb +396 -0
  61. data/lib/vanity/templates/_ab_test.erb +28 -0
  62. data/lib/vanity/templates/_experiment.erb +5 -0
  63. data/lib/vanity/templates/_experiments.erb +7 -0
  64. data/lib/vanity/templates/_metric.erb +14 -0
  65. data/lib/vanity/templates/_metrics.erb +13 -0
  66. data/lib/vanity/templates/_report.erb +27 -0
  67. data/lib/vanity/templates/_vanity.js.erb +20 -0
  68. data/lib/vanity/templates/flot.min.js +1 -0
  69. data/lib/vanity/templates/jquery.min.js +19 -0
  70. data/lib/vanity/templates/vanity.css +26 -0
  71. data/lib/vanity/templates/vanity.js +82 -0
  72. data/lib/vanity/version.rb +11 -0
  73. data/test/adapters/redis_adapter_test.rb +17 -0
  74. data/test/experiment/ab_test.rb +771 -0
  75. data/test/experiment/base_test.rb +150 -0
  76. data/test/experiments/age_and_zipcode.rb +19 -0
  77. data/test/experiments/metrics/cheers.rb +3 -0
  78. data/test/experiments/metrics/signups.rb +2 -0
  79. data/test/experiments/metrics/yawns.rb +3 -0
  80. data/test/experiments/null_abc.rb +5 -0
  81. data/test/metric/active_record_test.rb +277 -0
  82. data/test/metric/base_test.rb +293 -0
  83. data/test/metric/google_analytics_test.rb +104 -0
  84. data/test/metric/remote_test.rb +109 -0
  85. data/test/myapp/app/controllers/application_controller.rb +2 -0
  86. data/test/myapp/app/controllers/main_controller.rb +7 -0
  87. data/test/myapp/config/boot.rb +110 -0
  88. data/test/myapp/config/environment.rb +10 -0
  89. data/test/myapp/config/environments/production.rb +0 -0
  90. data/test/myapp/config/routes.rb +3 -0
  91. data/test/passenger_test.rb +43 -0
  92. data/test/playground_test.rb +26 -0
  93. data/test/rails_dashboard_test.rb +37 -0
  94. data/test/rails_helper_test.rb +36 -0
  95. data/test/rails_test.rb +389 -0
  96. data/test/test_helper.rb +145 -0
  97. data/vanity.gemspec +26 -0
  98. metadata +202 -0
@@ -0,0 +1,150 @@
1
+ require "test/test_helper"
2
+
3
+ class ExperimentTest < Test::Unit::TestCase
4
+
5
+ def setup
6
+ super
7
+ metric "Happiness"
8
+ end
9
+
10
+ # -- Defining experiment --
11
+
12
+ def test_can_access_experiment_by_id
13
+ exp = new_ab_test(:ice_cream_flavor) { metrics :happiness }
14
+ assert_equal exp, experiment(:ice_cream_flavor)
15
+ end
16
+
17
+ def test_fail_when_defining_same_experiment_twice
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)
30
+ end
31
+ end
32
+
33
+
34
+ # -- Loading experiments --
35
+
36
+ def test_fails_if_cannot_load_named_experiment
37
+ assert_raises NameError do
38
+ experiment(:ice_cream_flavor)
39
+ end
40
+ end
41
+
42
+ def test_loading_experiment
43
+ File.open "tmp/experiments/ice_cream_flavor.rb", "w" do |f|
44
+ f.write <<-RUBY
45
+ ab_test "Ice Cream Flavor" do
46
+ def xmts
47
+ "x"
48
+ end
49
+ metrics :happiness
50
+ end
51
+ RUBY
52
+ end
53
+ assert_equal "x", experiment(:ice_cream_flavor).xmts
54
+ end
55
+
56
+ def test_fails_if_error_loading_experiment
57
+ File.open "tmp/experiments/ice_cream_flavor.rb", "w" do |f|
58
+ f.write "fail 'yawn!'"
59
+ end
60
+ assert_raises NameError do
61
+ experiment(:ice_cream_flavor)
62
+ end
63
+ end
64
+
65
+ def test_complains_if_not_defined_where_expected
66
+ File.open "tmp/experiments/ice_cream_flavor.rb", "w" do |f|
67
+ f.write ""
68
+ end
69
+ assert_raises NameError do
70
+ experiment(:ice_cream_flavor)
71
+ end
72
+ end
73
+
74
+ def test_reloading_experiments
75
+ new_ab_test(:ab) { metrics :happiness }
76
+ new_ab_test(:cd) { metrics :happiness }
77
+ assert_equal 2, Vanity.playground.experiments.size
78
+ Vanity.playground.reload!
79
+ assert Vanity.playground.experiments.empty?
80
+ end
81
+
82
+
83
+ # -- Attributes --
84
+
85
+ def test_experiment_mapping_name_to_id
86
+ experiment = new_ab_test("Ice Cream Flavor/Tastes") { metrics :happiness }
87
+ assert_equal "Ice Cream Flavor/Tastes", experiment.name
88
+ assert_equal :ice_cream_flavor_tastes, experiment.id
89
+ end
90
+
91
+ def test_saving_experiment_after_definition
92
+ File.open "tmp/experiments/ice_cream_flavor.rb", "w" do |f|
93
+ f.write <<-RUBY
94
+ ab_test "Ice Cream Flavor" do
95
+ metrics :happiness
96
+ expects(:save)
97
+ end
98
+ RUBY
99
+ end
100
+ Vanity.playground.experiment(:ice_cream_flavor)
101
+ end
102
+
103
+ def test_experiment_has_created_timestamp
104
+ new_ab_test(:ice_cream_flavor) { metrics :happiness }
105
+ assert_instance_of Time, experiment(:ice_cream_flavor).created_at
106
+ assert_in_delta experiment(:ice_cream_flavor).created_at.to_i, Time.now.to_i, 1
107
+ end
108
+
109
+ def test_experiment_keeps_created_timestamp_across_definitions
110
+ past = Date.today - 1
111
+ Timecop.freeze past do
112
+ new_ab_test(:ice_cream_flavor) { metrics :happiness }
113
+ assert_equal past.to_time.to_i, experiment(:ice_cream_flavor).created_at.to_i
114
+ end
115
+
116
+ new_playground
117
+ metric :happiness
118
+ new_ab_test(:ice_cream_flavor) { metrics :happiness }
119
+ assert_equal past.to_time.to_i, experiment(:ice_cream_flavor).created_at.to_i
120
+ end
121
+
122
+ def test_experiment_has_description
123
+ new_ab_test :ice_cream_flavor do
124
+ description "Because 31 is not enough ..."
125
+ metrics :happiness
126
+ end
127
+ assert_equal "Because 31 is not enough ...", experiment(:ice_cream_flavor).description
128
+ end
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
+ # -- completion -- #
137
+
138
+ # check_completion is called by derived classes, but since it's
139
+ # part of the base_test I'm testing it here.
140
+ def test_error_in_check_completion
141
+ new_ab_test(:ab) { metrics :happiness }
142
+ e = experiment(:ab)
143
+ e.complete_if { true }
144
+ e.stubs(:complete!).raises(RuntimeError, "A forced error")
145
+ e.expects(:warn)
146
+ e.stubs(:identity).returns(:b)
147
+ e.track!(:a, Time.now, 10)
148
+ end
149
+
150
+ end
@@ -0,0 +1,19 @@
1
+ ab_test "Age and Zipcode" do
2
+ description <<-TEXT
3
+ Testing new registration form that asks for age and zipcode. Option A presents
4
+ the existing form, and option B adds age and zipcode fields.
5
+
6
+ We know option B will convert less, but higher quality leads. If we lose less
7
+ than 20% conversions, we're going to switch to option B.
8
+ TEXT
9
+ metrics :signups
10
+
11
+ complete_if do
12
+ alternatives.all? { |alt| alt.participants > 100 }
13
+ end
14
+ outcome_is do
15
+ one_field = alternative(false)
16
+ three_fields = alternative(true)
17
+ three_fields.conversion_rate >= 0.8 * one_field.conversion_rate ? three_fields : one_field
18
+ end
19
+ end
@@ -0,0 +1,3 @@
1
+ metric "Cheers" do
2
+ description "They love us, don't they?"
3
+ end
@@ -0,0 +1,2 @@
1
+ metric "Signups" do
2
+ end
@@ -0,0 +1,3 @@
1
+ metric "Yawns" do
2
+ description "How many yawns/sec can our video-sharing micro-blogging social network elicit?"
3
+ end
@@ -0,0 +1,5 @@
1
+ ab_test "Null/ABC" do
2
+ description "Testing three alternatives"
3
+ alternatives nil, :red, :green, :blue
4
+ metrics :signups
5
+ end
@@ -0,0 +1,277 @@
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
+ File.open "tmp/experiments/metrics/sky_is_limit.rb", "w" do |f|
46
+ f.write <<-RUBY
47
+ metric "Sky is limit" do
48
+ model Sky, :average=>:height
49
+ end
50
+ RUBY
51
+ end
52
+ Vanity.playground.metrics
53
+ Sky.create! :height=>8
54
+ Sky.create! :height=>2
55
+ assert_equal 5, Vanity::Metric.data(metric(:sky_is_limit)).last.last
56
+ end
57
+
58
+ test "record minimum" do
59
+ File.open "tmp/experiments/metrics/sky_is_limit.rb", "w" do |f|
60
+ f.write <<-RUBY
61
+ metric "Sky is limit" do
62
+ model Sky, :minimum=>:height
63
+ end
64
+ RUBY
65
+ end
66
+ Vanity.playground.metrics
67
+ Sky.create! :height=>4
68
+ Sky.create! :height=>2
69
+ assert_equal 2, Vanity::Metric.data(metric(:sky_is_limit)).last.last
70
+ end
71
+
72
+ test "record maximum" do
73
+ File.open "tmp/experiments/metrics/sky_is_limit.rb", "w" do |f|
74
+ f.write <<-RUBY
75
+ metric "Sky is limit" do
76
+ model Sky, :maximum=>:height
77
+ end
78
+ RUBY
79
+ end
80
+ Vanity.playground.metrics
81
+ Sky.create! :height=>4
82
+ Sky.create! :height=>2
83
+ assert_equal 4, Vanity::Metric.data(metric(:sky_is_limit)).last.last
84
+ end
85
+
86
+ test "with conditions" do
87
+ File.open "tmp/experiments/metrics/sky_is_limit.rb", "w" do |f|
88
+ f.write <<-RUBY
89
+ metric "Sky is limit" do
90
+ model Sky, :sum=>:height, :conditions=>["height > 4"]
91
+ end
92
+ RUBY
93
+ end
94
+ Vanity.playground.metrics
95
+ high_skies = 0
96
+ metric(:sky_is_limit).hook do |metric_id, timestamp, height|
97
+ assert height > 4
98
+ high_skies += height
99
+ end
100
+ [nil,5,3,6].each do |height|
101
+ Sky.create! :height=>height
102
+ end
103
+ assert_equal 11, Vanity::Metric.data(metric(:sky_is_limit)).sum(&:last)
104
+ assert_equal 11, high_skies
105
+ end
106
+
107
+ test "with scope" do
108
+ File.open "tmp/experiments/metrics/sky_is_limit.rb", "w" do |f|
109
+ f.write <<-RUBY
110
+ metric "Sky is limit" do
111
+ model Sky.high
112
+ end
113
+ RUBY
114
+ end
115
+ Vanity.playground.metrics
116
+ total = 0
117
+ metric(:sky_is_limit).hook do |metric_id, timestamp, count|
118
+ total += count
119
+ end
120
+ Sky.create! :height=>4
121
+ Sky.create! :height=>2
122
+ assert_equal 1, Vanity::Metric.data(metric(:sky_is_limit)).last.last
123
+ assert_equal 1, total
124
+ end
125
+
126
+ test "with timestamp" do
127
+ File.open "tmp/experiments/metrics/sky_is_limit.rb", "w" do |f|
128
+ f.write <<-RUBY
129
+ metric "Sky is limit" do
130
+ model Sky, :timestamp => :created_at
131
+ end
132
+ RUBY
133
+ end
134
+ Vanity.playground.metrics
135
+ Sky.create!
136
+ assert_equal 1, Sky.count
137
+ assert_equal 1, Vanity::Metric.data(metric(:sky_is_limit)).last.last
138
+ end
139
+
140
+ test "with timestamp and table" do
141
+ File.open "tmp/experiments/metrics/sky_is_limit.rb", "w" do |f|
142
+ f.write <<-RUBY
143
+ metric "Sky is limit" do
144
+ model Sky, :timestamp => 'skies.created_at'
145
+ end
146
+ RUBY
147
+ end
148
+ Vanity.playground.metrics
149
+ Sky.create!
150
+ assert_equal 1, Sky.count
151
+ assert_equal 1, Vanity::Metric.data(metric(:sky_is_limit)).last.last
152
+ end
153
+
154
+ test "hooks" do
155
+ File.open "tmp/experiments/metrics/sky_is_limit.rb", "w" do |f|
156
+ f.write <<-RUBY
157
+ metric "Sky is limit" do
158
+ model Sky, :sum=>:height
159
+ end
160
+ RUBY
161
+ end
162
+ Vanity.playground.metrics
163
+ total = 0
164
+ metric(:sky_is_limit).hook do |metric_id, timestamp, count|
165
+ assert_equal :sky_is_limit, metric_id
166
+ assert_in_delta Time.now.to_i, timestamp.to_i, 1
167
+ total += count
168
+ end
169
+ Sky.create! :height=>4
170
+ assert_equal 4, total
171
+ end
172
+
173
+ test "no hooks when metrics disabled" do
174
+ not_collecting!
175
+ File.open "tmp/experiments/metrics/sky_is_limit.rb", "w" do |f|
176
+ f.write <<-RUBY
177
+ metric "Sky is limit" do
178
+ model Sky, :sum=>:height
179
+ end
180
+ RUBY
181
+ end
182
+ Vanity.playground.metrics
183
+ total = 0
184
+ metric(:sky_is_limit).hook do |metric_id, timestamp, count|
185
+ total += count
186
+ end
187
+ Sky.create! :height=>4
188
+ assert_equal 0, total
189
+ end
190
+
191
+ test "after_create not after_save" do
192
+ File.open "tmp/experiments/metrics/sky_is_limit.rb", "w" do |f|
193
+ f.write <<-RUBY
194
+ metric "Sky is limit" do
195
+ model Sky
196
+ end
197
+ RUBY
198
+ end
199
+ Vanity.playground.metrics
200
+ once = nil
201
+ metric(:sky_is_limit).hook do
202
+ fail "Metric tracked twice" if once
203
+ once = true
204
+ end
205
+ Sky.create!
206
+ Sky.last.update_attributes :height=>4
207
+ end
208
+
209
+ test "with after_save" do
210
+ File.open "tmp/experiments/metrics/sky_is_limit.rb", "w" do |f|
211
+ f.write <<-RUBY
212
+ metric "Sky is limit" do
213
+ model Sky, :conditions=>["height > 3"]
214
+ Sky.after_save { |sky| track! if sky.height_changed? && sky.height > 3 }
215
+ end
216
+ RUBY
217
+ end
218
+ Vanity.playground.metrics
219
+ times = 0
220
+ metric(:sky_is_limit).hook do
221
+ times += 1
222
+ end
223
+ Sky.create!
224
+ (1..5).each do |height|
225
+ Sky.last.update_attributes! :height=>height
226
+ end
227
+ assert_equal 2, times
228
+ end
229
+
230
+ test "do it youself" do
231
+ File.open "tmp/experiments/metrics/sky_is_limit.rb", "w" do |f|
232
+ f.write <<-RUBY
233
+ metric "Sky is limit" do
234
+ Sky.after_save { |sky| track! if sky.height_changed? && sky.height > 3 }
235
+ end
236
+ RUBY
237
+ end
238
+ Vanity.playground.metrics
239
+ (1..5).each do |height|
240
+ Sky.create! :height=>height
241
+ end
242
+ Sky.first.update_attributes! :height=>4
243
+ assert_equal 3, Vanity::Metric.data(metric(:sky_is_limit)).last.last
244
+ end
245
+
246
+ test "last update for new metric" do
247
+ File.open "tmp/experiments/metrics/sky_is_limit.rb", "w" do |f|
248
+ f.write <<-RUBY
249
+ metric "Sky is limit" do
250
+ model Sky
251
+ end
252
+ RUBY
253
+ end
254
+ assert_nil metric(:sky_is_limit).last_update_at
255
+ end
256
+
257
+ test "last update with records" do
258
+ File.open "tmp/experiments/metrics/sky_is_limit.rb", "w" do |f|
259
+ f.write <<-RUBY
260
+ metric "Sky is limit" do
261
+ model Sky
262
+ end
263
+ RUBY
264
+ end
265
+ Sky.create! :height=>1
266
+ Timecop.freeze Time.now + 1.day do
267
+ Sky.create! :height=>1
268
+ end
269
+ assert_in_delta metric(:sky_is_limit).last_update_at.to_i, (Time.now + 1.day).to_i, 1
270
+ end
271
+
272
+ teardown do
273
+ Sky.delete_all
274
+ Sky.after_create.clear
275
+ Sky.after_save.clear
276
+ end
277
+ end