vanity 2.1.0 → 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 785019e8d6f59f84073d7645a9b8a12c7c07edf0
4
- data.tar.gz: 75b44173bbc79629b5c70d6bc545e3cdffb73962
3
+ metadata.gz: c9f59504780981f9e1f351de1afd9774374ca1d4
4
+ data.tar.gz: 2a211bbebf1c899a256bbfa81f86a161e91155ed
5
5
  SHA512:
6
- metadata.gz: a5b228f3a04a5f28db6a9c927b685c211340f3d6f4612e156d533683b35ede1089571c22119ff1db522259cb285948c63fba9d0c1b78ce9624f8c48b85ffb4ba
7
- data.tar.gz: 1b337660bc064b51074dfd3979cfa32696b4a103d98ad1adac987e58376f613a8a384a8f58423a317a36b08ec94f39784688477053347281918f5278b19e5a52
6
+ metadata.gz: d464e14f4116528ffdd94196daadb15a285d20e011262a19cbe91b567c38b822ceae28daf510f759681fd84a0f6d484959d53d8e24706d59674d9a737768107f
7
+ data.tar.gz: cb0c09bf9293ffbc677fbd2260bc5dc5c19d3daade26fbec926d9f4e17dcfd12a71222fae552b1daa11d4d3a8e285a2d6b176c593d15d394b0ad2135dbe50695
data/.travis.yml CHANGED
@@ -4,7 +4,7 @@ language: ruby
4
4
  before_script:
5
5
  - 'echo ''gem: --no-ri --no-rdoc'' > ~/.gemrc' # skip installing docs for gems
6
6
  before_install:
7
- - gem install bundler -v="1.8.0" # Minimum version of bundler required
7
+ - gem install bundler -v ">= 1.8.0" # Minimum version of bundler required
8
8
  bundler_args: --without development
9
9
  script: 'bundle exec rake test'
10
10
  services:
@@ -14,6 +14,7 @@ rvm:
14
14
  - 2.0.0
15
15
  - 2.1.0
16
16
  - 2.2.0
17
+ - 2.3.0
17
18
  - jruby-19mode
18
19
  env:
19
20
  - DB=mongodb
data/Appraisals CHANGED
@@ -5,6 +5,7 @@ appraise "rails32" do
5
5
  gem "minitest_tu_shim", "~> 1.3.3", :platforms => :mri_22
6
6
  gem "fastthread", :git => "git://github.com/zoltankiss/fastthread.git", :platforms => :mri_20
7
7
  gem "passenger", "~>3.0"
8
+ gem "test-unit", "~> 3.0"
8
9
  end
9
10
 
10
11
  appraise "rails41" do
data/CHANGELOG CHANGED
@@ -1,3 +1,9 @@
1
+ == 2.1.1 (2015-1-19)
2
+
3
+ * Allow method passed to `use_vanity` to return NullObjects. (@phillbaker)
4
+ * Add testing support for ruby 2.3. (@phillbaker)
5
+ * Add `reject` method to experiment definition. #290 (@peterkovacs)
6
+
1
7
  == 2.1.0 (2015-1-19)
2
8
 
3
9
  Add ability to define test alternatives with custom probabilities, #283. (@peterkovacs)
data/Gemfile CHANGED
@@ -11,7 +11,7 @@ gem "mongo"
11
11
 
12
12
  # Math libraries
13
13
  gem "integration", "<= 0.1.0"
14
- gem "rubystats"
14
+ gem "rubystats", ">= 0.2.4"
15
15
 
16
16
  # APIs
17
17
  gem "garb", "< 0.9.2", :require => false # API changes at this version
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- vanity (2.1.0)
4
+ vanity (2.1.1)
5
5
  i18n
6
6
 
7
7
  GEM
@@ -93,7 +93,7 @@ GEM
93
93
  redis (3.0.6)
94
94
  redis-namespace (1.3.2)
95
95
  redis (~> 3.0.4)
96
- rubystats (0.2.3)
96
+ rubystats (0.2.4)
97
97
  safe_yaml (1.0.4)
98
98
  sass (3.4.13)
99
99
  sqlite3 (1.3.10)
@@ -118,7 +118,7 @@ DEPENDENCIES
118
118
  activerecord-jdbc-adapter
119
119
  appraisal (~> 1.0.2)
120
120
  bson_ext
121
- bundler (>= 1.0.0)
121
+ bundler (>= 1.8.0)
122
122
  fakefs
123
123
  garb (< 0.9.2)
124
124
  integration (<= 0.1.0)
@@ -130,7 +130,7 @@ DEPENDENCIES
130
130
  rake
131
131
  redis (>= 2.1)
132
132
  redis-namespace (>= 1.1.0)
133
- rubystats
133
+ rubystats (>= 0.2.4)
134
134
  sqlite3 (~> 1.3.10)
135
135
  timecop
136
136
  vanity!
data/MIT-LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2010 Assaf Arkin
1
+ Copyright (c) 2009-2016 Assaf Arkin and Contributors
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -100,10 +100,17 @@ development:
100
100
  active_record_adapter: sqlite3
