vanity 2.0.1 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -2
  3. data/Appraisals +6 -6
  4. data/CHANGELOG +9 -3
  5. data/Gemfile.lock +1 -1
  6. data/README.md +299 -0
  7. data/doc/configuring.textile +8 -1
  8. data/doc/identity.textile +2 -0
  9. data/doc/metrics.textile +10 -0
  10. data/gemfiles/rails32.gemfile.lock +1 -1
  11. data/gemfiles/rails41.gemfile.lock +1 -1
  12. data/gemfiles/rails42.gemfile.lock +1 -1
  13. data/gemfiles/{rails4.gemfile → rails42_protected_attributes.gemfile} +2 -2
  14. data/gemfiles/rails42_protected_attributes.gemfile.lock +209 -0
  15. data/lib/generators/templates/vanity_migration.rb +1 -0
  16. data/lib/vanity/adapters/abstract_adapter.rb +11 -0
  17. data/lib/vanity/adapters/active_record_adapter.rb +15 -1
  18. data/lib/vanity/adapters/mock_adapter.rb +14 -0
  19. data/lib/vanity/adapters/mongodb_adapter.rb +14 -0
  20. data/lib/vanity/adapters/redis_adapter.rb +15 -0
  21. data/lib/vanity/configuration.rb +43 -11
  22. data/lib/vanity/experiment/ab_test.rb +145 -15
  23. data/lib/vanity/experiment/alternative.rb +4 -0
  24. data/lib/vanity/frameworks/rails.rb +69 -31
  25. data/lib/vanity/locales/vanity.en.yml +9 -0
  26. data/lib/vanity/locales/vanity.pt-BR.yml +4 -0
  27. data/lib/vanity/metric/active_record.rb +9 -1
  28. data/lib/vanity/templates/_ab_test.erb +9 -2
  29. data/lib/vanity/templates/_experiment.erb +21 -1
  30. data/lib/vanity/templates/vanity.css +11 -3
  31. data/lib/vanity/templates/vanity.js +35 -6
  32. data/lib/vanity/version.rb +1 -1
  33. data/test/commands/report_test.rb +1 -0
  34. data/test/dummy/config/application.rb +1 -0
  35. data/test/experiment/ab_test.rb +414 -0
  36. data/test/experiment/base_test.rb +16 -10
  37. data/test/frameworks/rails/action_controller_test.rb +14 -6
  38. data/test/frameworks/rails/action_mailer_test.rb +8 -6
  39. data/test/frameworks/rails/action_view_test.rb +1 -0
  40. data/test/helper_test.rb +2 -0
  41. data/test/metric/active_record_test.rb +56 -0
  42. data/test/playground_test.rb +3 -0
  43. data/test/test_helper.rb +28 -2
  44. data/test/web/rails/dashboard_test.rb +2 -0
  45. data/vanity.gemspec +2 -2
  46. metadata +8 -8
  47. data/README.rdoc +0 -231
  48. data/gemfiles/rails4.gemfile.lock +0 -179
@@ -9,7 +9,7 @@ describe Vanity::Experiment::Base do
9
9
  # -- Defining experiment --
10
10
 
11
11
  it "can access experiment by id" do
12
- exp = new_ab_test(:ice_cream_flavor) { metrics :happiness }
12
+ exp = new_ab_test(:ice_cream_flavor) { metrics :happiness; default false }
13
13
  assert_equal exp, experiment(:ice_cream_flavor)
14
14
  end
15
15
 
@@ -17,8 +17,10 @@ describe Vanity::Experiment::Base do
17
17
  File.open "tmp/experiments/ice_cream_flavor.rb", "w" do |f|
18
18
  f.write <<-RUBY
19
19
  ab_test "Ice Cream Flavor" do
20
+ default false
20
21
  end
21
22
  ab_test "Ice Cream Flavor" do
23
+ default false
22
24
  end
23
25
  RUBY
24
26
  end
@@ -44,6 +46,7 @@ describe Vanity::Experiment::Base do
44
46
  def xmts
