• Stars
    star
    342
  • Rank 123,697 (Top 3 %)
  • Language
    Ruby
  • License
    MIT License
  • Created about 4 years ago
  • Updated about 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A Rails engine for drip campaigns/scheduled sequences and periodical support. Works with ActionMailer, and other things.
Caffeinate logo

Caffeinate

Caffeinate is a drip engine for managing, creating, and performing scheduled messages sequences from your Ruby on Rails application. This was originally meant for email, but now supports anything!

Caffeinate provides a simple DSL to create scheduled sequences which can be sent by ActionMailer, or invoked by a Ruby object, without any additional configuration.

There's a cool demo app you can spin up here.

Now supports POROs!

Originally, this was meant for just email, but as of V2.3 supports plain old Ruby objects just as well. Having said, the documentation primarily revolves around using ActionMailer, but it's just as easy to plug in any Ruby class. See Using Without ActionMailer below.

Is this thing dead?

No! Not at all!

There's not a lot of activity here because it's stable and working! I am more than happy to entertain new features.

Oh my gosh, a web UI!

See https://github.com/joshmn/caffeinate-webui for an accompanying lightweight UI for simple administrative tasks and overview.

Do you suffer from ActionMailer tragedies?

If you have anything like this is your codebase, you need Caffeinate:

class User < ApplicationRecord
  after_commit on: :create do
    OnboardingMailer.welcome_to_my_cool_app(self).deliver_later
    OnboardingMailer.some_cool_tips(self).deliver_later(wait: 2.days)
    OnboardingMailer.help_getting_started(self).deliver_later(wait: 3.days)
  end
end
class OnboardingMailer < ActionMailer::Base
  def welcome_to_my_cool_app(user)
    mail(to: user.email, subject: "Welcome to CoolApp!")
  end

  def some_cool_tips(user)
    return if user.unsubscribed_from_onboarding_campaign?

    mail(to: user.email, subject: "Here are some cool tips for MyCoolApp")
  end

  def help_getting_started(user)
    return if user.unsubscribed_from_onboarding_campaign?
    return if user.onboarding_completed?

    mail(to: user.email, subject: "Do you need help getting started?")
  end
end

What's wrong with this?

  • You're checking state in a mailer
  • The unsubscribe feature is, most likely, tied to a User, which means...
  • It's going to be so fun to scale when you finally want to add more unsubscribe links for different types of sequences
    • "one of your projects has expired", but which one? Then you have to add a column to projects and manage all that state... ew

Perhaps you suffer from enqueued worker madness

If you have anything like this is your codebase, you need Caffeinate:

class User < ApplicationRecord
  after_commit on: :create do
    OnboardingWorker.perform_later(:welcome, self.id)
    OnboardingWorker.perform_in(2.days, :some_cool_tips, self.id)
    OnboardingWorker.perform_later(3.days, :help_getting_started, self.id)
  end
end
class OnboardingWorker
  include Sidekiq::Worker
  
  def perform(action, user_id)
    user = User.find(user_id)
    user.public_send(action)
  end
end

class User
  def welcome
    send_twilio_message("Welcome to our app!")
  end

  def some_cool_tips
    return if self.unsubscribed_from_onboarding_campaign?

    send_twilio_message("Here are some cool tips for MyCoolApp")
  end

  def help_getting_started
    return if unsubscribed_from_onboarding_campaign?
    return if onboarding_completed?

    send_twilio_message("Do you need help getting started?")
  end
  
  private 
  
  def send_twilio_message(message)
    twilio_client.messages.create(
            body: message,
            to: "+12345678901",
            from: "+15005550006",
    )
  end
  
  def twilio_client
    @twilio_client ||= Twilio::REST::Client.new Rails.application.credentials.twilio[:account_sid], Rails.application.credentials.twilio[:auth_token]
  end
end

I don't even need to tell you why this is smelly!

Do this all better in five minutes

In five minutes you can implement this onboarding campaign:

Install it

Add to Gemfile, run the installer, migrate:

$ bundle add caffeinate
$ rails g caffeinate:install
$ rake db:migrate

Clean up the business logic

