vanity 1.5.3 → 1.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +61 -13
- data/Gemfile +5 -5
- data/README.rdoc +10 -1
- data/lib/vanity/adapters/mongodb_adapter.rb +23 -7
- data/lib/vanity/adapters/redis_adapter.rb +8 -2
- data/lib/vanity/experiment/ab_test.rb +55 -30
- data/lib/vanity/experiment/base.rb +4 -2
- data/lib/vanity/frameworks/rails.rb +30 -2
- data/lib/vanity/helpers.rb +8 -1
- data/lib/vanity/playground.rb +35 -2
- data/lib/vanity/templates/_vanity.js.erb +20 -0
- data/lib/vanity/version.rb +1 -1
- data/test/adapters/redis_adapter_test.rb +17 -0
- data/test/experiment/ab_test.rb +83 -11
- data/test/experiment/base_test.rb +14 -0
- data/test/myapp/log/production.log +165 -0
- data/test/playground_test.rb +10 -0
- data/test/rails_dashboard_test.rb +37 -0
- data/test/rails_helper_test.rb +28 -0
- data/test/rails_test.rb +73 -4
- data/test/test_helper.rb +4 -6
- data/vanity.gemspec +1 -1
- metadata +42 -51
- data/test/myapp/app/controllers/application_controller.rbc +0 -66
- data/test/myapp/app/controllers/main_controller.rbc +0 -347
- data/test/myapp/config/boot.rbc +0 -2534
- data/test/myapp/config/environment.rbc +0 -403
- data/test/myapp/config/routes.rbc +0 -174
- data/test/passenger_test.rbc +0 -0
- data/test/playground_test.rbc +0 -256
- data/test/rails_test.rbc +0 -4086
- data/test/test_helper.rbc +0 -4297
data/CHANGELOG
CHANGED
@@ -1,3 +1,51 @@
|
|
1
|
+
== 1.6.0 (2011-07-18)
|
2
|
+
|
3
|
+
If robots or spiders make up a significant portion of your sites traffic they
|
4
|
+
can affect your conversion rate. Vanity can optionally add participants to the
|
5
|
+
experiments using asynchronous JavaScript callbacks, which will keep almost all
|
6
|
+
robots out. To set this up simply do the following:
|
7
|
+
|
8
|
+
1. Vanity.playground.use_js!
|
9
|
+
2. Set Vanity.playground.add_participant_path = '/path/to/vanity/action'
|
10
|
+
3. Add <%= Vanity.vanity_js %> to the bottom of any view that needs to set up
|
11
|
+
an ab_test
|
12
|
+
|
13
|
+
|
14
|
+
Fix for metrics on rails 3 models (Esteban Pastorino).
|
15
|
+
|
16
|
+
Use JavaScript to report participants, useful for ignoring bots on publicly
|
17
|
+
accessible pages (Doug Cole).
|
18
|
+
|
19
|
+
AbTest#choose returns an Alternative rather than just the value (Doug Cole).
|
20
|
+
|
21
|
+
Add warnings instead of swallowing errors (Anthony Eden).
|
22
|
+
|
23
|
+
Fixing broken test for mongodb adapter (Joshua Krall).
|
24
|
+
|
25
|
+
Fix returning correct experiment when in test mode and manually set via
|
26
|
+
#chooses (Ryan Sonnek).
|
27
|
+
|
28
|
+
Don't round the conversion rate before using it, it affects the test results,
|
29
|
+
making them less accurate (Doug Cole).
|
30
|
+
|
31
|
+
Fixed loading config from yml when using other than redis adapter (Arttu Tervo)
|
32
|
+
|
33
|
+
Default to localhost unless host in config file (Arttu Tervo)
|
34
|
+
|
35
|
+
Fixed mongo connection adapter connect! when called after disconnect! (Arttu
|
36
|
+
Tervo)
|
37
|
+
|
38
|
+
Use mongo replica set connection if multiple hosts were given in YAML
|
39
|
+
configuration file (Arttu Tervo)
|
40
|
+
|
41
|
+
Cookie domain from rails configuration (Arttu Tervo)
|
42
|
+
|
43
|
+
Add bson_ext to Gemfile to load C extension for mongodb ruby driver, and
|
44
|
+
prevent Notice messages as the tests run (tenaciousflea)
|
45
|
+
|
46
|
+
Update redis-namespace dependency to 1.0 (Ville Lautanala)
|
47
|
+
|
48
|
+
|
1
49
|
== 1.5.3 (2011-04-11)
|
2
50
|
|
3
51
|
Added number of participants and number of converted participants to ab_test
|
@@ -59,9 +107,9 @@ to multiple adapters (not just Redis). The easiest is to use the configuration
|
|
59
107
|
file config/vanity.yml. For example:
|
60
108
|
|
61
109
|
development:
|
62
|
-
|
63
|
-
|
64
|
-
|
110
|
+
adapter: redis
|
111
|
+
production:
|
112
|
+
adapter: mongodb
|
65
113
|
|
66
114
|
We get to keep Redis, add new MongoDB adapter, but lose Mock. It's still there,
|
67
115
|
but there's a new way to use Vanity outside production: you can turn data
|
@@ -117,7 +165,7 @@ Next, authenticate using your account credentials. For example, in your
|
|
117
165
|
config/environments/production.rb:
|
118
166
|
|
119
167
|
require "garb"
|
120
|
-
|
168
|
+
Garb::Session.login('..email..', '..password..', account_type: "GOOGLE") rescue nil
|
121
169
|
|
122
170
|
Last, define Vanity metrics that tap to Google Analytics metrics. For example:
|
123
171
|
|
@@ -179,22 +227,22 @@ This release introduces metrics. Metrics are the gateway drug to better softwar
|
|
179
227
|
|
180
228
|
It’s as simple as defining a metric:
|
181
229
|
|
182
|
-
|
183
|
-
|
184
|
-
|
230
|
+
metric "Cheers" do
|
231
|
+
description "They love us, don't they?"
|
232
|
+
end
|
185
233
|
|
186
234
|
Tracking it from your code:
|
187
235
|
|
188
|
-
|
236
|
+
track! :cheers
|
189
237
|
|
190
238
|
And watching the graph from the Dashboard.
|
191
239
|
|
192
240
|
You can (should) also use metrics with your A/B tests, for example:
|
193
241
|
|
194
242
|
ab_test "Pricing options" do
|
195
|
-
|
196
|
-
|
197
|
-
|
243
|
+
metrics :signup
|
244
|
+
alternatives 15, 25, 29
|
245
|
+
end
|
198
246
|
|
199
247
|
This new usage may become requirement in a future release.
|
200
248
|
|
@@ -213,8 +261,8 @@ Much thanks to Ian Sefferman for fixing issues with Ruby 1.8.7 and Rails support
|
|
213
261
|
This release changes the way you define a new experiment. You can use a method suitable for the type of experiment you want to define, or call the generic define method (previously: experiment method). For example:
|
214
262
|
|
215
263
|
ab_test "My A/B test" do
|
216
|
-
|
217
|
-
|
264
|
+
alternatives :a, :b
|
265
|
+
end
|
218
266
|
|
219
267
|
The experiment method is no longer overloaded: it looks up an experiment (loading its definition if necessary), but does not define an experiment. The ab_test method is overloaded, though this may change in the future.
|
220
268
|
|
data/Gemfile
CHANGED
@@ -13,12 +13,12 @@ group :test do
|
|
13
13
|
gem "garb"
|
14
14
|
gem "mocha"
|
15
15
|
gem "mongo"
|
16
|
-
gem "
|
17
|
-
gem "
|
18
|
-
gem "
|
16
|
+
gem "bson_ext"
|
17
|
+
gem "mysql"
|
18
|
+
gem "passenger", "~>2.0"
|
19
|
+
gem "rails", "~>2.3.8"
|
20
|
+
gem "rack"
|
19
21
|
gem "shoulda"
|
20
|
-
gem "sqlite3-ruby", "1.2.5" # 1.3.0 doesn't like Ruby 1.9.1
|
21
22
|
gem "timecop"
|
22
|
-
#gem "SystemTimer"
|
23
23
|
gem "webmock"
|
24
24
|
end
|
data/README.rdoc
CHANGED
@@ -62,7 +62,8 @@ And:
|
|
62
62
|
|
63
63
|
vanity report --output vanity.html
|
64
64
|
|
65
|
-
|
65
|
+
|
66
|
+
== Rails 3
|
66
67
|
|
67
68
|
There is currently an issue with report generation. The vanity-talk Google Group has a couple posts that outline the issue for now. This is one of the posts: http://groups.google.com/group/vanity-talk/browse_thread/thread/343081a72a0cefb6
|
68
69
|
|
@@ -73,6 +74,14 @@ And:
|
|
73
74
|
`match '/vanity(/:action(/:id(.:format)))', :controller=>:vanity`
|
74
75
|
|
75
76
|
|
77
|
+
== Registering participants with Javascript
|
78
|
+
|
79
|
+
If robots or spiders make up a significant portion of your sites traffic they can affect your conversion rate. Vanity can optionally add participants to the experiments using asynchronous javascript callbacks, which will keep almost all robots out. To set this up simply do the following:
|
80
|
+
|
81
|
+
* Add Vanity.playground.use_js!
|
82
|
+
* Set Vanity.playground.add_participant_path = '/path/to/vanity/action', this should point to the add_participant path that is added with Vanity::Rails::Dashboard, make sure that this action is available to all users
|
83
|
+
* Add <%= vanity_js %> to any page that needs uses an ab_test. vanity_js needs to be included after your call to ab_test so that it knows which version of the experiment the participant is a member of. The helper will render nothing if the there are no ab_tests running on the current page, so adding vanity_js to the bottom of your layouts is a good option. Keep in mind that if you call use_js! and don't include vanity_js in your view no participants will be recorded.
|
84
|
+
|
76
85
|
== Contributing
|
77
86
|
|
78
87
|
* Fork the project
|
@@ -15,13 +15,25 @@ module Vanity
|
|
15
15
|
#
|
16
16
|
# @since 1.4.0
|
17
17
|
class MongodbAdapter < AbstractAdapter
|
18
|
+
attr_reader :mongo
|
19
|
+
|
18
20
|
def initialize(options)
|
19
|
-
|
21
|
+
setup_connection(options)
|
20
22
|
@options = options.clone
|
21
23
|
@options[:database] ||= (@options[:path] && @options[:path].split("/")[1]) || "vanity"
|
22
24
|
connect!
|
23
25
|
end
|
24
26
|
|
27
|
+
def setup_connection(options = {})
|
28
|
+
if options[:hosts]
|
29
|
+
args = (options[:hosts].map{|host| [host, options[:port]] } << {:connect => false})
|
30
|
+
@mongo = Mongo::ReplSetConnection.new(*args)
|
31
|
+
else
|
32
|
+
@mongo = Mongo::Connection.new(options[:host], options[:port], :connect => false)
|
33
|
+
end
|
34
|
+
@mongo
|
35
|
+
end
|
36
|
+
|
25
37
|
def active?
|
26
38
|
@mongo.connected?
|
27
39
|
end
|
@@ -38,6 +50,7 @@ module Vanity
|
|
38
50
|
end
|
39
51
|
|
40
52
|
def connect!
|
53
|
+
@mongo ||= setup_connection(@options)
|
41
54
|
@mongo.connect
|
42
55
|
database = @mongo.db(@options[:database])
|
43
56
|
database.authenticate @options[:username], @options[:password], true if @options[:username]
|
@@ -45,11 +58,14 @@ module Vanity
|
|
45
58
|
@experiments = database.collection("vanity.experiments")
|
46
59
|
@participants = database.collection("vanity.participants")
|
47
60
|
@participants.create_index [[:experiment, 1], [:identity, 1]], :unique=>true
|
61
|
+
@participants.create_index [[:experiment, 1], [:seen, 1]]
|
62
|
+
@participants.create_index [[:experiment, 1], [:converted, 1]]
|
63
|
+
@mongo
|
48
64
|
end
|
49
65
|
|
50
66
|
def to_s
|
51
67
|
userinfo = @options.values_at(:username, :password).join(":") if @options[:username]
|
52
|
-
URI::Generic.build(:scheme=>"mongodb", :userinfo=>userinfo, :host
|
68
|
+
URI::Generic.build(:scheme=>"mongodb", :userinfo=>userinfo, :host=>(@mongo.host || @options[:host]), :port=>(@mongo.port || @options[:port]), :path=>"/#{@options[:database]}").to_s
|
53
69
|
end
|
54
70
|
|
55
71
|
def flushdb
|
@@ -112,8 +128,8 @@ module Vanity
|
|
112
128
|
def ab_counts(experiment, alternative)
|
113
129
|
record = @experiments.find_one({ :_id=>experiment }, { :fields=>[:conversions] })
|
114
130
|
conversions = record && record["conversions"]
|
115
|
-
{ :participants => @participants.find({ :experiment=>experiment, :seen=>alternative }
|
116
|
-
:converted => @participants.find({ :experiment=>experiment, :converted=>alternative }
|
131
|
+
{ :participants => @participants.find({ :experiment=>experiment, :seen=>alternative }).count,
|
132
|
+
:converted => @participants.find({ :experiment=>experiment, :converted=>alternative }).count,
|
117
133
|
:conversions => conversions && conversions[alternative.to_s] || 0 }
|
118
134
|
end
|
119
135
|
|
@@ -131,16 +147,16 @@ module Vanity
|
|
131
147
|
end
|
132
148
|
|
133
149
|
def ab_add_participant(experiment, alternative, identity)
|
134
|
-
@participants.update({ :experiment=>experiment, :identity=>identity }, { "$
|
150
|
+
@participants.update({ :experiment=>experiment, :identity=>identity }, { "$push"=>{ :seen=>alternative } }, :upsert=>true)
|
135
151
|
end
|
136
152
|
|
137
153
|
def ab_add_conversion(experiment, alternative, identity, count = 1, implicit = false)
|
138
154
|
if implicit
|
139
|
-
@participants.update({ :experiment=>experiment, :identity=>identity }, { "$
|
155
|
+
@participants.update({ :experiment=>experiment, :identity=>identity }, { "$push"=>{ :seen=>alternative } }, :upsert=>true)
|
140
156
|
else
|
141
157
|
participating = @participants.find_one(:experiment=>experiment, :identity=>identity, :seen=>alternative)
|
142
158
|
end
|
143
|
-
@participants.update({ :experiment=>experiment, :identity=>identity }, { "$
|
159
|
+
@participants.update({ :experiment=>experiment, :identity=>identity }, { "$push"=>{ :converted=>alternative } }, :upsert=>true) if implicit || participating
|
144
160
|
@experiments.update({ :_id=>experiment }, { "$inc"=>{ "conversions.#{alternative}"=>count } }, :upsert=>true)
|
145
161
|
end
|
146
162
|
|
@@ -26,7 +26,13 @@ module Vanity
|
|
26
26
|
end
|
27
27
|
|
28
28
|
def disconnect!
|
29
|
-
|
29
|
+
if redis
|
30
|
+
begin
|
31
|
+
redis.quit
|
32
|
+
rescue Exception => e
|
33
|
+
warn("Error while disconnecting from redis: #{e.message}")
|
34
|
+
end
|
35
|
+
end
|
30
36
|
@redis = nil
|
31
37
|
end
|
32
38
|
|
@@ -42,7 +48,7 @@ module Vanity
|
|
42
48
|
end
|
43
49
|
|
44
50
|
def to_s
|
45
|
-
|
51
|
+
redis.id
|
46
52
|
end
|
47
53
|
|
48
54
|
def redis
|
@@ -6,12 +6,11 @@ module Vanity
|
|
6
6
|
# One of several alternatives in an A/B test (see AbTest#alternatives).
|
7
7
|
class Alternative
|
8
8
|
|
9
|
-
def initialize(experiment, id, value
|
9
|
+
def initialize(experiment, id, value) #, participants, converted, conversions)
|
10
10
|
@experiment = experiment
|
11
11
|
@id = id
|
12
12
|
@name = "option #{(@id + 65).chr}"
|
13
13
|
@value = value
|
14
|
-
@participants, @converted, @conversions = participants, converted, conversions
|
15
14
|
end
|
16
15
|
|
17
16
|
# Alternative id, only unique for this experiment.
|
@@ -27,13 +26,22 @@ module Vanity
|
|
27
26
|
attr_reader :experiment
|
28
27
|
|
29
28
|
# Number of participants who viewed this alternative.
|
30
|
-
|
29
|
+
def participants
|
30
|
+
load_counts unless @participants
|
31
|
+
@participants
|
32
|
+
end
|
31
33
|
|
32
34
|
# Number of participants who converted on this alternative (a participant is counted only once).
|
33
|
-
|
35
|
+
def converted
|
36
|
+
load_counts unless @converted
|
37
|
+
@converted
|
38
|
+
end
|
34
39
|
|
35
40
|
# Number of conversions for this alternative (same participant may be counted more than once).
|
36
|
-
|
41
|
+
def conversions
|
42
|
+
load_counts unless @conversions
|
43
|
+
@conversions
|
44
|
+
end
|
37
45
|
|
38
46
|
# Z-score for this alternative, related to 2nd-best performing alternative. Populated by AbTest#score.
|
39
47
|
attr_accessor :z_score
|
@@ -44,9 +52,9 @@ module Vanity
|
|
44
52
|
# Difference from least performing alternative. Populated by AbTest#score.
|
45
53
|
attr_accessor :difference
|
46
54
|
|
47
|
-
# Conversion rate calculated as converted/participants
|
55
|
+
# Conversion rate calculated as converted/participants
|
48
56
|
def conversion_rate
|
49
|
-
@conversion_rate ||= (participants > 0 ?
|
57
|
+
@conversion_rate ||= (participants > 0 ? converted.to_f/participants.to_f : 0.0)
|
50
58
|
end
|
51
59
|
|
52
60
|
# The measure we use to order (sort) alternatives and decide which one is better (by calculating z-score).
|
@@ -71,25 +79,32 @@ module Vanity
|
|
71
79
|
"#{name}: #{value} #{converted}/#{participants}"
|
72
80
|
end
|
73
81
|
|
82
|
+
def load_counts
|
83
|
+
if @experiment.playground.collecting?
|
84
|
+
@participants, @converted, @conversions = @experiment.playground.connection.ab_counts(@experiment.id, id).values_at(:participants, :converted, :conversions)
|
85
|
+
else
|
86
|
+
@participants = @converted = @conversions = 0
|
87
|
+
end
|
88
|
+
end
|
74
89
|
end
|
75
90
|
|
76
91
|
|
77
|
-
|
78
|
-
|
79
|
-
|
92
|
+
# The meat.
|
93
|
+
class AbTest < Base
|
94
|
+
class << self
|
80
95
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
96
|
+
# Convert z-score to probability.
|
97
|
+
def probability(score)
|
98
|
+
score = score.abs
|
99
|
+
probability = AbTest::Z_TO_PROBABILITY.find { |z,p| score >= z }
|
100
|
+
probability ? probability.last : 0
|
101
|
+
end
|
87
102
|
|
88
|
-
|
89
|
-
|
90
|
-
|
103
|
+
def friendly_name
|
104
|
+
"A/B Test"
|
105
|
+
end
|
91
106
|
|
92
|
-
|
107
|
+
end
|
93
108
|
|
94
109
|
def initialize(*args)
|
95
110
|
super
|
@@ -139,8 +154,7 @@ module Vanity
|
|
139
154
|
def _alternatives
|
140
155
|
alts = []
|
141
156
|
@alternatives.each_with_index do |value, i|
|
142
|
-
|
143
|
-
alts << Alternative.new(self, i, value, counts[:participants], counts[:converted], counts[:conversions])
|
157
|
+
alts << Alternative.new(self, i, value)
|
144
158
|
end
|
145
159
|
alts
|
146
160
|
end
|
@@ -187,8 +201,10 @@ module Vanity
|
|
187
201
|
index = connection.ab_showing(@id, identity)
|
188
202
|
unless index
|
189
203
|
index = alternative_for(identity)
|
190
|
-
|
191
|
-
|
204
|
+
if !@playground.using_js?
|
205
|
+
connection.ab_add_participant @id, index, identity
|
206
|
+
check_completion!
|
207
|
+
end
|
192
208
|
end
|
193
209
|
else
|
194
210
|
index = connection.ab_get_outcome(@id) || alternative_for(identity)
|
@@ -197,8 +213,9 @@ module Vanity
|
|
197
213
|
identity = identity()
|
198
214
|
@showing ||= {}
|
199
215
|
@showing[identity] ||= alternative_for(identity)
|
216
|
+
index = @showing[identity]
|
200
217
|
end
|
201
|
-
|
218
|
+
alternatives[index.to_i]
|
202
219
|
end
|
203
220
|
|
204
221
|
# Returns fingerprint (hash) for given alternative. Can be used to lookup
|
@@ -233,8 +250,16 @@ module Vanity
|
|
233
250
|
connection.ab_not_showing @id, identity
|
234
251
|
else
|
235
252
|
index = @alternatives.index(value)
|
253
|
+
#add them to the experiment unless they are already in it
|
254
|
+
unless index == connection.ab_showing(@id, identity)
|
255
|
+
connection.ab_add_participant @id, index, identity
|
256
|
+
check_completion!
|
257
|
+
end
|
236
258
|
raise ArgumentError, "No alternative #{value.inspect} for #{name}" unless index
|
237
|
-
connection.
|
259
|
+
if (connection.ab_showing(@id, identity) && connection.ab_showing(@id, identity) != index) ||
|
260
|
+
alternative_for(identity) != index
|
261
|
+
connection.ab_show @id, identity, index
|
262
|
+
end
|
238
263
|
end
|
239
264
|
else
|
240
265
|
@showing ||= {}
|
@@ -247,7 +272,7 @@ module Vanity
|
|
247
272
|
def showing?(alternative)
|
248
273
|
identity = identity()
|
249
274
|
if @playground.collecting?
|
250
|
-
connection.ab_showing(@id, identity) == alternative.id
|
275
|
+
(connection.ab_showing(@id, identity) || alternative_for(identity)) == alternative.id
|
251
276
|
else
|
252
277
|
@showing ||= {}
|
253
278
|
@showing[identity] == alternative.id
|
@@ -379,9 +404,9 @@ module Vanity
|
|
379
404
|
if @outcome_is
|
380
405
|
begin
|
381
406
|
result = @outcome_is.call
|
382
|
-
outcome = result.id if result && result.experiment == self
|
383
|
-
rescue
|
384
|
-
#
|
407
|
+
outcome = result.id if Alternative === result && result.experiment == self
|
408
|
+
rescue
|
409
|
+
warn "Error in AbTest#complete!: #{$!}"
|
385
410
|
end
|
386
411
|
else
|
387
412
|
best = score.best
|
@@ -80,6 +80,8 @@ module Vanity
|
|
80
80
|
# Button" becomes :green_button.
|
81
81
|
attr_reader :id
|
82
82
|
|
83
|
+
attr_reader :playground
|
84
|
+
|
83
85
|
# Time stamp when experiment was created.
|
84
86
|
def created_at
|
85
87
|
@created_at ||= connection.get_experiment_created_at(@id)
|
@@ -124,7 +126,7 @@ module Vanity
|
|
124
126
|
@description = text if text
|
125
127
|
@description
|
126
128
|
end
|
127
|
-
|
129
|
+
|
128
130
|
|
129
131
|
# -- Experiment completion --
|
130
132
|
|
@@ -189,7 +191,7 @@ module Vanity
|
|
189
191
|
begin
|
190
192
|
complete! if @complete_block.call
|
191
193
|
rescue
|
192
|
-
|
194
|
+
warn "Error in Vanity::Experiment::Base: #{$!}"
|
193
195
|
end
|
194
196
|
end
|
195
197
|
end
|
@@ -46,7 +46,11 @@ module Vanity
|
|
46
46
|
@vanity_identity = object.id
|
47
47
|
elsif response # everyday use
|
48
48
|
@vanity_identity = cookies["vanity_id"] || ActiveSupport::SecureRandom.hex(16)
|
49
|
-
|
49
|
+
cookie = { :value=>@vanity_identity, :expires=>1.month.from_now }
|
50
|
+
# Useful if application and admin console are on separate domains.
|
51
|
+
# This only works in Rails 3.x.
|
52
|
+
cookie_options[:domain] ||= ::Rails.application.config.session_options[:domain] if ::Rails.respond_to?(:application)
|
53
|
+
cookies["vanity_id"] = cookie
|
50
54
|
@vanity_identity
|
51
55
|
else # during functional testing
|
52
56
|
@vanity_identity = "test"
|
@@ -140,7 +144,14 @@ module Vanity
|
|
140
144
|
# <%= count %> features to choose from!
|
141
145
|
# <% end %>
|
142
146
|
def ab_test(name, &block)
|
143
|
-
|
147
|
+
if Vanity.playground.using_js?
|
148
|
+
@_vanity_experiments ||= {}
|
149
|
+
@_vanity_experiments[name] ||= Vanity.playground.experiment(name).choose
|
150
|
+
value = @_vanity_experiments[name].value
|
151
|
+
else
|
152
|
+
value = Vanity.playground.experiment(name).choose.value
|
153
|
+
end
|
154
|
+
|
144
155
|
if block
|
145
156
|
content = capture(value, &block)
|
146
157
|
block_called_from_erb?(block) ? concat(content) : content
|
@@ -149,6 +160,13 @@ module Vanity
|
|
149
160
|
end
|
150
161
|
end
|
151
162
|
|
163
|
+
def vanity_js
|
164
|
+
return if @_vanity_experiments.nil?
|
165
|
+
javascript_tag do
|
166
|
+
render Vanity.template("vanity.js.erb")
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
152
170
|
def vanity_h(text)
|
153
171
|
h(text)
|
154
172
|
end
|
@@ -186,6 +204,16 @@ module Vanity
|
|
186
204
|
exp.chooses(exp.alternatives[params[:a].to_i].value)
|
187
205
|
render :file=>Vanity.template("_experiment"), :locals=>{:experiment=>exp}
|
188
206
|
end
|
207
|
+
|
208
|
+
def add_participant
|
209
|
+
if params[:e].nil? || params[:e].empty?
|
210
|
+
render :status => 404, :nothing => true
|
211
|
+
return
|
212
|
+
end
|
213
|
+
exp = Vanity.playground.experiment(params[:e])
|
214
|
+
exp.chooses(exp.alternatives[params[:a].to_i].value)
|
215
|
+
render :status => 200, :nothing => true
|
216
|
+
end
|
189
217
|
end
|
190
218
|
end
|
191
219
|
end
|
data/lib/vanity/helpers.rb
CHANGED
@@ -34,7 +34,14 @@ module Vanity
|
|
34
34
|
# end
|
35
35
|
# @since 1.2.0
|
36
36
|
def ab_test(name, &block)
|
37
|
-
|
37
|
+
if Vanity.playground.using_js?
|
38
|
+
@_vanity_experiments ||= {}
|
39
|
+
@_vanity_experiments[name] ||= Vanity.playground.experiment(name).choose
|
40
|
+
value = @_vanity_experiments[name].value
|
41
|
+
else
|
42
|
+
value = Vanity.playground.experiment(name).choose.value
|
43
|
+
end
|
44
|
+
|
38
45
|
if block
|
39
46
|
content = capture(value, &block)
|
40
47
|
block_called_from_erb?(block) ? concat(content) : content
|
data/lib/vanity/playground.rb
CHANGED
@@ -10,6 +10,7 @@ module Vanity
|
|
10
10
|
class Playground
|
11
11
|
|
12
12
|
DEFAULTS = { :collecting => true, :load_path=>"experiments" }
|
13
|
+
DEFAULT_ADD_PARTICIPANT_PATH = '/vanity/add_participant'
|
13
14
|
|
14
15
|
# Created new Playground. Unless you need to, use the global
|
15
16
|
# Vanity.playground.
|
@@ -38,7 +39,7 @@ module Vanity
|
|
38
39
|
end
|
39
40
|
|
40
41
|
@options = defaults.merge(config).merge(options)
|
41
|
-
if @options.values_at(:host, :port, :db).any?
|
42
|
+
if @options[:host] == 'redis' && @options.values_at(:host, :port, :db).any?
|
42
43
|
warn "Deprecated: please specify Redis connection as URL (\"redis://host:port/db\")"
|
43
44
|
establish_connection :adapter=>"redis", :host=>@options[:host], :port=>@options[:port], :database=>@options[:db] || @options[:database]
|
44
45
|
elsif @options[:redis]
|
@@ -58,6 +59,8 @@ module Vanity
|
|
58
59
|
@logger.level = Logger::ERROR
|
59
60
|
end
|
60
61
|
@loading = []
|
62
|
+
@use_js = false
|
63
|
+
self.add_participant_path = DEFAULT_ADD_PARTICIPANT_PATH
|
61
64
|
@collecting = !!@options[:collecting]
|
62
65
|
end
|
63
66
|
|
@@ -70,6 +73,9 @@ module Vanity
|
|
70
73
|
# Logger.
|
71
74
|
attr_accessor :logger
|
72
75
|
|
76
|
+
# Path to the add_participant action, necessary if you have called use_js!
|
77
|
+
attr_accessor :add_participant_path
|
78
|
+
|
73
79
|
# Defines a new experiment. Generally, do not call this directly,
|
74
80
|
# use one of the definition methods (ab_test, measure, etc).
|
75
81
|
#
|
@@ -95,6 +101,34 @@ module Vanity
|
|
95
101
|
experiments[id.to_sym] or raise NameError, "No experiment #{id}"
|
96
102
|
end
|
97
103
|
|
104
|
+
|
105
|
+
# -- Robot Detection --
|
106
|
+
|
107
|
+
# Call to indicate that participants should be added via js
|
108
|
+
# This helps keep robots from participating in the ab test
|
109
|
+
# and skewing results.
|
110
|
+
#
|
111
|
+
# If you use this, there are two more steps:
|
112
|
+
# - Set Vanity.playground.add_participant_path = '/path/to/vanity/action',
|
113
|
+
# this should point to the add_participant path that is added with
|
114
|
+
# Vanity::Rails::Dashboard, make sure that this action is available
|
115
|
+
# to all users
|
116
|
+
# - Add <%= vanity_js %> to any page that needs uses an ab_test. vanity_js
|
117
|
+
# needs to be included after your call to ab_test so that it knows which
|
118
|
+
# version of the experiment the participant is a member of. The helper
|
119
|
+
# will render nothing if the there are no ab_tests running on the current
|
120
|
+
# page, so adding vanity_js to the bottom of your layouts is a good
|
121
|
+
# option. Keep in mind that if you call use_js! and don't include
|
122
|
+
# vanity_js in your view no participants will be recorded.
|
123
|
+
def use_js!
|
124
|
+
@use_js = true
|
125
|
+
end
|
126
|
+
|
127
|
+
def using_js?
|
128
|
+
@use_js
|
129
|
+
end
|
130
|
+
|
131
|
+
|
98
132
|
# Returns hash of experiments (key is experiment id).
|
99
133
|
#
|
100
134
|
# @see Vanity::Experiment
|
@@ -329,7 +363,6 @@ module Vanity
|
|
329
363
|
path << ".erb" unless name["."]
|
330
364
|
path
|
331
365
|
end
|
332
|
-
|
333
366
|
end
|
334
367
|
end
|
335
368
|
|
@@ -0,0 +1,20 @@
|
|
1
|
+
<% unless @_vanity_experiments.empty? %>
|
2
|
+
var httpRequest;
|
3
|
+
<%
|
4
|
+
@_vanity_experiments.each do |name, alternative|
|
5
|
+
%>
|
6
|
+
var params = "e=<%= name %>&a=<%= alternative.id %>&authenticity_token=" + encodeURIComponent("<%= form_authenticity_token %>");
|
7
|
+
if (window.XMLHttpRequest) { // Mozilla, Safari, ...
|
8
|
+
httpRequest = new XMLHttpRequest();
|
9
|
+
} else if (window.ActiveXObject) { // IE
|
10
|
+
try { httpRequest = new ActiveXObject("Msxml2.XMLHTTP"); }
|
11
|
+
catch (e) { }
|
12
|
+
}
|
13
|
+
if (httpRequest) {
|
14
|
+
httpRequest.open('POST', "<%= Vanity.playground.add_participant_path %>", true);
|
15
|
+
httpRequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
|
16
|
+
httpRequest.setRequestHeader("X-Requested-With", "XMLHttpRequest");
|
17
|
+
httpRequest.send(params);
|
18
|
+
}
|
19
|
+
<% end %>
|
20
|
+
<% end %>
|
data/lib/vanity/version.rb
CHANGED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'test/test_helper'
|
2
|
+
|
3
|
+
class RedisAdapterTest < Test::Unit::TestCase
|
4
|
+
def test_warn_on_disconnect_error
|
5
|
+
if defined?(Redis)
|
6
|
+
assert_nothing_raised do
|
7
|
+
Redis.any_instance.stubs(:connect!)
|
8
|
+
mocked_redis = stub("Redis")
|
9
|
+
mocked_redis.expects(:quit).raises(RuntimeError)
|
10
|
+
redis_adapter = Vanity::Adapters::RedisAdapter.new({})
|
11
|
+
redis_adapter.expects(:warn).with("Error while disconnecting from redis: RuntimeError")
|
12
|
+
redis_adapter.stubs(:redis).returns(mocked_redis)
|
13
|
+
redis_adapter.disconnect!
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|