45
47
  "x"
46
48
  end
49
+ default false
47
50
  end
48
51
  RUBY
49
52
  end
@@ -72,8 +75,8 @@ describe Vanity::Experiment::Base do
72
75
  end
73
76
 
74
77
  it "reloading experiments" do
75
- new_ab_test(:ab) { metrics :happiness }
76
- new_ab_test(:cd) { metrics :happiness }
78
+ new_ab_test(:ab) { metrics :happiness; default false }
79
+ new_ab_test(:cd) { metrics :happiness; default false }
77
80
  assert_equal 2, Vanity.playground.experiments.size
78
81
  Vanity.playground.reload!
79
82
  assert Vanity.playground.experiments.empty?
@@ -83,7 +86,7 @@ describe Vanity::Experiment::Base do
83
86
  # -- Attributes --
84
87
 
85
88
  it "maps the experiment name to id" do
86
- experiment = new_ab_test("Ice Cream Flavor/Tastes") { metrics :happiness }
89
+ experiment = new_ab_test("Ice Cream Flavor/Tastes") { metrics :happiness; default false }
87
90
  assert_equal "Ice Cream Flavor/Tastes", experiment.name
88
91
  assert_equal :ice_cream_flavor_tastes, experiment.id
89
92
  end
@@ -92,15 +95,17 @@ describe Vanity::Experiment::Base do
92
95
  File.open "tmp/experiments/ice_cream_flavor.rb", "w" do |f|
93
96
  f.write <<-RUBY
94
97
  ab_test "Ice Cream Flavor" do
98
+ default false
95
99
  end
96
100
  RUBY
97
101
  end
98
102
  Vanity.unload!
103
+ metric :happiness
99
104
  Vanity.playground.experiment(:ice_cream_flavor)
100
105
  end
101
106
 
102
107
  it "has created timestamp" do
103
- new_ab_test(:ice_cream_flavor) { metrics :happiness }
108
+ new_ab_test(:ice_cream_flavor) { metrics :happiness; default false }
104
109
  assert_kind_of Time, experiment(:ice_cream_flavor).created_at
105
110
  assert_in_delta experiment(:ice_cream_flavor).created_at.to_i, Time.now.to_i, 1
106
111
  end
@@ -108,12 +113,12 @@ describe Vanity::Experiment::Base do
108
113
  it "keeps created timestamp across definitions" do
109
114
  past = Date.today - 1
110
115
  Timecop.freeze past.to_time do
111
- new_ab_test(:ice_cream_flavor) { metrics :happiness }
116
+ new_ab_test(:ice_cream_flavor) { metrics :happiness; default false }
112
117
  end
113
118
 
114
119
  vanity_reset
115
120
  metric :happiness
116
- new_ab_test(:ice_cream_flavor) { metrics :happiness }
121
+ new_ab_test(:ice_cream_flavor) { metrics :happiness; default false }
117
122
  assert_equal past.to_time.to_i, experiment(:ice_cream_flavor).created_at.to_i
118
123
  end
119
124
 
@@ -121,13 +126,14 @@ describe Vanity::Experiment::Base do
121
126
  new_ab_test :ice_cream_flavor do
122
127
  description "Because 31 is not enough ..."
123
128
  metrics :happiness
129
+ default false
124
130
  end
125
131
  assert_equal "Because 31 is not enough ...", experiment(:ice_cream_flavor).description
126
132
  end
127
133
 
128
134
  it "stores nothing when collection disabled" do
129
135
  not_collecting!
130
- new_ab_test(:ice_cream_flavor) { metrics :happiness }
136
+ new_ab_test(:ice_cream_flavor) { metrics :happiness; default false }
131
137
  experiment(:ice_cream_flavor).complete!
132
138
  end
133
139
 
@@ -136,7 +142,7 @@ describe Vanity::Experiment::Base do
136
142
  # check_completion is called by derived classes, but since it's
137
143
  # part of the base_test I'm testing it here.
138
144
  it "handles error in check completion" do