Assuming you intend to use Caffeinate to handle emails using ActionMailer, mailers should be responsible for receiving context and creating a mail object. Nothing more. (If you are looking for examples that don't use ActionMailer, see Without ActionMailer.)

The only other change you need to make is the argument that the mailer action receives. It will now receive a Caffeinate::Mailing. Learn more about the data models:

class OnboardingMailer < ActionMailer::Base
  def welcome_to_my_cool_app(mailing)
    @user = mailing.subscriber
    mail(to: @user.email, subject: "Welcome to CoolApp!")
  end

  def some_cool_tips(mailing)
    @user = mailing.subscriber
    mail(to: @user.email, subject: "Here are some cool tips for MyCoolApp")
  end

  def help_getting_started(mailing)
    @user = mailing.subscriber
    mail(to: @user.email, subject: "Do you need help getting started?")
  end
end

Create a Dripper

A Dripper has all the logic for your sequence and coordinates with ActionMailer on what to send.

In app/drippers/onboarding_dripper.rb:

class OnboardingDripper < ApplicationDripper
  # each sequence is a campaign. This will dynamically create one by the given slug
  self.campaign = :onboarding 
  
  # gets called before every time we process a drip
  before_drip do |_drip, mailing| 
    if mailing.subscription.subscriber.onboarding_completed?
      mailing.subscription.unsubscribe!("Completed onboarding")
      throw(:abort)
    end 
  end
  
  # map drips to the mailer
  drip :welcome_to_my_cool_app, mailer: 'OnboardingMailer', delay: 0.hours
  drip :some_cool_tips, mailer: 'OnboardingMailer', delay: 2.days
  drip :help_getting_started, mailer: 'OnboardingMailer', delay: 3.days
end

We want to skip sending the mailing if the subscriber (User) completed onboarding. Let's unsubscribe with #unsubscribe! and give it an optional reason of Completed onboarding so we can reference it later when we look at analytics. throw(:abort) halts the callback chain just like regular Rails callbacks, stopping the mailing from being sent.

Add a subscriber to the Campaign

Call OnboardingDripper.subscribe to subscribe a polymorphic subscriber to the Campaign, which creates a Caffeinate::CampaignSubscription.

class User < ApplicationRecord
  after_commit on: :create do
    OnboardingDripper.subscribe!(self)
  end
end

Run the Dripper

You'll usually do this in a scheduled background job or cron.

OnboardingDripper.perform!

Alternatively, you can run all of the registered drippers with Caffeinate.perform!.

Done

You're done.

Check out the docs for a more in-depth guide that includes all the options you can use for more complex setups, tips, tricks, and shortcuts.

Using Without ActionMailer

Now supports POROs that inherit from a magical class! Using the example above, implementing an SMS client. The same rules apply, just change mailer_class or mailer to action_class, and create a Caffeinate::ActionProxy (acts just like an ActionMailer). See Without ActionMailer.) for more.

But wait, there's more

Caffeinate also...

  • ✅ Works with regular Ruby methods as of V2.3
  • ✅ Allows hyper-precise scheduled times. 9:19AM in the user's timezone? Sure! Only on business days? YES!
  • ✅ Periodicals
  • ✅ Manages unsubscribes
  • ✅ Works with singular and multiple associations
  • ✅ Compatible with every background processor
  • ✅ Tested against large databases at AngelList and is performant as hell
  • ✅ Effortlessly handles complex workflows
    • Need to skip a certain mailing? You can!

Documentation

Upcoming features/todo

Handy dandy roadmap.

Alternatives

Not a fan of Caffeinate? I built it because I wasn't a fan of the alternatives. To each their own:

Contributing

There's so much more that can be done with this. I'd love to see what you're thinking.

If you have general feedback, I'd love to know what you're using Caffeinate for! Please email me (any-thing [at] josh.mn) or tweet me @joshmn or create an issue! I'd love to chat.

Contributors & thanks

  • Thanks to sourdoughdev for releasing the gem name to me. :)
  • Thanks to markokajzer for listening to me talk about this most mornings.

License

The gem is available as open source under the terms of the MIT License.

More Repositories

1

ahoy_captain

A full-featured, mountable analytics dashboard for your Rails app, powered by the Ahoy gem.
Ruby
357
star
2

turbo_flash

Automagically include your flash messages in your Ruby on Rails Hotwire TurboStream responses.
Ruby
61
star
3

setsy

Settings for classes backed by a database with defaults.
Ruby
37
star
4

attributary

Like ActiveModel::Attributes but less fluffy and more attribute-y
Ruby
37
star
5

mailtime

Make sending mails from Rails suck less.
Ruby
28
star
6

metricky

Build, query, and render metrics from your Rails application.
Ruby
26
star
7

mattermost-ruby

An ActiveRecord-inspired API client for Mattermost
Ruby
26
star
8

jeanine

A lightning-fast, batteries-included Ruby web micro-framework.
Ruby
14
star
9

hotflash

Automagically inject flash messages into Ruby on Rails TurboStream responses using Hotwire.
Ruby
12
star
10

gearhead

Generate an API for your database, with searching and filtering, automatically
Ruby
10
star
11

caffeinate-webui

HTML
9
star
12

activeduty

Service objects.
Ruby
9
star
13

consolecreep

Creep on which one of your developers is doing what in Rails console
Ruby
7
star
14

caffeinate-marketing

The caffeinate.email site
CSS
4
star
15

formed

The form object pattern for Ruby on Rails you never knew you needed so badly.
Ruby
4
star
16

turbo

JavaScript
4
star
17

json-benchmark

Ruby
3
star
18

acts_as_boolean

Treat time-y attributes like boolean attributes.
Ruby
2
star
19

auto_previews

Automatically create mailer previews for your mailers.
Ruby
2
star
20

active_form

Ruby
2
star
21

delete_you_later

Delete/destroy associated ActiveRecord records in the background with ease.
Ruby
2
star
22

sidekiq_retry_on

Ruby
2
star
23

brody_app

My really opinionated Rails starter app that should be a template
Ruby
1
star
24

asdfasdf

Ruby
1
star
25

activeadminamazingcrap

Ruby
1
star
26

typicode

Ruby
1
star
27

darted

Ruby
1
star
28

rails-settings-cached

Ruby
1
star
29

active_action

An elegant DSL to define batch actions in your Rails controllers and display them in your views.
Ruby
1
star