101
101
  database: db/development.sqlite3
102
102
  test:
103
- collecting: false
104
- production:
105
103
  adapter: active_record
106
104
  active_record_adapter: default
105
+ collecting: false
106
+ production:
107
+ active_record_adapter: postgresql
108
+ <% uri = URI.parse(ENV['DATABASE_URL']) %>
109
+ host: <%= uri.host %>
110
+ username: <%= uri.username %>
111
+ password: <%= uri.password %>
112
+ port: <%= uri.port %>
113
+ database: <%= uri.path.sub('/', '') %>
107
114
  ```
108
115
 
109
116
  If you're going to store data in the database, run the generator and
@@ -251,6 +258,9 @@ Here's what's tested and known to work:
251
258
  Ruby 2.2
252
259
  Persistence: Redis, Mongo, ActiveRecord
253
260
  Rails: 3.2, 4.1, 4.2
261
+ Ruby 2.3
262
+ Persistence: Redis, Mongo, ActiveRecord
263
+ Rails: 3.2, 4.1, 4.2
254
264
 
255
265
  ## Testing
256
266
 
@@ -259,6 +269,9 @@ an experiment. This may be done using the `chooses` method. For example:
259
269
 
260
270
  ```ruby
261
271
  Vanity.playground.experiment(:price_options).chooses(19)