139
- new_ab_test(:ab) { metrics :happiness }
145
+ new_ab_test(:ab) { metrics :happiness; default false }
140
146
  e = experiment(:ab)
141
147
  e.complete_if { true }
142
148
  e.stubs(:complete!).raises(RuntimeError, "A forced error")
@@ -146,7 +152,7 @@ describe Vanity::Experiment::Base do
146
152
  end
147
153
 
148
154
  it "complete updates completed_at" do
149
- new_ab_test(:ice_cream_flavor) { metrics :happiness }
155
+ new_ab_test(:ice_cream_flavor) { metrics :happiness; default false }
150
156
 
151
157
  time = Time.utc(2008, 9, 1, 12, 0, 0)
152
158
  Timecop.freeze(time) do
@@ -33,6 +33,8 @@ class UseVanityControllerTest < ActionController::TestCase
33
33
  metric :sugar_high
34
34
  new_ab_test :pie_or_cake do
35
35
  metrics :sugar_high
36
+ alternatives :pie, :cake
37
+ default :pie
36
38
  end
37
39
 
38
40
  # Class eval this instead of including in the controller to delay
@@ -64,13 +66,13 @@ class UseVanityControllerTest < ActionController::TestCase
64
66
  end
65
67
 
66
68
  def test_chooses_sets_alternatives_for_rails_tests
67
- experiment(:pie_or_cake).chooses(true)
69
+ experiment(:pie_or_cake).chooses(:pie)
68
70
  get :index
69
- assert_equal 'true', @response.body
71
+ assert_equal 'pie', @response.body
70
72
 
71
- experiment(:pie_or_cake).chooses(false)
73
+ experiment(:pie_or_cake).chooses(:cake)
72
74
  get :index
73
- assert_equal 'false', @response.body
75
+ assert_equal 'cake', @response.body
74
76
  end
75
77
 
76
78
  def test_adds_participant_to_experiment
@@ -91,7 +93,7 @@ class UseVanityControllerTest < ActionController::TestCase
91
93
  assert_match /vanity_id=[a-f0-9]{32};/, cookie
92
94
  expires = cookie[/expires=(.*)(;|$)/, 1]
93
95
  assert expires
94
- assert_in_delta Time.parse(expires), Time.now + 1.month, 1.day
96
+ assert_in_delta Time.parse(expires), Time.now + 20 * 365 * 24 * 60 * 60, 1.day
95
97
  end
96
98
 
97
99
  def test_vanity_cookie_default_id
@@ -105,6 +107,12 @@ class UseVanityControllerTest < ActionController::TestCase
105
107
  assert_equal "from_last_time", cookies["vanity_id"]
106
108
  end
107
109
 
110
+ def test_vanity_cookie_uses_configuration
111
+ Vanity.configuration.cookie_name = "new_id"
112
+ get :index
113
+ assert cookies["new_id"] =~ /^[a-f0-9]{32}$/
114
+ end
115
+
108
116
  def test_vanity_identity_set_from_cookie
109
117
  @request.cookies["vanity_id"] = "from_last_time"
110
118
  get :index
@@ -204,4 +212,4 @@ class UseVanityControllerTest < ActionController::TestCase
204
212
  assert_match /domain=.foo.bar/, @response["Set-Cookie"] if ::Rails.respond_to?(:application)
205
213
  end
206
214
 
207
- end
215
+ end
@@ -28,13 +28,15 @@ class UseVanityMailerTest < ActionMailer::TestCase
28
28
  metric :sugar_high
29
29
  new_ab_test :pie_or_cake do
30
30
  metrics :sugar_high
31
+ alternatives :pie, :cake
32
+ default :pie
31
33
  end
32
34
  end
33
35
 
34
36
  def test_js_enabled_still_adds_participant
35
37
  Vanity.playground.use_js!
36
38
  experiment(:pie_or_cake).identify { }
37
- experiment(:pie_or_cake).chooses(true)
39
+ experiment(:pie_or_cake).chooses(:pie)
38
40
  VanityMailer.ab_test_subject(nil)
