/ahoy_email

First-party email analytics for Rails

Primary LanguageRubyMIT LicenseMIT

Ahoy Email

First-party email analytics for Rails

🔥 For web and native app analytics, check out Ahoy

🚄 To manage unsubscribes, check out Mailkick

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

Getting Started

There are three main features:

Message History

Ahoy Email 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 Email records the user a message is sent to - not just the email address. This gives you a history of messages for each user, even if they change 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[6.1]
  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

Use UTM tagging to attribute a conversion (like an order) to an email campaign. If you use Ahoy for web analytics:

  1. Send an email with UTM parameters
  2. When a user visits the site, Ahoy will create a visit with the UTM parameters
  3. When a user orders, the visit will be associated with the order (if configured)

Add UTM parameters to links with:

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} %>

Open & Click Analytics

While it’s nice to get feedback on the performance of your emails, we discourage the use of open tracking. If you do decide to use open or click tracking, be sure to get consent from your users and consider a short retention period. Check out this article for more best practices.

Setup

Create a migration with:

class AddTokenToAhoyMessages < ActiveRecord::Migration[6.1]
  def change
    add_column :ahoy_messages, :token, :string
    add_index :ahoy_messages, :token

    # for opens
    add_column :ahoy_messages, :opened_at, :timestamp

    # for clicks
    add_column :ahoy_messages, :clicked_at, :timestamp
  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
end

Use only and except to limit actions

class CouponMailer < ApplicationMailer
  track click: true, only: [:welcome]
end

Or make it conditional

class CouponMailer < ApplicationMailer
  track click: -> { params[:user].opted_in? }
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

We recommend encrypting the to field (as well as the subject if it’s sensitive). Lockbox 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

  encrypts :to
  blind_index :to
end

Data Retention

Delete older data with:

Ahoy::Message.where("created_at < ?", 1.year.ago).in_batches.delete_all

Delete data for a specific user with:

Ahoy::Message.where(user_id: 1).in_batches.delete_all

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 Active Record, 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

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:

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