272
+ ```
273
+
274
+ See [the docs on testing](http://vanity.labnotes.org/ab_testing.html#test) for more.
262
275
 
263
276
  ## Updating documentation
264
277
 
@@ -51,6 +51,9 @@ The other half will see this one:
51
51
  <a href="#" style="font:14pt;font-weight:bold">Sign up</a>
52
52
  </notextile>
53
53
 
54
+
55
+ h3(#interpret). Interpreting the Results
56
+
54
57
  An A/B test has two parts, we just covered the part which decides which alternative to show. The second part measures the effectiveness of each alternative. This happens as result of measuring the metric.
55
58
 
56
59
  Remember that we're measuring signups, so we already have this in the code:
@@ -65,8 +68,6 @@ end
65
68
  </pre>
66
69
 
67
70
 
68
- h3(#interpret). Interpreting the Results
69
-
70
71
  We're going to let the experiment run for a while and track the results using "the dashboard":rails.html#dashboard, or by running the command @vanity report@.
71
72
 
72
73
  Vanity splits the audience randomly -- using "cookies and other mechanisms":identity.html -- and records who got to see each alternative, and how many in each group converted (in our case, signed up). Dividing conversions by participants gives you the conversion rate.
@@ -137,6 +138,32 @@ end
137
138
  <% end %>
138
139
  </pre>
139
140
 
141
+ h3(#weights). Weighted alternatives & Multi-arm bandits
142
+
143
+ By default, for n variations, Vanity will assign each of those with a probability of 1/n. If non-uniform weights for alternatives are desired, weights can be assigned to different alternatives. For example:
144
+
145
+ <pre>
146
+ ab_test "Background color" do
147
+ metrics :coolness
148
+ alternatives "red" => 10, "blue" => 5, "orange => 1
149
+ default "red"
150
+ end
151
+ </pre>
152
+
153
+ This would make "red" 10 times as likely to appear as orange. (Note that these are weights, not percentanges, so the probability of assigning red above is about 62%, while blue is 31% and orange is 6%.) This is useful, for example, to assign a higher weight to a 'control' variation to ensure that most users continue having the default experience while assigning lower probabilities to test variations.
154
+
155
+ h4(#bandits). Multi-arm bandits
156
+
157
+ Another alternative to uniform splits of traffic is called "multi-armed bandits", and the specific implementation included is Bayesian. In this mode, most traffic is sent to the currently best performing alternative (called exploitation). The minority of traffic is split between the available alternatives (called exploration). Since worse performing alternatives receive much less traffic, this leads to higher average conversion rates in bandit-driven experiments than traditional A/B split testing. The disadvantage is that in the worst case scenario, it can take a lot more traffic to declare a conclusion with statistical significance.
158
+
159
+ This can be enabled in the experiment definition, for example:
160
+ <pre>
161
+ ab_test "noodle_test" do
162
+ alternatives "spaghetti", "linguine"
163
+ metrics :signup
164
+ score_method :bayes_bandit_score
165
+ end
166
+ </pre>
140
167
 
141
168
  h3(#test). A/B Testing and Code Testing
142
169
 
data/doc/identity.textile CHANGED
@@ -9,29 +9,45 @@ For effective A/B tests, you want to:
9
9
  # Know which alternative you're showing and tracking
10
10
  # When running multiple tests at once, keep them independent
11
11
 
12
- Vanity will assign each visitor a unique identifier and store it in a cookie that persists across sessions. That way, each visitor will get to see the same alternatives on repeating visits. Assuming they use the same browser on all visits.
12
+ Vanity can assign each visitor a unique identifier and store it in a cookie that persists across sessions. That way, each visitor will get to see the same alternatives on repeating visits. (Assuming they use the same browser on all visits.)
13
13
 
14
- If you have a better way of tracking visitors, e.g. using sign in, you'll want to use that instead. That way the same person gets treated to the same experiment, even if they switch between machines and browsers.
14
+ If you have a more persistant way of tracking visitors, e.g. using sign in, you'll want to use that instead. That way the same person gets treated to the same experiment, even if they switch between machines and browsers.
15
15
 
16
- You can choose either option using the @use_vanity@ method. In the first case, just call @use_vanity@ from within the @ApplicationController@. In the second case, you'll want to pass @use_vanity@ either a block that returns an identity value, or the name of a method that returns an object which provides the identity.
16
+ You can choose either option using the @use_vanity@ method. In the first case, just call @use_vanity@ from within the @ApplicationController@. In the second case, you'll want to pass @use_vanity@ either a block that returns an identity value, or the name of a method that returns an object which provides the identity when @id@ is called on it. If you pass a method name and it returns @nil@, Vanity will fallback on the persistent cookie mechanism.
17
17
 
18
- Sounds complicated? These two examples are equivalent:
18
+ Sounds complicated? These two examples are equivalent:
19
19
 
20
20
  <pre>
21
21
  class ApplicationController < ActionController::Base
22
22
  use_vanity :current_user
23
+
24
+ def current_user
25
+ User.find(session[:user_id])
26
+ end
23
27
  end
24
28
  </pre>
25
29
 
26
30
  <pre>
27
31
  class ApplicationController < ActionController::Base
28
- use_vanity { |c| c.current_user && c.current_user.id }
32
+ use_vanity :current_user
33
+
34
+ def current_user
35
+ user = User.find(session[:user_id])
36
+
37
+ user if user && user.id
38
+ end
29
39
  end
30
40
  </pre>
31
41
 
32
- If you use either block or method name and they return @nil@, Vanity will fallback on persistent cookie mechanism.
42
+ If @current_user@ does not return @nil@ for non-persisted accounts, e.g. it returns a @NullUser@, the @id@ call should return @nil@.
33
43
 
34
- Note that @current_user@ in this case must return @nil@ for non-persisted accounts. That is, @current_user.id@ *must* be non-@nil@.
44
+ If you pass a block, it must return a non-nil value. For example:
45
+
46
+ <pre>
47
+ class ProjectController < ApplicationController
48
+ use_vanity { |controller| controller.params[:project_id] || }
49
+ end
50
+ </pre>
35
51
 
36
52
  An identity can be anything. For example, if you're running an experiment to test a new feature that will be available in some projects but not others, you'll want to slice the audience by project identifier, not user ID.
37
53
 
@@ -7,7 +7,7 @@ gem "redis", ">= 2.1"
7
7
  gem "redis-namespace", ">= 1.1.0"
8
8
  gem "mongo"
9
9
  gem "integration", "<= 0.1.0"
10
- gem "rubystats"
10
+ gem "rubystats", ">= 0.2.4"
11
11
  gem "garb", "< 0.9.2", :require => false
12
12
  gem "timecop", :require => false
13
13
  gem "webmock", :require => false
@@ -18,6 +18,7 @@ gem "rails", "3.2.22"
18
18
  gem "minitest_tu_shim", "~> 1.3.3", :platforms => :mri_22
19
19
  gem "fastthread", :git => "git://github.com/zoltankiss/fastthread.git", :platforms => :mri_20
20
20
  gem "passenger", "~>3.0"
21
+ gem "test-unit", "~> 3.0"
21
22
 
22
23
  group :development do
23
24
  gem "appraisal", "~> 1.0.2"
@@ -7,7 +7,7 @@ GIT
7
7
  PATH
8
8
  remote: ..
9
9
  specs:
10
- vanity (2.1.0)
10
+ vanity (2.1.1)
11
11
  i18n
12
12
 
13
13
  GEM
@@ -133,6 +133,7 @@ GEM
133
133
  rake (>= 0.8.1)
134
134
  polyglot (0.3.5)
135
135
  posix-spawn (0.3.11)
136
+ power_assert (0.2.2)
136
137
  pygments.rb (0.6.3)
137
138
  posix-spawn (~> 0.3.6)
138
139
  yajl-ruby (~> 1.2.0)
@@ -168,7 +169,7 @@ GEM
168
169
  redis (3.0.6)
169
170
  redis-namespace (1.3.2)
170
171
  redis (~> 3.0.4)
171
- rubystats (0.2.3)
172
+ rubystats (0.2.4)
172
173
  safe_yaml (1.0.4)
173
174
  sass (3.4.13)
174
175
  sprockets (2.2.3)
@@ -177,6 +178,8 @@ GEM
177
178
  rack (~> 1.0)
178
179
  tilt (~> 1.1, != 1.3.0)
179
180
  sqlite3 (1.3.10)
181
+ test-unit (3.0.9)
182
+ power_assert
180
183
  thor (0.19.1)
181
184
  tilt (1.4.1)
182
185
  timecop (0.3.5)
@@ -203,7 +206,7 @@ DEPENDENCIES
203
206
  activerecord-jdbc-adapter
204
207
  appraisal (~> 1.0.2)
205
208
  bson_ext
206
- bundler (>= 1.0.0)
209
+ bundler (>= 1.8.0)
207
210
  fakefs
208
211
  fastthread!
209
212
  garb (< 0.9.2)
@@ -220,8 +223,9 @@ DEPENDENCIES
220
223
  rake
221
224
  redis (>= 2.1)
222
225
  redis-namespace (>= 1.1.0)
223
- rubystats
226
+ rubystats (>= 0.2.4)
224
227
  sqlite3 (~> 1.3.10)
228
+ test-unit (~> 3.0)
225
229
  timecop
226
230
  vanity!
227
231
  webmock
@@ -7,7 +7,7 @@ gem "redis", ">= 2.1"
7
7
  gem "redis-namespace", ">= 1.1.0"
8
8
  gem "mongo"
9
9
  gem "integration", "<= 0.1.0"
10
- gem "rubystats"
10
+ gem "rubystats", ">= 0.2.4"
11
11
  gem "garb", "< 0.9.2", :require => false
12
12
  gem "timecop", :require => false
13
13
  gem "webmock", :require => false
@@ -7,7 +7,7 @@ GIT
7
7
  PATH
8
8
  remote: .././
9
9
  specs:
10
- vanity (2.1.0)
10
+ vanity (2.1.1)
11
11
  i18n
12
12
 
13
13
  GEM
@@ -161,7 +161,7 @@ GEM
161
161
  redis (3.0.7)
162
162
  redis-namespace (1.4.1)
163
163
  redis (~> 3.0.4)
164
- rubystats (0.2.3)
164
+ rubystats (0.2.4)
165
165
  safe_yaml (1.0.4)
166
166
  sass (3.4.13)
167
167
  sprockets (2.12.3)
@@ -200,7 +200,7 @@ DEPENDENCIES
200
200
  activerecord-jdbc-adapter
201
201
  appraisal (~> 1.0.2)
202
202
  bson_ext
203
- bundler (>= 1.0.0)
203
+ bundler (>= 1.8.0)
204
204
  fakefs
205
205
  fastthread!
206
206
  garb (< 0.9.2)
@@ -216,7 +216,7 @@ DEPENDENCIES
216
216
  rake
217
217
  redis (>= 2.1)
218
218
  redis-namespace (>= 1.1.0)
219
- rubystats
219
+ rubystats (>= 0.2.4)
220
220
  sqlite3 (~> 1.3.10)
221
221
  timecop
222
222
  vanity!
@@ -7,7 +7,7 @@ gem "redis", ">= 2.1"
7
7
  gem "redis-namespace", ">= 1.1.0"
8
8
  gem "mongo"
9
9
  gem "integration", "<= 0.1.0"
10
- gem "rubystats"
10
+ gem "rubystats", ">= 0.2.4"
11
11
  gem "garb", "< 0.9.2", :require => false
12
12
  gem "timecop", :require => false
13
13
  gem "webmock", :require => false
@@ -7,7 +7,7 @@ GIT
7
7
  PATH
8
8
  remote: ..
9
9
  specs:
10
- vanity (2.1.0)
10
+ vanity (2.1.1)
11
11
  i18n
12
12
 
13
13
  GEM
@@ -187,7 +187,7 @@ GEM
187
187
  redis (3.2.0)
188
188
  redis-namespace (1.5.1)
189
189
  redis (~> 3.0, >= 3.0.4)
190
- rubystats (0.2.3)
190
+ rubystats (0.2.4)
191
191
  safe_yaml (1.0.4)
192
192
  sass (3.4.13)
193
193
  sprockets (2.12.3)
@@ -226,7 +226,7 @@ DEPENDENCIES
226
226
  activerecord-jdbc-adapter
227
227
  appraisal (~> 1.0.2)
228
228
  bson_ext
229
- bundler (>= 1.0.0)
229
+ bundler (>= 1.8.0)
230
230
  fakefs
231
231
  fastthread!
232
232
  garb (< 0.9.2)
@@ -242,7 +242,7 @@ DEPENDENCIES
242
242
  rake
243
243
  redis (>= 2.1)
244
244
  redis-namespace (>= 1.1.0)
245
- rubystats
245
+ rubystats (>= 0.2.4)
246
246
  sqlite3 (~> 1.3.10)
247
247
  timecop
248
248
  vanity!
@@ -7,7 +7,7 @@ gem "redis", ">= 2.1"
7
7
  gem "redis-namespace", ">= 1.1.0"
8
8
  gem "mongo"
9
9
  gem "integration", "<= 0.1.0"
10
- gem "rubystats"
10
+ gem "rubystats", ">= 0.2.4"
11
11
  gem "garb", "< 0.9.2", :require => false
12
12
  gem "timecop", :require => false
13
13
  gem "webmock", :require => false
@@ -7,7 +7,7 @@ GIT
7
7
  PATH
8
8
  remote: ../
9
9
  specs:
10
- vanity (2.1.0)
10
+ vanity (2.1.1)
11
11
  i18n
12
12
 
13
13
  GEM
@@ -151,7 +151,7 @@ GEM
151
151
  redis-namespace (1.5.2)
152
152
  redis (~> 3.0, >= 3.0.4)
153
153
  rouge (1.10.1)
154
- rubystats (0.2.3)
154
+ rubystats (0.2.4)
155
155
  safe_yaml (1.0.4)
156
156
  sass (3.4.20)
157
157
  sprockets (3.5.2)
@@ -181,7 +181,7 @@ DEPENDENCIES
181
181
  activerecord-jdbc-adapter
182
182
  appraisal (~> 1.0.2)
183
183
  bson_ext
184
- bundler (>= 1.0.0)
184
+ bundler (>= 1.8.0)
185
185
  fakefs
186
186
  fastthread!
187
187
  garb (< 0.9.2)
@@ -198,7 +198,7 @@ DEPENDENCIES
198
198
  rake
199
199
  redis (>= 2.1)
200
200
  redis-namespace (>= 1.1.0)
201
- rubystats
201
+ rubystats (>= 0.2.4)
202
202
  sqlite3 (~> 1.3.10)
203
203
  timecop
204
204
  vanity!
@@ -58,7 +58,7 @@ module Vanity
58
58
 
59
59
  # Returns true if experiment is enabled, false if disabled.
60
60
  def enabled?
61
- !@playground.collecting? || ( active? && connection.is_experiment_enabled?(@id) )
61
+ !@playground.collecting? || (active? && connection.is_experiment_enabled?(@id))
62
62
  end
63
63
 
64
64
  # Enable or disable the experiment. Only works if the playground is collecting
@@ -187,10 +187,10 @@ module Vanity
187
187
  if @playground.collecting?
188
188
  if active?
189
189
  if enabled?
190
- index = alternative_index_for_identity(request)
190
+ return assignment_for_identity(request)
191
191
  else
192
192
  # Show the default if experiment is disabled.
193
- index = alternatives.index(default)
193
+ return default
194
194
  end
195
195
  else
196
196
  # If inactive, always show the outcome. Fallback to generation if one can't be found.
@@ -233,7 +233,7 @@ module Vanity
233
233
  connection.ab_not_showing @id, identity
234
234
  else
235
235
  index = @alternatives.index(value)
236
- save_assignment_if_valid_visitor(identity, index, request)
236
+ save_assignment(identity, index, request) unless filter_visitor?(request, identity)
237
237
 
238
238
  raise ArgumentError, "No alternative #{value.inspect} for #{name}" unless index
239
239
  if (connection.ab_showing(@id, identity) && connection.ab_showing(@id, identity) != index) ||
@@ -588,14 +588,18 @@ module Vanity
588
588
 
589
589
  # Returns the assigned alternative, previously chosen alternative, or
590
590
  # alternative_for for a given identity.
591
- def alternative_index_for_identity(request)
591
+ def assignment_for_identity(request)
592
592
  identity = identity()
593
- index = connection.ab_showing(@id, identity) || connection.ab_assigned(@id, identity)
594
- unless index
595
- index = alternative_for(identity).to_i
596
- save_assignment_if_valid_visitor(identity, index, request) unless @playground.using_js?
593
+ if filter_visitor?(request, identity)
594
+ default
595
+ else
596
+ index = connection.ab_showing(@id, identity) || connection.ab_assigned(@id, identity)
597
+ unless index
598
+ index = alternative_for(identity).to_i
599
+ save_assignment(identity, index, request) unless @playground.using_js?
600
+ end
601
+ alternatives[index.to_i]
597
602
  end
598
- index
599
603
  end
600
604
 
601
605
  # Chooses an alternative for the identity and returns its index. This
@@ -618,8 +622,8 @@ module Vanity
618
622
  # Saves the assignment of an alternative to a person and performs the
619
623
  # necessary housekeeping. Ignores repeat identities and filters using
620
624
  # Playground#request_filter.
621
- def save_assignment_if_valid_visitor(identity, index, request)
622
- return if index == connection.ab_showing(@id, identity) || filter_visitor?(request)
625
+ def save_assignment(identity, index, request)
626
+ return if index == connection.ab_showing(@id, identity)
623
627
 
624
628
  call_on_assignment_if_available(identity, index)
625
629
  rebalance_if_necessary!
@@ -628,8 +632,9 @@ module Vanity
628
632
  check_completion!
629
633
  end
630
634
 
631
- def filter_visitor?(request)
632
- @playground.request_filter.call(request)
635
+ def filter_visitor?(request, identity)
636
+ @playground.request_filter.call(request) ||
637
+ (@request_filter_block && @request_filter_block.call(request, identity))
633
638
  end
634
639
 
635
640
  def call_on_assignment_if_available(identity, index)
@@ -664,9 +669,9 @@ module Vanity
664
669
  @use_probabilities = []
665
670
  result = []
666
671
  @alternatives = @alternatives.each_with_index.map do |(value, probability), i|
667
- result << alternative = Alternative.new( self, i, value )
672
+ result << alternative = Alternative.new(self, i, value)
668
673
  probability = probability.to_f / sum_of_probability
669
- @use_probabilities << [ alternative, cumulative_probability += probability ]
674
+ @use_probabilities << [alternative, cumulative_probability += probability]
670
675
  value
671
676
  end
672
677
 
@@ -158,6 +158,20 @@ module Vanity
158
158
  return unless @playground.collecting?
159
159
  connection.set_experiment_created_at @id, Time.now
160
160
  end
161
+
162
+ # -- Filtering Particpants --
163
+
164
+ # Define an experiment specific request filter. For example:
165
+ #
166
+ # reject do |request|
167
+ # true if Vanity.context.cookies["somecookie"]
168
+ # end
169
+ #
170
+ def reject(&block)
171
+ fail "Missing block" unless block
172
+ raise "filter already called on this experiment" if @request_filter_block
173
+ @request_filter_block = block
174
+ end
161
175
 
162
176
  protected
163
177
 
@@ -16,7 +16,7 @@ module Vanity
16
16
  # The use_vanity method will setup the controller to allow testing and
17
17
  # tracking of the current user.
18
18
  module UseVanity
19
- # Defines the vanity_identity method and the set_identity_context filter.
19
+ # Defines the vanity_identity method and the vanity_context_filter filter.
20
20
  #
21
21
  # Call with the name of a method that returns an object whose identity
22
22
  # will be used as the Vanity identity if the user is not already
@@ -40,55 +40,16 @@ module Vanity
40
40
  # class ProjectController < ApplicationController
41
41
  # use_vanity { |controller| controller.params[:project_id] }
42
42
  # end
43
- def use_vanity(symbol = nil, &block)
43
+ def use_vanity(method_name = nil, &block)
44
44
  define_method :vanity_store_experiment_for_js do |name, alternative|
45
45
  @_vanity_experiments ||= {}
46
46
  @_vanity_experiments[name] ||= alternative
47
47
  @_vanity_experiments[name].value
48
48
  end
49
49
 
50
- if block
51
- define_method(:vanity_identity) { block.call(self) }
52
- else
53
- define_method :vanity_identity do
54
- return @vanity_identity if @vanity_identity
55
-
56
- cookie = lambda do |value|
57
- result = {
58
- value: value,
59
- expires: Time.now + Vanity.configuration.cookie_expires,
60
- path: Vanity.configuration.cookie_path,
61
- domain: Vanity.configuration.cookie_domain,
62
- secure: Vanity.configuration.cookie_secure,
63
- httponly: Vanity.configuration.cookie_httponly
64
- }
65
- result[:domain] ||= ::Rails.application.config.session_options[:domain]
66
- result
67
- end
50
+ define_method(:vanity_identity_block) { block }
51
+ define_method(:vanity_identity_method) { method_name }
68
52
 
69
- # With user sign in, it's possible to visit not-logged in, get
70
- # cookied and shown alternative A, then sign in and based on
71
- # user.id, get shown alternative B.
72
- # This implementation prefers an initial vanity cookie id over a
73
- # new user.id to avoid the flash of alternative B (FOAB).
74
- if request.get? && params[:_identity]
75
- @vanity_identity = params[:_identity]
76
- cookies[Vanity.configuration.cookie_name] = cookie.call(@vanity_identity)
77
- @vanity_identity
78
- elsif cookies[Vanity.configuration.cookie_name]
79
- @vanity_identity = cookies[Vanity.configuration.cookie_name]
80
- elsif symbol && object = send(symbol)
81
- @vanity_identity = object.id
82
- elsif response # everyday use
83
- @vanity_identity = cookies[Vanity.configuration.cookie_name] || SecureRandom.hex(16)
84
- cookies[Vanity.configuration.cookie_name] = cookie.call(@vanity_identity)
85
- @vanity_identity
86
- else # during functional testing
87
- @vanity_identity = "test"
88
- end
89
- end
90
- end
91
- protected :vanity_identity
92
53
  around_filter :vanity_context_filter
93
54
  before_filter :vanity_reload_filter unless ::Rails.configuration.cache_classes
94
55
  before_filter :vanity_query_parameter_filter
@@ -97,7 +58,65 @@ module Vanity
97
58
  protected :use_vanity
98
59
  end
99
60
 
61
+ module Identity
62
+ def vanity_identity # :nodoc:
63
+ return vanity_identity_block.call(self) if vanity_identity_block
64
+ return @vanity_identity if @vanity_identity
65
+
66
+ # With user sign in, it's possible to visit not-logged in, get
67
+ # cookied and shown alternative A, then sign in and based on
68
+ # user.id, get shown alternative B.
69
+ # This implementation prefers an initial vanity cookie id over a
70
+ # new user.id to avoid the flash of alternative B (FOAB).
71
+ if request.get? && params[:_identity]
72
+ @vanity_identity = params[:_identity]
73
+ cookies[Vanity.configuration.cookie_name] = build_vanity_cookie(@vanity_identity)
74
+ @vanity_identity
75
+ elsif cookies[Vanity.configuration.cookie_name]
76
+ @vanity_identity = cookies[Vanity.configuration.cookie_name]
77
+ elsif identity = vanity_identity_from_method(vanity_identity_method)
78
+ @vanity_identity = identity
79
+ elsif response # everyday use
80
+ @vanity_identity = cookies[Vanity.configuration.cookie_name] || SecureRandom.hex(16)
81
+ cookies[Vanity.configuration.cookie_name] = build_vanity_cookie(@vanity_identity)
82
+ @vanity_identity
83
+ else # during functional testing
84
+ @vanity_identity = "test"
85
+ end
86
+ end
87
+ protected :vanity_identity
88
+
89
+ def vanity_identity_from_method(method_name) # :nodoc:
90
+ return unless method_name
91
+
92
+ object = send(method_name)
93
+ object.try(:id)
94
+ end
95
+ private :vanity_identity_from_method
96
+
97
+ def build_vanity_cookie(identity) # :nodoc:
98
+ result = {
99
+ value: identity,
100
+ expires: Time.now + Vanity.configuration.cookie_expires,
101
+ path: Vanity.configuration.cookie_path,
102
+ domain: Vanity.configuration.cookie_domain,
103
+ secure: Vanity.configuration.cookie_secure,
104
+ httponly: Vanity.configuration.cookie_httponly
105
+ }
106
+ result[:domain] ||= ::Rails.application.config.session_options[:domain]
107
+ result
108
+ end
109
+ private :build_vanity_cookie
110
+ end
111
+
100
112
  module UseVanityMailer
113
+ # Should be called from within the mailer function. For example:
114
+ #
115
+ # def invite_email(user)
116
+ # use_vanity_mailer user
117
+ # mail to: user.email, subject: ab_test(:invite_subject)
118
+ # end
119
+ #
101
120
  def use_vanity_mailer(symbol = nil)
102
121
  # Context is the instance of ActionMailer::Base
103
122
  Vanity.context = self
@@ -373,7 +392,7 @@ module Vanity
373
392
  exp.enabled = false
374
393
  render :file=>Vanity.template("_experiment"), :locals=>{:experiment=>exp}
375
394
  end
376
-
395
+
377
396
  def enable
378
397
  exp = Vanity.playground.experiment(params[:e].to_sym)
379
398
  exp.enabled = true
@@ -412,6 +431,7 @@ ActiveSupport.on_load(:action_controller) do
412
431
  ActionController::Base.class_eval do
413
432
  extend Vanity::Rails::UseVanity
414
433
  include Vanity::Rails::Filters
434
+ include Vanity::Rails::Identity
415
435
  helper Vanity::Rails::Helpers
416
436
  end
417
437
  end
@@ -1,5 +1,5 @@
1
1
  module Vanity
2
- VERSION = "2.1.0"
2
+ VERSION = "2.1.1"
3
3
 
4
4
  module Version
5
5
  version = VERSION.to_s.split(".").map { |i| i.to_i }
@@ -0,0 +1,23 @@
1
+ class UseVanityController < ActionController::Base
2
+ class TestModel
3
+ def test_method
4
+ Vanity.ab_test(:pie_or_cake)
5
+ end
6
+ end
7
+
8
+ attr_accessor :current_user
9
+
10
+ def index
11
+ render :text=>Vanity.ab_test(:pie_or_cake)
12
+ end
13
+
14
+ def js
15
+ Vanity.ab_test(:pie_or_cake)
16
+ render :inline => "<%= vanity_js -%>"
17
+ end
18
+
19
+ def model_js
20
+ TestModel.new.test_method
21
+ render :inline => "<%= vanity_js -%>"
22
+ end
23
+ end
@@ -0,0 +1,19 @@
1
+ class VanityMailer < ActionMailer::Base
2
+ include Vanity::Rails::Helpers
3
+ include ActionView::Helpers::AssetTagHelper
4
+ include ActionView::Helpers::TagHelper
5
+
6
+ def ab_test_subject(user)
7
+ use_vanity_mailer user
8
+
9
+ mail :subject =>ab_test(:pie_or_cake).to_s, :body => ""
10
+ end
11
+
12
+ def ab_test_content(user)
13
+ use_vanity_mailer user
14
+
15
+ mail do |format|
16
+ format.html { render :text=>view_context.vanity_tracking_image(Vanity.context.vanity_identity, :open, :host => "127.0.0.1:3000") }
17
+ end
18
+ end
19
+ end
@@ -1430,6 +1430,24 @@ This experiment did not run long enough to find a clear winner.
1430
1430
  assert [:a, :b, :c].include?(choice)
1431
1431
  assert_equal choice, experiment(:simple).choose.value
1432
1432
  end
1433
+
1434
+ def test_filter_visitor_always_returns_default
1435
+ exp = new_ab_test :simple do
1436
+ alternatives :a, :b, :c
1437
+ default :b
1438
+ metrics :coolness
1439
+
1440
+ reject do |request, identity|
1441
+ true
1442
+ end
1443
+ end
1444
+
1445
+ results = Set.new
1446
+ 100.times do
1447
+ results << exp.choose.value
1448
+ end
1449
+ assert_equal results, [:b].to_set
1450
+ end
1433
1451
 
1434
1452
  # -- Reset --
1435
1453
 
@@ -1,30 +1,5 @@
1
1
  require "test_helper"
2
2
 
3
- # Pages accessible to everyone, e.g. sign in, community search.
4
- class UseVanityController < ActionController::Base
5
- class TestModel
6
- def test_method
7
- Vanity.ab_test(:pie_or_cake)
8
- end
9
- end
10
-
11
- attr_accessor :current_user
12
-
13
- def index
14
- render :text=>Vanity.ab_test(:pie_or_cake)
15
- end
16
-
17
- def js
18
- Vanity.ab_test(:pie_or_cake)
19
- render :inline => "<%= vanity_js -%>"
20
- end
21
-
22
- def model_js
23
- TestModel.new.test_method
24
- render :inline => "<%= vanity_js -%>"
25
- end
26
- end
27
-
28
3
  class UseVanityControllerTest < ActionController::TestCase
29
4
  tests UseVanityController
30
5
 
@@ -120,11 +95,17 @@ class UseVanityControllerTest < ActionController::TestCase
120
95
  end
121
96
 
122
97
  def test_vanity_identity_set_from_user
123
- @controller.current_user = mock("user", :id=>"user_id")
98
+ @controller.current_user = stub("user", :id=>"user_id")
124
99
  get :index
125
100
  assert_equal "user_id", @controller.send(:vanity_identity)
126
101
  end
127
102
 
103
+ def test_vanity_identity_with_null_user_falls_back_to_cookie
104
+ @controller.current_user = stub("user", :id=>nil)
105
+ get :index
106
+ assert cookies["vanity_id"] =~ /^[a-f0-9]{32}$/
107
+ end
108
+
128
109
  def test_vanity_identity_with_no_user_model
129
110
  UseVanityController.class_eval do
130
111
  use_vanity nil
@@ -1,25 +1,5 @@
1
1
  require "test_helper"
2
2
 
3
- class VanityMailer < ActionMailer::Base
4
- include Vanity::Rails::Helpers
5
- include ActionView::Helpers::AssetTagHelper
6
- include ActionView::Helpers::TagHelper
7
-
8
- def ab_test_subject(user)
9
- use_vanity_mailer user
10
-
11
- mail :subject =>ab_test(:pie_or_cake).to_s, :body => ""
12
- end
13
-
14
- def ab_test_content(user)
15
- use_vanity_mailer user
16
-
17
- mail do |format|
18
- format.html { render :text=>view_context.vanity_tracking_image(Vanity.context.vanity_identity, :open, :host => "127.0.0.1:3000") }
19
- end
20
- end
21
- end
22
-
23
3
  class UseVanityMailerTest < ActionMailer::TestCase
24
4
  tests VanityMailer
25
5
 
data/vanity.gemspec CHANGED
@@ -25,6 +25,6 @@ Gem::Specification.new do |spec|
25
25
 
26
26
  spec.add_runtime_dependency "i18n"
27
27
 
28
- spec.add_development_dependency "bundler", ">= 1.0.0"
28
+ spec.add_development_dependency "bundler", ">= 1.8.0"
29
29
  spec.add_development_dependency "minitest", ">= 4.2"
30
30
  end
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.1.0
4
+ version: 2.1.1
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-19 00:00:00.000000000 Z
11
+ date: 2016-02-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: i18n
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - '>='
32
32
  - !ruby/object:Gem::Version
33
- version: 1.0.0
33
+ version: 1.8.0
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - '>='
39
39
  - !ruby/object:Gem::Version
40
- version: 1.0.0
40
+ version: 1.8.0
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: minitest
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -164,7 +164,9 @@ files:
164
164
  - test/data/vanity.yml.redis-erb
165
165
  - test/dummy/Rakefile
166
166
  - test/dummy/app/controllers/application_controller.rb
167
+ - test/dummy/app/controllers/use_vanity_controller.rb
167
168
  - test/dummy/app/helpers/application_helper.rb
169
+ - test/dummy/app/mailers/vanity_mailer.rb
168
170
  - test/dummy/app/views/layouts/application.html.erb
169
171
  - test/dummy/config.ru
170
172
  - test/dummy/config/application.rb
@@ -211,7 +213,7 @@ metadata: {}
211
213
  post_install_message: To get started run vanity --help
212
214
  rdoc_options:
213
215
  - --title
214
- - Vanity 2.1.0
216
+ - Vanity 2.1.1
215
217
  - --main
216
218
  - README.md
217
219
  - --webcvs
@@ -248,7 +250,9 @@ test_files:
248
250
  - test/data/vanity.yml.redis-erb
249
251
  - test/dummy/Rakefile
250
252
  - test/dummy/app/controllers/application_controller.rb
253
+ - test/dummy/app/controllers/use_vanity_controller.rb
251
254
  - test/dummy/app/helpers/application_helper.rb
255
+ - test/dummy/app/mailers/vanity_mailer.rb
252
256
  - test/dummy/app/views/layouts/application.html.erb
253
257
  - test/dummy/config.ru
254
258
  - test/dummy/config/application.rb