39
41
 
40
42
 
@@ -45,13 +47,13 @@ class UseVanityMailerTest < ActionMailer::TestCase
45
47
  def test_returns_different_alternatives
46
48
  experiment(:pie_or_cake).identify { }
47
49
 
48
- experiment(:pie_or_cake).chooses(true)
50
+ experiment(:pie_or_cake).chooses(:pie)
49
51
  email = VanityMailer.ab_test_subject(nil)
50
- assert_equal 'true', email.subject
52
+ assert_equal 'pie', email.subject
51
53
 
52
- experiment(:pie_or_cake).chooses(false)
54
+ experiment(:pie_or_cake).chooses(:cake)
53
55
  email = VanityMailer.ab_test_subject(nil)
54
- assert_equal 'false', email.subject
56
+ assert_equal 'cake', email.subject
55
57
  end
56
58
 
57
59
  def test_tracking_image_is_rendered
@@ -59,4 +61,4 @@ class UseVanityMailerTest < ActionMailer::TestCase
59
61
  assert email.body =~ /<img/
60
62
  assert email.body =~ /_identity=/
61
63
  end
62
- end
64
+ end
@@ -10,6 +10,7 @@ class RailsHelperTest < ActionView::TestCase
10
10
  metrics :sugar_high
11
11
  identify { '1' }
12
12
  alternatives :pie, :cake
13
+ default :pie
13
14
  end
14
15
  end
15
16
 
data/test/helper_test.rb CHANGED
@@ -6,6 +6,7 @@ describe Vanity::Helpers do
6
6
  metric "Coolness"
7
7
  new_ab_test :foobar do
8
8
  alternatives "foo", "bar"
9
+ default "foo"
9
10
  metrics :coolness
10
11
  end
11
12
  Vanity.track!(:coolness, :identity=>'quux')
@@ -17,6 +18,7 @@ describe Vanity::Helpers do
17
18
  metric "Coolness"
18
19
  new_ab_test :foobar do
19
20
  alternatives "foo", "bar"
21
+ default "foo"
20
22
  metrics :coolness
21
23
  end
22
24
  Vanity.track!(:coolness, :identity=>'quux', :values=>[2])
@@ -1,7 +1,15 @@
1
1
  require "test_helper"
2
2
 
3
+ class User < ActiveRecord::Base
4
+ has_many :skies
5
+
6
+ attr_accessible :height if defined?(ProtectedAttributes)
7
+ end
3
8
  class Sky < ActiveRecord::Base
9
+ belongs_to :user
4
10
  scope :high, lambda { where("height >= 4") }
11
+
12
+ attr_accessible :height if defined?(ProtectedAttributes)
5
13
  end
6
14
 
7
15
  if ENV["DB"] == "active_record"
@@ -9,13 +17,20 @@ if ENV["DB"] == "active_record"
9
17
  describe Vanity::Metric::ActiveRecord do
10
18
 
11
19
  before do
20
+ User.connection.create_table(:users) do |t|
21
+ t.timestamps
22
+ end
12
23
  Sky.connection.create_table(:skies) do |t|
24
+ t.integer :user_id
13
25
  t.integer :height
14
26
  t.timestamps
15
27
  end
16
28
  end
17
29
 
18
30
  after do
31
+ User.connection.drop_table(:users) if User.connection.table_exists?(User.table_name)
32
+ User.reset_callbacks(:create)
33
+ User.reset_callbacks(:save)
19
34
  Sky.connection.drop_table(:skies) if Sky.connection.table_exists?(Sky.table_name)
20
35
  Sky.reset_callbacks(:create)
21
36
  Sky.reset_callbacks(:save)
@@ -235,6 +250,47 @@ describe Vanity::Metric::ActiveRecord do
235
250
  assert_equal 2, times
236
251
  end
237
252
 
