Permalink
Browse files

A/B testing in ActionMailer

  • Loading branch information...
1 parent 02c6277 commit e86140204e85888afdf375c7deb156c646b87ffe @josephsofaer josephsofaer committed Jul 27, 2011
View
@@ -0,0 +1,145 @@
+---
+layout: page
+title: Vanity & ActionMailer
+---
+
+<div id="toc">
+# "Configuring ActionMailer":#config
+# "Testing email subject lines":#subject
+# "Testing email content":#content
+</div>
+
+h3(#config). Configuring ActionMailer
+
+First setup Rails to send email. For example, if you are using gmail you can setup your SMTP settings like this:
+
+in RAILS_ROOT/config/environment.rb
+
+<pre>
+ActionMailer::Base.smtp_settings = {
+ :address => "smtp.gmail.com",
+ :port => "587",
+ :domain => "gmail.com",
+ :authentication => :plain,
+ :user_name => "your-email@gmail.com",
+ :password => "your-pass"
+}
+</pre>
+
+
+h3(#subject). Testing email subject lines
+
+In your RAILS_ROOT/experiments/ folder create an experiment file.
+
+e.g. invite_subject.rb:
+
+<pre>
+ab_test "Invite subject" do
+ description "Optimize invite subject line"
+ alternatives "Join now!", "You're invited to an exclusive event."
+ metrics :open
+end
+</pre>
+
+In your RAILS_ROOT/experiments/metrics/ folder create a metric file for the metric you are testing.
+
+e.g. open.rb
+
+<pre>
+metric "Open (Activation)" do
+ description "Measures how many recipients opened an email."
+end
+</pre>
+
+Create an ActionMailer class
+
+e.g. invite_mailer.rb
+
+<pre>
+class VanityMailer < ActionMailer::Base
+ def invite_email(user)
+ use_vanity_mailer(user)
+ mail(:to => user.email,
+ :subject => (ab_test :invite_subject))
+ end
+end
+</pre>
+
+We set the identity of the "user" in the use_vanity_mailer method. This can take a string or an object that responds to id. If it's nil then it will set it as a random number. Setting the appropriate context is important to have each user consistently get the same alternative in our experiment.
+
+
+Now we need to include a tracking image in the email content.
+We pass in the vanity identity which we set when we called use_vanity_mailer(user) and the metric we are tracking.
+<pre>
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
+ </head>
+ <body>
+ <h1>Hey Joseph</h1>
+ <p>
+ <%= vanity_tracking_image(Vanity.context.vanity_identity, :open, :host => "127.0.0.1:3000") %>
+ </p>
+ </body>
+ </html>
+</pre>
+
+
+Then we have to include the TrackingImage module into our VanityController.
+This is the same place that you can include the Dashboard.
+Vanity::Rails::TrackingImage will add a image method that will render a blank img.
+<pre>
+class VanityController < ApplicationController
+ include Vanity::Rails::Dashboard
+ include Vanity::Rails::TrackingImage
+end
+</pre>
+
+
+h3(#subject). Testing email content
+
+In your RAILS_ROOT/experiments/ folder create a new experiment file.
+
+e.g. invite_text.rb:
+
+<pre>
+ab_test "Invite text" do
+ description "Optimize invite text"
+ alternatives "A friend of yours invited you to use Vanity", "Vanity is the latest and greatest in a/b testing technology"
+ metrics :click
+end
+</pre>
+
+In your RAILS_ROOT/experiments/metrics/ folder create a metric file for the metric you are testing.
+
+e.g. click.rb
+
+<pre>
+metric "Click (Acquisition)" do
+ description "Measures clickthough on email."
+end
+</pre>
+
+A/B test your email content
+
+e.g. invite_email.html.erb
+
+<pre>
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
+ </head>
+ <body>
+ <h1>Hi!</h1>
+ <p>
+ <%= link_to ab_test(:invite_text), vanity_track_url_for(Vanity.context.vanity_identity, :click, :controller => "home", :action => "index", :host => "127.0.0.1:3000") %>
+ </p>
+ </body>
+ </html>
+</pre>
+
+Here we use the text from the "invite_text" experiment and then use the vanity_track_url_for helper to add the identity and the metric to track into the url so that Vanity can track the clickthroughs.
+
+Remember: By default, Vanity only collects data in production mode.
@@ -44,6 +44,10 @@ def use_vanity(symbol = nil, &block)
return @vanity_identity if @vanity_identity
if symbol && object = send(symbol)
@vanity_identity = object.id
+ elsif request.get? && params[:_identity]
+ @vanity_identity = params[:_identity]
+ cookies["vanity_id"] = { :value=>@vanity_identity, :expires=>1.month.from_now }
+ @vanity_identity
elsif response # everyday use
@vanity_identity = cookies["vanity_id"] || ActiveSupport::SecureRandom.hex(16)
cookies["vanity_id"] = { :value=>@vanity_identity, :expires=>1.month.from_now }
@@ -56,11 +60,33 @@ def use_vanity(symbol = nil, &block)
around_filter :vanity_context_filter
before_filter :vanity_reload_filter unless ::Rails.configuration.cache_classes
before_filter :vanity_query_parameter_filter
+ after_filter :vanity_track_filter
end
protected :use_vanity
end
-
+ module UseVanityMailer
+ def use_vanity_mailer(symbol = nil)
+ # Context is the instance of ActionMailer::Base
+ Vanity.context = self
+ if symbol && (@object = symbol)
+ class << self
+ define_method :vanity_identity do
+ @vanity_identity = (String === @object ? @object : @object.id)
+ end
+ end
+ else
+ class << self
+ define_method :vanity_identity do
+ @vanity_identity = @vanity_identity || ActiveSupport::SecureRandom.hex(16)
+ end
+ end
+ end
+ end
+ protected :use_vanity_mailer
+ end
+
+
# Vanity needs these filters. They are includes in ActionController and
# automatically added when you use #use_vanity in your controller.
module Filters
@@ -108,7 +134,15 @@ def vanity_query_parameter_filter
def vanity_reload_filter
Vanity.playground.reload!
end
-
+
+ # Filter to track metrics
+ # pass _track param along to call track! on that alternative
+ def vanity_track_filter
+ if request.get? && params[:_track]
+ track! params[:_track]
+ end
+ end
+
protected :vanity_context_filter, :vanity_query_parameter_filter, :vanity_reload_filter
end
@@ -155,6 +189,18 @@ def ab_test(name, &block)
value
end
end
+
+ # Generate url with the identity of the current user and the metric to track on click
+ def vanity_track_url_for(identity, metric, options = {})
+ options = options.merge(:_identity => identity, :_track => metric)
+ url_for(options)
+ end
+
+ # Generate url with the fingerprint for the current Vanity experiment
+ def vanity_tracking_image(identity, metric, options = {})
+ options = options.merge(:controller => :vanity, :action => :image, :_identity => identity, :_track => metric)
+ image_tag(url_for(options), :width => "1px", :height => "1px", :alt => "")
+ end
def vanity_js
return if @_vanity_experiments.nil?
@@ -202,15 +248,22 @@ def chooses
end
def add_participant
- if params[:e].nil? || params[:e].empty?
- render :status => 404, :nothing => true
- return
- end
+ if params[:e].nil? || params[:e].empty?
+ render :status => 404, :nothing => true
+ return
+ end
exp = Vanity.playground.experiment(params[:e])
exp.chooses(exp.alternatives[params[:a].to_i].value)
render :status => 200, :nothing => true
end
end
+
+ module TrackingImage
+ def image
+ # send image
+ send_file(File.expand_path("../images/x.gif", File.dirname(__FILE__)), :type => 'image/gif', :stream => false, :disposition => 'inline')
+ end
+ end
end
end
@@ -237,6 +290,14 @@ def setup_controller_request_and_response
end
end
+if defined?(ActionMailer)
+ # Include in mailer, add view helper methods.
+ ActionMailer::Base.class_eval do
+ include Vanity::Rails::UseVanityMailer
+ include Vanity::Rails::Filters
+ helper Vanity::Rails::Helpers
+ end
+end
# Automatically configure Vanity.
if defined?(Rails)
View
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
@@ -25,4 +25,12 @@ def test_ab_test_using_js_returns_the_same_alternative
assert result == ab_test(:pie_or_cake)
end
end
+
+ def test_vanity_track_url_for_returns_url_with_identity_and_metrics
+ assert_equal vanity_track_url_for("123", :sugar_high, :controller => "controller", :action => "action"), "/controller/action?_identity=123&amp;_track=sugar_high"
+ end
+
+ def test_vanity_tracking_image
+ assert_equal vanity_tracking_image("123", :sugar_high, options = {}), image_tag("/vanity/image?_identity=123&amp;_track=sugar_high", :width => "1px", :height => "1px", :alt => "")
+ end
end
View
@@ -82,7 +82,17 @@ def test_vanity_identity_set_with_block
get :index
assert_equal "576", @controller.send(:vanity_identity)
end
-
+
+ def test_vanity_identity_set_with_indentity_paramater
+ get :index, :_identity => "id_from_params"
+ assert_equal "id_from_params", @controller.send(:vanity_identity)
+
+ @request.cookies['vanity_id'] = "old_id"
+ get :index, :_identity => "id_from_params"
+ assert_equal "id_from_params", @controller.send(:vanity_identity)
+ assert cookies['vanity_id'], "id_from_params"
+ end
+
# query parameter filter
def test_redirects_and_loses_vanity_query_parameter
@@ -110,7 +120,11 @@ def test_does_nothing_with_vanity_query_parameter_for_posts
assert !experiment(:pie_or_cake).showing?(first)
end
-
+ def test_track_param_tracks_a_metric
+ get :index, :_identity => "123", :_track => "sugar_high"
+ assert_equal experiment(:pie_or_cake).alternatives[0].converted, 1
+ end
+
# -- Load path --
def test_load_path

0 comments on commit e861402

Please sign in to comment.