Track email opens using a pixel tracker from Rails
If you are sending out emails to you customers from your web app, it pretty handy to know if they are opening them. If you have ever sent out an email newsletter from a service like Campaign Monitor, you would have seen email open graphs. Of course, tracking this stuff is super important for a newsletter campaign, but it would also be interesting to see if users are opening, for example, welcome emails or onboarding emails.
The simplest way to do this is via a tracking pixel – a small, invisible image that is loaded off your server every time the email is opened (See caveat below). This is fairly simple to achieve using Rails by building a simple Rack application.
Caveat
This only works for HTML emails (unless you can work out how to embed an image in a plain text email), and relies on the user having “Load images” turned on. Clearly, this isn’t super accurate, but it should give you a decent estimate.
The Setup
We’ll add two models: One to tracking sending email, and one to track opening of email:
rails generate model sent_email
rails generate model sent_email_open
The schema for these are fairly simple: Save a name to identify the email, the email address it was sent to, an ip address and when the email was sent and opened.
class CreateSentEmails < ActiveRecord::Migration
def change
create_table :sent_emails do |t|
t.string :name
t.string :email
t.datetime :sent
t.timestamps
end
end
end
class CreateSentEmailOpens < ActiveRecord::Migration
def change
create_table :sent_email_opens do |t|
t.string :name
t.string :email
t.string :ip_address
t.string :opened
t.timestamps
end
end
end
class SentEmail < ActiveRecord::Base
attr_accessible :name, :email, :sent
end
class SentEmailOpen < ActiveRecord::Base
attr_accessible :name, :email, :ip_address, :opened
end
With the models setup, let’s add a mail helper that will generate the image pixel – create this is /app/helpers/mailer_helper.rb
module MailerHelper
def track(name, email)
SentEmail.create!(:name =d> name, :email =d> email, :sent =d> DateTime.now)
url = "#{root_path(:only_path =d> false)}email/track/#{Base64.encode64("name=#{name}&email=#{email}")}.png"
raw("<img src=\"#{url}\" alt="" width=\"1\" height=\"1\"d>")
end
end
What this does is give our mailers a method called track that takes a name for the email and the email address of the person we are sending it to. To enable it, call the helper method in the mailers you want to track:
class UserMailer < ActionMailer::Base
helper :mailer
end
Now we can add the tracker to our html emails. Say we have a registration email that goes out, and there is a user variable, with an email attribute:
<!-- Snip --d>
<%= track('register', @user.email) %d>
<!-- Snip --d>
Right, now the magic bit:
Create a directory called /lib/email_tracker and create a new file called rack.rb
module EmailTracker
class Rack
def initialize(app)
@app = app
end
def call(env)
req = ::Rack::Request.new(env)
if req.path_info =~ /^\/email\/track\/(.+).png/
details = Base64.decode64(Regexp.last_match[1])
name = nil
email = nil
details.split('&').each do |kv|
(key, value) = kv.split('=')
case(key)
when('name')
name = value
when('email')
email = value
end
end
if name && email
SentEmailOpen.create!({
:name =d> name,
:email =d> email,
:ip_address =d> req.ip,
:opened =d> DateTime.now
})
end
[ 200, { 'Content-Type' =d> 'image/png' }, [ File.read(File.join(File.dirname(__FILE__), 'track.png')) ] ]
else
@app.call(env)
end
end
end
end
Create a 1×1 pixel transparent PNG and save it as track.png and place it in the same directory. Next, include it in your config/application.rb
module App
class Application < Rails::Application
require Rails.root.join('lib', 'email_tracker', 'rack')
# Some other stuff
config.middleware.use EmailTracker::Rack
end
end
And that’s it! Now, every time the email gets sent out, it will create a record in the send_emails table, and if it is opened (and images are turned on) it will create a record in send_email_opens. Doing up a status board is left up as an exercise to the user, but you can check you percentage open rate by doing something like:
(SentEmailOpen.where(:name =d> 'register').count.to_f / SentEmail.where(:name =d> 'register').to_f) * 100
How it works
It’s super simple. The track method generates a Base64 encoded string that stores the name of the email and the email address it is being sent to. It them returns an image URL that can be embedded in the email. The Rack app looks for a URL that looks like /track/email/[encodedstring].png and if it matches records the hit. It then returns a transparent PNG to keep the email client happy.
I might get around to turning this into a gem if there is enough interest.