Heya đ is a campaign mailer for Rails. Think of it like ActionMailer, but for timed email sequences. It can also perform other actions like sending a text message.
Heya is a campaign mailer for Rails. Think of it like ActionMailer, but for
timed email sequences. It can also perform other actions like sending a text
message.
Getting started with Heya is easy:
Heya was built to work with PostgreSQL. Pull requests are welcome to support
more databases.
Add this line to your applicationâs Gemfile:
gem "heya", github: "honeybadger-io/heya"
Then execute:
bundle install
rails generate heya:install
rails db:migrate
This will:
User
model (it never writes). If you
have a different user model, change the user_type
configuration
option in config/initializers/heya.rb.# config/initializers/heya.rb
Heya.configure do |config|
config.user_type = "MyUser"
end
Create a campaign:
rails generate heya:campaign Onboarding welcome:0
Add a user to your campaign:
OnboardingCampaign.add(user)
Add the following to your User
model to send them the campaign
when they first sign up:
after_create_commit do
OnboardingCampaign.add(self)
end
To start queuing emails, run the scheduler task periodically:
rails heya:scheduler
Heya uses ActiveJob to send emails in the background. Make sure your
ActiveJob backend is configured to process the heya
queue. For example,
hereâs how you might start Sidekiq:
bundle exec sidekiq -q default -q heya
You can change Heyaâs default queue using the queue
option:
# app/campaigns/application_campaign.rb
class ApplicationCampaign < Heya::Campaigns::Base
default queue: "custom"
end
# config/environments/development.rb
Rails.application.configure do
# ..
# Use MailCatcher to inspect emails
# http://mailcatcher.me
# Usage:
# gem install mailcatcher
# mailcatcher
# # => Starting MailCatcher
# # => ==> smtp://127.0.0.1:1025
# # => ==> http://127.0.0.1:1080
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {host: "localhost", port: 1025}
end
$ bundle add maildown
$ rails generate heya:campaign Onboarding welcome
create app/campaigns/application_campaign.rb
create app/campaigns/onboarding_campaign.rb
create app/views/heya/campaign_mailer/onboarding_campaign/welcome.md.erb
âď¸ Notice how only one template was generated; Maildown automatically builds
the HTML and text variants from the markdown file.
Heyaâs campaign generator generates previews for campaigns at
(test|spec)/mailers/previews/*_campaign_preview.rb
. To see them, open
http://localhost:3000/rails/mailers/. If you didnât use the generator, you
can still build your own preview:
# test/mailers/previews/onboarding_campaign_preview.rb
class OnboardingCampaignPreview < ActionMailer::Preview
def welcome
OnboardingCampaign.welcome(user)
end
private
def user
User.where(id: params[:user_id]).first || User.first
end
end
You can use the following options to configure Heya (find this file in
config/initializers/heya.rb):
Heya.configure do |config|
# The name of the model you want to use with Heya.
config.user_type = "User"
# The default options to use when processing campaign steps.
config.campaigns.default_options = {from: "[email protected]"}
# Campaign priority. When a user is added to multiple campaigns, they are
# sent in this order. Campaigns are sent in the order that the users were
# added if no priority is configured.
config.campaigns.priority = [
"FirstCampaign",
"SecondCampaign",
"ThirdCampaign"
]
end
Heya stores campaigns in app/campaigns/, similar to how Rails stores mailers
in app/mailers/. To create a campaign, run the following command inside your
Rails project:
rails generate heya:campaign Onboarding first second third
This will:
Hereâs the campaign that the above command generates:
# app/campaigns/application_campaign.rb
class ApplicationCampaign < Heya::Campaigns::Base
default from: "[email protected]"
end
# app/campaigns/onboarding_campaign.rb
class OnboardingCampaign < ApplicationCampaign
step :first,
subject: "First subject"
step :second,
subject: "Second subject"
step :third,
subject: "Third subject"
end
The step
method defines a new step in the sequence. When you add a user to the
campaign, Heya completes each step in the order that it appears.
The default time to wait between steps is two days, calculated from the time
the user completed the previous step (or the time the user entered the campaign,
in the case of the first step).
Each step has several options available (see the section Creating
messages).
Messages are defined inside Heya campaigns using the step
method. When you add
a user to a campaign, Heya completes each step in the order that it appears.
The most important part of each step is its name, which must be unique to the
campaign. The stepâs name is how Heya tracks which user has received which
message, so itâs essential that you donât change it after the campaign is active
(if you do, Heya will assume itâs a new message).
Hereâs an example of defining a message inside a campaign:
class OnboardingCampaign < ApplicationCampaign
step :welcome, wait: 1.day,
subject: "Welcome to my app!"
end
In the above example, Heya will send a message named :welcome
one day after a
user enters the campaign, with the subject âWelcome to my app!â
The wait
option tells Heya how long to wait before sending each message (the
default is two days). There are a few scheduling options that you can customize
for each step:
Option Name | Default | Description |
---|---|---|
wait |
2.days |
The duration of time to wait before sending each message |
segment |
nil |
The segment who should receive the message |
action |
Heya::Campaigns::Actions::Email |
The action to perform (usually sending an email) |
queue |
"heya" |
The ActiveJob queue |
Heya uses the following additional options to build the message itself:
Option Name | Default | Description |
---|---|---|
subject |
required | The emailâs subject |
from |
Heya default | The senderâs email address |
layout |
Heya default | The emailâs layout file |
to |
See below | The recipientâs name & email address |
bcc |
nil |
BCC when sending emails |
headers |
{} |
Headers to include when sending emails |
You can change the default options using the default
method at the top of the
campaign. Heya applies default options to each step which doesnât supply its
own:
class OnboardingCampaign < ApplicationCampaign
default wait: 1.day,
queue: "onboarding",
from: "[email protected]",
layout: "onboarding"
# Will still be sent after one day from the
# email address [email protected]
step :welcome,
subject: "Welcome to my app!"
end
to
fieldYou can customize the to
field by passing a callable object, which Heya will
invoke with the user. For instance:
class OnboardingCampaign < ApplicationCampaign
step :welcome,
subject: "Welcome to my app!",
to: -> (user) { ActionMailer::Base.email_address_with_name(user.email, user.nickname) }
end
It is recommended to rely on ActionMailer::Base.email_address_with_name
so
that sanitization is correctly applied.
If the to
param is not provided, Heya will default to:
user#first_name
user#name
If the user
object doesnât respond to these methods, it will fallback to a
simple user.email
in the to
field.
You may wish to apply quality control to individual steps of a campaign. For
example, when adding a new step to an existing campaign it is a good idea to
inspect real-time results in production. You can do this by using the bcc:
step option, which would look like this:
class OnboardingCampaign < ApplicationCampaign
default wait: 1.day,
queue: "onboarding",
from: "[email protected]"
# Will still be sent after one day from the
# email address [email protected]
step :welcome,
subject: "Welcome to my app!"
step :added_two_months_later,
subject: "We now have something new to say!",
bcc: '[email protected]'
end
The subject can be customized for each user by using a lambda
instead of a String
:
# app/campaigns/onboarding_campaign.rb
class OnboardingCampaign < ApplicationCampaign
step :welcome,
subject: ->(user) { "Heya #{user.first_name}!" }
end
If you donât pass a subject
to the step
method, Heya will try to find it in
your translations. The performed lookup will use the pattern
<campaign_scope>.<step_name>.subject
to construct the key.
# app/campaigns/onboarding_campaign.rb
class OnboardingCampaign < ApplicationCampaign
step :welcome
end
# config/locales/en.yml
en:
onboarding_campaign:
welcome:
subject: "Heya!"
To define parameters for interpolation, define a #heya_attributes
method on
your user model:
# app/models/user.rb
class User < ApplicationRecord
def heya_attributes
{
first_name: name.split(" ").first
}
end
end
# config/locales/en.yml
en:
onboarding_campaign:
welcome:
subject: "Heya %{first_name}!"
You can override the default step behavior to perform custom actions by passing
a block to the step
method:
class OnboardingCampaign < ApplicationCampaign
step :first_email,
subject: "You're about to receive a txt"
step :sms do |user|
SMS.new(to: user.cell, body: "Hi, #{user.first_name}!").deliver
end
step :second_email,
subject: "Did you get it?"
end
Step blocks receive two optional arguments: user
and step
, and are processed
in a background job alongside other actions.
Heya leaves when to add users to campaigns completely up to you; hereâs how to
add a user to a campaign from anywhere in your app:
OnboardingCampaign.add(user)
To remove a user from a campaign:
OnboardingCampaign.remove(user)
Adding users to campaigns from Rails opens up some interesting automation
possibilitiesâfor instance, you can start or stop campaigns from ActiveRecord
callbacks, or in response to other events that youâre already tracking in your
application. See here for a list of ideas.
Because Heya stacks campaigns by default (meaning it will never send more than
one at a time), you can also queue up several campaigns for a user, and theyâll
receive them in order:
WelcomeCampaign.add(user)
OnboardingCampaign.add(user)
EvergreenCampaign.add(user)
Note: you can customize the priority of campaigns via Heyaâs configuration.
If you want to send a user two campaigns simultaneously, you can do so with the
concurrent
option:
FlashSaleCampaign.add(user, concurrent: true)
When you remove a user from a campaign and add them back later, theyâll continue
where they left off. If you want them to start over from the beginning, use the
restart
option:
TrialConversionCampaign.add(user, restart: true)
Using ActiveSupport::Notifications
to respond to lifecycle events (which could
be sent from your Stripe controller, for instance):
ActiveSupport::Notifications.subscribe("user.trial_will_end") do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
if event.payload[:user_id]
user = User.find(event.payload[:user_id])
TrialConversionCampaign.add(user, restart: true)
end
end
Scheduling campaigns in ActiveRecord
callbacks:
class User < ApplicationRecord
after_create_commit do
WelcomeCampaign.add(self)
OnboardingCampaign.add(self)
EvergreenCampaign.add(user)
end
end
Heya can send individual messages to certain users using the segment
option.
The following campaign will send the message to inactive usersâactive users
will be skipped:
class ActivationCampaign < ApplicationCampaign
step :activate, segment: ->(user) { user.inactive? }
end
When youâre checking the value of a single method on the user, the segment can
be simplified to the symbol version:
class ActivationCampaign < ApplicationCampaign
step :activate, segment: :inactive?
end
You can also narrow entire campaigns to certain users using the segment
method. For instance, if you have a campaign with a specific goal such as
performing an action in your app, then you can send the campaign only to the
users who havenât performed the action:
class UpgradeCampaign < ApplicationCampaign
segment { |u| !u.upgraded? }
step :one
step :two
step :three
end
If they upgrade half way through the campaign, Heya will stop sending messages
and remove them from the campaign.
Likewise, you can require that users meet conditions to continue receiving a
campaign. Hereâs a campaign which sends messages only to trial usersânon-trial
users will be removed from the campaign:
class TrialCampaign < ApplicationCampaign
segment :trial?
step :one
step :two
step :three
end
Heya campaigns inherit options from parent campaigns. For example, to make sure
unsubscribed users never receive an email from Heya, create a segment
in the
ApplicationCampaign
, and then have all other campaigns inherit from it:
class ApplicationCampaign < Heya::Campaigns::Base
segment :subscribed?
end
Heya campaigns are rescuable.
Use the rescue_from
method to handle exceptions in campaigns:
class OnboardingCampaign < ApplicationCampaign
rescue_from Postmark::InactiveRecipientError, with: :log_error
private
def log_error(error)
Rails.logger.error("Got Heya error: #{error}")
end
end
See the
Rails documentation
for additional details.
The campaign generator does not create a Mailer class for campaigns. In order to
enhance a campaign with a macro from another gem (such as for adding analytics),
you can do so by extending the Heya::ApplicationMailer
class.
app/mailers/heya/application_mailer.rb
module Heya
class ApplicationMailer < ActionMailer::Base
macro_to_add_to_all_campaign_mailers
end
end
For example, hereâs how to extended Heya::ApplicationMailer
to include Ahoy
Emailâs
has_history
and
track_clicks
macros:
# app/mailers/heya/application_mailer.rb
module Heya
class ApplicationMailer < ActionMailer::Base
has_history
track_clicks campaign: -> {
params[:step].campaign.name
}
end
end
This does two things:
has_history
enables history tracking for all Heya emailstrack_clicks
block appends the name of the campaign to allThe result of this is that you can run a command like this in the console:
AhoyEmail.stats "OnboardingCampaign"
âŚand receive a result:
=> {:sends=>1, :clicks=>2, :unique_clicks=>1, :ctr=>100.0}
What happens when:
Heya sends the next unsent message after the last message the user received.
When you move a message, the users who last received it will be moved with it,
and continue from that point in the campaign. Heya skips messages which the user
has already seen.
Users who have already received a message after the new message will not
receive the message.
Users who last received the message will be moved up to the previously received
message, and continue from that point in the campaign. Heya skips messages which
the user has already seen.
Renaming a message is equivalent to removing the message and adding a new
one. Users who are waiting to receive an earlier message in the campaign will
receive the new message. Users who last received the old message will also
receive the new one since it has replaced its position in the campaign.
Heya waits the defined wait time for every message in the campaign. If a user
doesnât match the conditions, Heya skips it. If the next messageâs wait time
is less than or equal to the skipped messageâs, it sends it immediately. If the
next wait period is longer, it sends it after the new wait time has elapsed.
Heya will immediately stop sending the campaign; the campaignâs data will remain
until you manually delete it. If you restore the file before deleting the
campaignâs data, Heya will resume sending the campaign.
By default, Heya sends each user one campaign at a time. It determines the order
of campaigns using the campaign priority
. When you add a user to a higher
priority campaign, the new campaign will begin immediately. Once completed, the
next highest priority campaign will resume sending.
To send a campaign concurrent to other active campaigns, use the concurrent
option.
When you add a user to a campaign that they previously completed, Heya sends new
messages which were added to the end of the campaign. Skipped messages will
not be sent. To resend all messages, use the restart
option.
Less frequently asked questions:
Nope, not without restarting the campaign using the restart
option (which will
resend all the messages).
Yep. When you add a user to a campaign that they previously completed, Heya
sends new messages which were added to the end of the campaign. Skipped
messages will not be sent. To resend all messages, use the restart
option.
Yep. Use the restart
option to resend a campaign to a user (if they are
already in the campaign, the campaign will start over from the beginning).
Yep. By default, Heya sends campaigns ain order of priority
. Use the
concurrent
option to send campaigns concurrently.
Heya adheres to Semantic Versioning, and
should be considered unstable until version 1.0.0. Always check
CHANGELOG.md prior to upgrading (breaking changes will always
be called out there). Upgrade instructions for breaking changes are in
UPGRADING.md.
See here for things weâre
considering adding to Heya.
git checkout -b my_branch
git commit -am "Boom"
git push origin my_branch
gem install gem-release
gem bump -v [version] -t -r
git push origin main --tags
Heya is licensed under the LGPL.