Ahoy Email

:postbox: Email analytics for Rails

You get:

  • A history of emails sent to each user
  • Easy UTM tagging
  • Optional open and click tracking

Ahoy Email 1.0 was recently released! See how to upgrade

:bullettrain_side: To manage unsubscribes, check out Mailkick

:fire: To track visits and events, check out Ahoy

Build Status

Installation

Add this line to your application’s Gemfile:

gem 'ahoy_email'

And run the generator. This creates a model to store messages.

rails generate ahoy_email:install
rails db:migrate

How It Works

Message History

Ahoy creates an Ahoy::Message record for each email sent by default. You can disable history for a mailer:

class CouponMailer < ApplicationMailer
  track message: false # use only/except to limit actions
end

Or by default:

AhoyEmail.default_options[:message] = false

Users

Ahoy records the user a message is sent to - not just the email address. This gives you a full history of messages for each user, even if he or she changes addresses.

By default, Ahoy tries @user then params[:user] then User.find_by(email: message.to) to find the user.

You can pass a specific user with:

class CouponMailer < ApplicationMailer
  track user: -> { params[:some_user] }
end

The user association is polymorphic, so use it with any model.

To get all messages sent to a user, add an association:

class User < ApplicationRecord
  has_many :messages, class_name: "Ahoy::Message", as: :user
end

And run:

user.messages

Extra Attributes

Record extra attributes on the Ahoy::Message model.

Create a migration to add extra attributes to the ahoy_messages table. For example:

class AddCouponIdToAhoyMessages < ActiveRecord::Migration[5.2]
  def change
    add_column :ahoy_messages, :coupon_id, :integer
  end
end

Then use:

class CouponMailer < ApplicationMailer
  track extra: {coupon_id: 1}
end

You can use a proc as well.

class CouponMailer < ApplicationMailer
  track extra: -> { {coupon_id: params[:coupon].id} }
end

UTM Tagging

Automatically add UTM parameters to links.

class CouponMailer < ApplicationMailer
  track utm_params: true # use only/except to limit actions
end

The defaults are:

  • utm_medium - email
  • utm_source - the mailer name like coupon_mailer
  • utm_campaign - the mailer action like offer

You can customize them with:

class CouponMailer < ApplicationMailer
  track utm_params: true, utm_campaign: -> { "coupon#{params[:coupon].id}" }
end

Skip specific links with:

<%= link_to "Go", some_url, data: {skip_utm_params: true} %>

Opens & Clicks

Setup

Additional setup is required to track opens and clicks.

Create a migration with:

class AddTokenToAhoyMessages < ActiveRecord::Migration[5.2]
  def change
    add_column :ahoy_messages, :token, :string
    add_column :ahoy_messages, :opened_at, :timestamp
    add_column :ahoy_messages, :clicked_at, :timestamp

    add_index :ahoy_messages, :token
  end
end

Create an initializer config/initializers/ahoy_email.rb with:

AhoyEmail.api = true

And add to mailers you want to track:

class CouponMailer < ApplicationMailer
  track open: true, click: true # use only/except to limit actions
end

How It Works

For opens, an invisible pixel is added right before the </body> tag in HTML emails. If the recipient has images enabled in their email client, the pixel is loaded and the open time recorded.

For clicks, a redirect is added to links to track clicks in HTML emails.

https://chartkick.com

becomes

https://yoursite.com/ahoy/messages/rAnDoMtOkEn/click?url=https%3A%2F%2Fchartkick.com&signature=...

A signature is added to prevent open redirects.

Skip specific links with:

<%= link_to "Go", some_url, data: {skip_click: true} %>

By default, unsubscribe links are excluded. To change this, use:

AhoyEmail.default_options[:unsubscribe_links] = true

You can specify the domain to use with:

AhoyEmail.default_options[:url_options] = {host: "mydomain.com"}

Events

Subscribe to open and click events by adding to the initializer:

class EmailSubscriber
  def open(event)
    # your code
  end

  def click(event)
    # your code
  end
end

AhoyEmail.subscribers << EmailSubscriber.new

Here’s an example if you use Ahoy to track visits and events:

class EmailSubscriber
  def open(event)
    event[:controller].ahoy.track "Email opened", message_id: event[:message].id
  end

  def click(event)
    event[:controller].ahoy.track "Email clicked", message_id: event[:message].id, url: event[:url]
  end
end

AhoyEmail.subscribers << EmailSubscriber.new

Data Protection

Protect the privacy of your users by encrypting the to field. attr_encrypted is great for this. Use blind_index if you need to query by the to field.

Create app/models/ahoy/message.rb with:

class Ahoy::Message < ApplicationRecord
  self.table_name = "ahoy_messages"
  belongs_to :user, polymorphic: true, optional: true

  attr_encrypted :to, key: ...
  blind_index :to, key: ...
end

Reference

Set global options

AhoyEmail.default_options[:user] = -> { params[:admin] }

Use a different model

AhoyEmail.message_model = -> { UserMessage }

Or fully customize how messages are tracked

AhoyEmail.track_method = lambda do |data|
  # your code
end

Mongoid

If you prefer to use Mongoid instead of ActiveRecord, create app/models/ahoy/message.rb with:

class Ahoy::Message
  include Mongoid::Document

  belongs_to :user, polymorphic: true, optional: true, index: true

  field :to, type: String
  field :mailer, type: String
  field :subject, type: String
  field :sent_at, type: Time
end

Upgrading

1.0

Breaking changes

  • UTM tagging, open tracking, and click tracking are no longer enabled by default. To enable, create an initializer with:
  AhoyEmail.api = true

  AhoyEmail.default_options[:open] = true
  AhoyEmail.default_options[:click] = true
  AhoyEmail.default_options[:utm_params] = true
  • Only sent emails are recorded
  • Proc options are now executed in the context of the mailer and take no arguments
  # old
  user: ->(mailer, message) { User.find_by(email: message.to) }

  # new
  user: -> { User.find_by(email: message.to) }
  • Invalid options now throw an ArgumentError
  • AhoyEmail.track was removed in favor of AhoyEmail.default_options
  • The heuristic_parse option was removed and is now the default

History

View the changelog

Contributing

Everyone is encouraged to help improve this project. Here are a few ways you can help:

To get started with development and testing:

git clone https://github.com/ankane/ahoy_email.git
cd ahoy_email
bundle install
rake test