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 +4 -4
- data/.travis.yml +2 -1
- data/Appraisals +1 -0
- data/CHANGELOG +6 -0
- data/Gemfile +1 -1
- data/Gemfile.lock +4 -4
- data/MIT-LICENSE +1 -1
- data/README.md +15 -2
- data/doc/ab_testing.textile +29 -2
- data/doc/identity.textile +23 -7
- data/gemfiles/rails32.gemfile +2 -1
- data/gemfiles/rails32.gemfile.lock +8 -4
- data/gemfiles/rails41.gemfile +1 -1
- data/gemfiles/rails41.gemfile.lock +4 -4
- data/gemfiles/rails42.gemfile +1 -1
- data/gemfiles/rails42.gemfile.lock +4 -4
- data/gemfiles/rails42_protected_attributes.gemfile +1 -1
- data/gemfiles/rails42_protected_attributes.gemfile.lock +4 -4
- data/lib/vanity/experiment/ab_test.rb +21 -16
- data/lib/vanity/experiment/base.rb +14 -0
- data/lib/vanity/frameworks/rails.rb +64 -44
- data/lib/vanity/version.rb +1 -1
- data/test/dummy/app/controllers/use_vanity_controller.rb +23 -0
- data/test/dummy/app/mailers/vanity_mailer.rb +19 -0
- data/test/experiment/ab_test.rb +18 -0
- data/test/frameworks/rails/action_controller_test.rb +7 -26
- data/test/frameworks/rails/action_mailer_test.rb +0 -20
- data/vanity.gemspec +1 -1
- metadata +9 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c9f59504780981f9e1f351de1afd9774374ca1d4
|
4
|
+
data.tar.gz: 2a211bbebf1c899a256bbfa81f86a161e91155ed
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
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
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
vanity (2.1.
|
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.
|
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.
|
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
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
|
|
data/doc/ab_testing.textile
CHANGED
@@ -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
|
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
|
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@.
|
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?
|
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
|
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
|
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
|
-
|
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
|
|
data/gemfiles/rails32.gemfile
CHANGED
@@ -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.
|
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.
|
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.
|
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
|
data/gemfiles/rails41.gemfile
CHANGED
@@ -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.
|
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.
|
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.
|
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!
|
data/gemfiles/rails42.gemfile
CHANGED
@@ -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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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? || (
|
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
|
-
|
190
|
+
return assignment_for_identity(request)
|
191
191
|
else
|
192
192
|
# Show the default if experiment is disabled.
|
193
|
-
|
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
|
-
|
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
|
591
|
+
def assignment_for_identity(request)
|
592
592
|
identity = identity()
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
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
|
622
|
-
return if index == connection.ab_showing(@id, identity)
|
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(
|
672
|
+
result << alternative = Alternative.new(self, i, value)
|
668
673
|
probability = probability.to_f / sum_of_probability
|
669
|
-
@use_probabilities << [
|
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
|
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(
|
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
|
-
|
51
|
-
|
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
|
data/lib/vanity/version.rb
CHANGED
@@ -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
|
data/test/experiment/ab_test.rb
CHANGED
@@ -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 =
|
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
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.
|
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-
|
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.
|
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.
|
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.
|
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
|