253
+ it "with model identity" do
254
+ File.open "tmp/experiments/metrics/sky_is_limit.rb", "w" do |f|
255
+ f.write <<-RUBY
256
+ metric "Sky is limit" do
257
+ model Sky, :identity => lambda { |record| record.user_id }
258
+ end
259
+ RUBY
260
+ end
261
+
262
+ File.open "tmp/experiments/simple.rb", "w" do |f|
263
+ f.write <<-RUBY
264
+ ab_test "simple" do
265
+ metrics :sky_is_limit
266
+ alternatives :a, :b
267
+ identity { "me" }
268
+ end
269
+ RUBY
270
+ end
271
+
272
+ user = User.create
273
+ Vanity.context = stub(vanity_identity: user.id)
274
+ experiment = Vanity.playground.experiment(:simple)
275
+ experiment.choose
276
+
277
+ Vanity.context = nil
278
+ # Should count as a conversion for the user in the experiment.
279
+ user.skies.create
280
+
281
+ # Should count as a conversion for the newly created User.
282
+ User.create.skies.create
283
+
284
+ Vanity.context = stub(vanity_identity: "other")
285
+ experiment.choose
286
+ # Should count as a conversion for "other"
287
+ Sky.create
288
+
289
+ assert_equal 3, Sky.count
290
+ assert_equal 2, experiment.alternatives.map(&:participants).sum
291
+ assert_equal 3, experiment.alternatives.map(&:conversions).sum
292
+ end
293
+
238
294
  it "do it yourself" do
239
295
  File.open "tmp/experiments/metrics/sky_is_limit.rb", "w" do |f|
240
296
  f.write <<-RUBY
@@ -19,6 +19,7 @@ describe Vanity::Playground do
19
19
  f.write <<-RUBY
20
20
  ab_test :foobar do
21
21
  metrics :coolness
22
+ default false
22
23
  end
23
24
  RUBY
24
25
  end
@@ -84,6 +85,7 @@ describe Vanity::Playground do
84
85
  alternatives "foo", "bar"
85
86
  identify { "abcdef" }
86
87
  metrics :coolness
88
+ default "foo"
87
89
  end
88
90
 
89
91
  assert Vanity.playground.experiments_persisted?
@@ -122,6 +124,7 @@ describe Vanity::Playground do
122
124
  alternatives "foo", "bar"
123
125
  identify { "abcdef" }
124
126
  metrics :coolness
127
+ default "foo"
125
128
  end
126
129
  alt = experiment(:foobar).choose
127
130
  assert_equal [[Vanity.playground.experiment(:foobar), alt]], Vanity.playground.participant_info("abcdef")
data/test/test_helper.rb CHANGED
@@ -81,6 +81,23 @@ module VanityTestHelpers
81
81
  Vanity.unload!
82
82
  end
83
83
 
84
+ # Call this on teardown. It wipes put the playground and any state held in it
85
+ # (mostly experiments), resets vanity ID, and clears database of all experiments.
86
+ def nuke_playground
87
+ Vanity.playground.connection.flushdb
88
+ new_playground
89
+ end
90
+
91
+ # Call this if you need a new playground, e.g. to re-define the same experiment,
92
+ # or reload an experiment (saved by the previous playground).
93
+ def new_playground
94
+ Vanity.playground = Vanity::Playground.new
95
+ Vanity.disconnect!
96
+ ActiveRecord::Base.establish_connection
97
+ Vanity.connect!(DATABASE)
98
+ end
99
+
100
+
84
101
  # Defines the specified metrics (one or more names). Returns metric, or array
85
102
  # of metric (if more than one argument).
86
103
  def metric(*names)
@@ -92,11 +109,20 @@ module VanityTestHelpers
92
109
  end
93
110
 
94
111
  # Defines an A/B experiment.
95
- def new_ab_test(name, &block)
112
+ # @param [Hash] options Options include:
113
+ # [Boolean] enable (default true) - Whether or not to enable this ab_test when it gets instantiated;
114
+ # this flag is here to simply the testing of experiment features, and also to allow
115
+ # testing of the default behavior of an experiment when it gets loaded.
116
+ # Note that :enable => false does NOT mean to set the ab_test to false; it
117
+ # means to not set enabled at all (the 'actual' behavior).
118
+ def new_ab_test(name, options = {}, &block)
119
+ enable = options.fetch(:enable, true)
96
120
  id = name.to_s.downcase.gsub(/\W/, "_").to_sym
