vanity 2.0.1 → 2.1.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 (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: