Painless Activity Streams in Rails
An easier way to track events
The Late Train by Alex Becerra is licensed under CC BY 2.0
Whether you’re using one of the many third-party analytics tools for tracking user events or you’re implementing an activity stream on a social site with something like public_activity, at some point in your rails career you’re going to want the server to log events when certain parts of your business logic code fire.
Most tutorials and README files for server-side event tracking libraries will have you track events in your controllers, but sprawling controller methods that do too many things – update state, implement business logic, and track events – are one of the worst aspects of rails programming.
Thankfully, there’s a better way.
We’re big fans of the interactor gem here at Collective Idea, because it lets us move business logic out of our controllers and models, and into POROs (Plain Old Ruby Objects) with clear names and responsibilities.
An app/interactors
directory for the stereotypical rails blog app would have interactors with names like CreatePost
, UpdatePost
, CreateUserComment
, and so on.
Those object names look like event names of the type that we might track with an analytics service like Intercom or Mixpanel, or surface to users as part of an activity stream. That makes these interactors not only the ideal place to implement business logic, but also a perfect site for tracking events.
For some apps interactors may seem like overkill, but once your controller methods start to take on event tracking responsibilities, you’ll find that moving everything into interactors can clean up your app/controllers
directory just as dramatically as the presenter pattern can clean up your models and views.
Note: I actually have my own twist on interactor
, called troupe, which is basically the interactor
gem with contracts added. In this example I’m going to use troupe
-flavored interactors because I find them to be a lot easier and more comfortable to read.
Activity Streams
Consider a blog engine where we’d like to use the public_activity
gem to add a WordPress-style dashboard that features an activity stream. We’d probably have a PostsController#create
method that looks like this:
# POST /posts
def create
@post = Post.new(post_params)
if @post.save
redirect_to @post, notice: 'Post was successfully created.'
else
render :new
end
end
That’s not too bad, and it’s not exactly crying out for an interactor. But what happens when we add activity tracking to it?
Before I continue, I should not that I very much dislike the way that public_activity
recommends that you add a model hook that pulls the current\_user
in from a controller in order to tie the model change record to a particular user. (If you don’t know what I’m talking about, don’t look it up because it’s gross and you shouldn’t see it.) It’s so much better to create activities with a bit of code in the relevant interactor than to use hooks, which you should actually stay away from anyway in your models if you can avoid them.
Let’s take a look at a way that we might add activity tracking directly to the controller above:
def create
@post = Post.new(post_params)
if @post.save
@post.create_activity(
action: :create,
user: current_user
)
redirect_to @post, notice: 'Post was successfully created.'
else
render :new
end
end
This isn’t too bad – it’s just an extra method call. But let’s rewrite the above with an interactor and take a look at the result.
First, the controller method:
def create
create_post = CreatePost.call(user: current_user, params: post_params)
if create_post.success?
redirect_to create_post.post, notice: 'Post was successfully created.'
else
render :new
end
end
That looks nicer already. Now let’s check out the interactor:
class CreatePost
include Troupe
expects :params, :user
provides :post do
Post.new(params)
end
def call
context.fail!(error: post.errors) unless post.save
end
after do
post.create_activity(
action: :create,
owner: user
) if context.success?
end
end
The object above is readable and encapsulates all of the logic around creating posts and updating the activity stream in a single source file. Yeah, it’s more code, and it’s probably overkill for this toy example, but when we begin adding more and more logic to the Post
creation process, having all of that logic broken out into a single file or a collection of discrete but related files begins to make a ton of sense.
It’s clear to anyone reading the above code that the interactor expects two input objects, params
and user
, and that it introduces a new post
object into the interactor context.
The main body of the interactor checks for a successful save
, and then logs the activity by tying it to the current user. If the post doesn’t save, then the interactor fails, so that the controller knows to take the sad path.
Maintaining and Testing
If CreatePost
starts to bloat by taking on too much responsibility, interactor
makes it easier to split it up into multiple interactors that are chained together with an Organizer
object.
An organizer for a more complex post creation process might look something like the following:
class PublishPost
include Interactor::Organizer
organize CreatePost,
CreateActivity,
EmailPostAuthor,
WritePostToCDN,
BustPartialCaches
end
Nobody in their right mind would want to cram such things into a controller. And trying to use hooks on the Post
model would be a nightmare. It’s far better to break all of the functions up into separate POROs that you can reason about and test individually. Also, the controller method would look the same (except you’d call PublishPost
instead of CreatePost
, of course).
Speaking of tests, in addition to making the app easier to maintain as it grows, the other big advantage to the interactor-based approach is that it makes testing easier. You can skip writing unit tests for controllers, and just write a unit tests for the interactors. These test files cleanly encapsulate and document all of the business logic in a way that’s easy for newcomers to the app to find and read.
So, go forth and track events on the server side without cluttering up your controllers and models. It’s easy, and once you get the hang of it you might be able to decrease your reliance on third-party tracking tools and begin taking back your own data.
Comments
I ran across your article after implementing an activity stream based on a Railcasts episode because I didn’t want to use a gem. I have never used an Interactor but this was a very interesting article. I would be curious to see how you deal with not just the recording of an activity, but the display. It looks like you are still using public_activity, but are really dealing with one issue I encountered but not another.
Ryan wrote a very simple method added to application_controller so that each controller method only gains one additional line of code to create an activity event. The largest volume of code comes in dealing with all the partials involved in displaying the resulting activity stream to the user.
Note: If you forego all the partials, you can easily just present the results, but without additional processing the usefulness is diminished. I’d love to see how you would deal with this side of implementing an activity stream.
Hi, just wondering if in your example of using the
CreatePost
in the controller, you either reversed the usage order in the controller, if if the implementation of your methods are incorrect. It would appear that yourcall
method does the save and that would not parallel the initial example of how the controller was supposed to work. Maybe I am missing something. Thanks!…then again, I think I see how you changed the location of the activities now. I think I get it now. Thanks