97
121
  experiment = Vanity::Experiment::AbTest.new(Vanity.playground, id, name)
98
- experiment.instance_eval(&block)
122
+ experiment.instance_eval &block if block
99
123
  experiment.save
124
+ # new experiments start off as disabled, enable them for testing
125
+ experiment.enabled = true if enable
100
126
  Vanity.playground.experiments[id] = experiment
101
127
  end
102
128
 
@@ -14,12 +14,14 @@ class RailsDashboardTest < ActionController::TestCase
14
14
  new_ab_test :food do
15
15
  alternatives :apple, :orange
16
16
  metrics :sugar_high
17
+ default :apple
17
18
  identify { '1' }
18
19
  end
19
20
 
20
21
  metric :liquidity
21
22
  new_ab_test :drink do
22
23
  alternatives :tea, :coffee
24
+ default :tea
23
25
  metrics :liquidity
24
26
  identify { '1' }
25
27
  end
data/vanity.gemspec CHANGED
@@ -17,8 +17,8 @@ Gem::Specification.new do |spec|
17
17
  spec.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
18
  spec.require_paths = ["lib"]
19
19
 
20
- spec.extra_rdoc_files = "README.rdoc", "CHANGELOG"
21
- spec.rdoc_options = "--title", "Vanity #{spec.version}", "--main", "README.rdoc",
20
+ spec.extra_rdoc_files = "README.md", "CHANGELOG"
21
+ spec.rdoc_options = "--title", "Vanity #{spec.version}", "--main", "README.md",
22
22
  "--webcvs", "http://github.com/assaf/#{spec.name}"
23
23
 
24
24
  spec.required_ruby_version = ">= 1.9.3"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vanity
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.1
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Assaf Arkin
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-01-18 00:00:00.000000000 Z
11
+ date: 2016-01-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: i18n
@@ -58,7 +58,7 @@ executables:
58
58
  - vanity
59
59
  extensions: []
60
60
  extra_rdoc_files:
61
- - README.rdoc
61
+ - README.md
62
62
  - CHANGELOG
63
63
  files:
64
64
  - .autotest
@@ -69,7 +69,7 @@ files:
69
69
  - Gemfile
70
70
  - Gemfile.lock
71
71
  - MIT-LICENSE
72
- - README.rdoc
72
+ - README.md
73
73
  - Rakefile
74
74
  - bin/vanity
75
75
  - doc/_config.yml
@@ -99,12 +99,12 @@ files:
99
99
  - doc/site.js
100
100
  - gemfiles/rails32.gemfile
101
101
  - gemfiles/rails32.gemfile.lock
102
- - gemfiles/rails4.gemfile
103
- - gemfiles/rails4.gemfile.lock
104
102
  - gemfiles/rails41.gemfile
105
103
  - gemfiles/rails41.gemfile.lock
106
104
  - gemfiles/rails42.gemfile
107
105
  - gemfiles/rails42.gemfile.lock
106
+ - gemfiles/rails42_protected_attributes.gemfile
107
+ - gemfiles/rails42_protected_attributes.gemfile.lock
108
108
  - lib/generators/templates/vanity_migration.rb
109
109
  - lib/generators/vanity/views_generator.rb
110
110
  - lib/generators/vanity_generator.rb
@@ -211,9 +211,9 @@ metadata: {}
211
211
  post_install_message: To get started run vanity --help
212
212
  rdoc_options:
213
213
  - --title
214
- - Vanity 2.0.1
214
+ - Vanity 2.1.0
215
215
  - --main
216
- - README.rdoc
216
+ - README.md
217
217
  - --webcvs
218
218
  - http://github.com/assaf/vanity
219
219
  require_paths: