Timber helps you create flexible, unobtrusive activity logs using Rails notifications. Timber is best for making the type of logs you normally see on an administrative dashboard:
Sally drafted "A snowy night in the woods" (about 2 hours ago) Jim edited "A snowy night in the woods" (about 6 hours ago) Frank published "Autumnal Bliss" (2 days ago) Andy... and so on
¶ ↑
InstallationInclude the gem to your Gemfile
:
gem 'timber'
Install and run the necessary migrations:
rake timber_engine:install:migrations rake db:migrate
¶ ↑
UsageLet’s say your application has authors, posts, and publishers:
class Author < ActiveRecord::Base belongs_to :publisher has_many :posts end class Post < ActiveRecord::Base belongs_to :publisher belongs_to :author end class Publisher < ActiveRecord::Base has_many :posts has_many :authors end
And you want to create a log item whenever a post is created or updated.
First, you need to set up an initializer at config/initializers/timber.rb
.
Timber.register do subscribe "posts#create" do |event| post = Post.find_by_title(event.params[:post][:title]) event.log( trackable: post, owner: post.publisher, key: "timber.posts.create", parameters: { link_to_author: link_to(event.current_user.name, author_path(event.current_user)) } ) end end
Inside the Timber.registration
block, you can subscribe
to as many controller#action
events as you like. Also, notice that you have access to your applications custom url helpers (e.g. author_path
) as well as built-in helpers (e.g. link_to
). You can also access the current user off the event object (i.e. event.current_user
).
In order to create an activity record, each subscription should include a call to event.log
:
event.log( # The object being acted on (i.e. created, updated, etc.). Polymorphic association. trackable: post, # The owner (i.e. often the person doing the work). Polymorphic association. owner: post.publisher, # The key string used to lookup the text template in +timber.en.yml+. If omitted, it will # default to `timber.#{event.controller}.#{event.action}` key: "timber.posts.create", # A serialized hash of arbitrary data. This is a good place to stuff miscellaneous # data in order to cut down on database queries when rendering the activity `text`. # But keep in mind that ActiveRecord doesn't make it easy to query serialized data # (see: http://stackoverflow.com/questions/9814622). parameters: { link_to_author: link_to(event.current_user.name, author_path(event.current_user)) } )
Next, create a yaml file to specify your templates in config/locales/timber.en.yml
. Within this file, you have access to the activity instance, allowing your messages to mix static and dynamic content:
timber: posts: create: "<%= parameters[:link_to_author] %> created a post titled <%= trackable.title %>" # You can use `p` as a shorthand for `parameters` update: "<%= p[:link_to_author] %> edited <%= trackable.title %>"
Then you’ll need to somehow fetch a collection of activities. Timber deliberately adds no magical methods to you models: you’ll need to write them yourself. An activities
association, for example, might look like:
has_many :activities, foreign_key: :owner_id, class_name: 'Timber::Activity'
Finally, you’ll want to display the activities in your view:
<ul> <% @publisher.activities.each do |activity| %> <li> <%= activity.text %> (<%= time_ago_in_words(activity.created_at) %> ago) </li> <% end %> </ul>
¶ ↑
Best Practices¶ ↑
Minimize database queries when rendering activity messagesThe parameters
hash should be used to store whatever data is required to render your activity message. Doing so will prevent slow page loads (especially if you’re preforming complicated joins, etc). For example, let’s say you’re logging the creation of a post:
event.log( trackable: post, owner: event.current_user, parameters: { link_to_user_profile: link_to(event.current_user.name, event.current_user.id), link_to_post: link_to(post.title, post.id) } )
Then your timber.yml
should look like:
<%= p[:link_to_user_profile] %> drafted a post titled: <%= p[:link_to_post] %>
We’re pulling everything we need directly off the activity.parameters
. No additional lookups, joins, etc.
¶ ↑
Keep view rendering logic out of your yaml templateIf you need links and whatnot, you should construct them in your initializer (i.e. timber.rb
) and definitely not in your template (i.e. timber.yml
).
# Good <%= p[:link_to_user_profile] %> drafted a post titled: <%= p[:link_to_post] %> # Awful <a href=<%= "/users/#{owner.id}"%>><%= owner.name %></a> drafted a post titled: <a href=<%= "/posts/#{trackable.id}"%>><%= trackable.title %></a>
¶ ↑
Use numerical ids rather than pretty urls when adding links to your activity messagesThis doesn’t apply unless you’re using something like friendly_id to generate pretty urls. Generally, however, you should use ordinary numerical ids to reference objects within your activity messages. This will help protect your links from breaking due to data changes over time.
¶ ↑
Creating an activity outside of the timber initializerIn the timber initializer, you can easily create activities whenever an action is completed. Sometimes, however, you need to create activities that are not tied to controller logic (e.g. in an after_save
).
It’s easy enough to create a timber activity directly:
Timber::Activity.create( trackable: @lumberjack, owner: @lumberjack.saw_mill, key: "timber.path.to.template", parameters: { user_name: 'Paul Bunyan', user_friends: ['Blue Ox'] } )
¶ ↑
Testing your activitiesTesting activities is sorta tricky since it crosses the entire stack. Integration tests, however, are not a good option since a large application may subscribe to scores of activities. In most cases then, writing an integration test for each activity is just not feasible.
Instead, I suggest you devise a way to unit test your subscriptions. To make things a bit easier, you can include Timber::NotificationHelpers
in your RSpec.configure
block:
# Include +publish_notification+ method for unit testing Timber activities config.include Timber::NotificationHelpers
This module provides a publish_notification
method which allows you to publish a notification – specifying a controller, action, current_user and request params. You can find example usage in the test suite.
¶ ↑
ContributingFork the project, make your changes, and submit a pull request. Please ensure the tests pass:
rspec .
This gem is a based on public_activity. Thanks to @pokonski for his good ideas, especially the use of i18n for generating the activity text.
¶ ↑
TODO-
Currently, timber expects your current user method to be
current_user
. This should be configurable. -
When editing an object, it would be really nice if the
event
contained information about what changed, allowing you to do things like: Jim updated the post title from “Snowy Night” to “Snowing Evening”. -
Support for HAML style interpolation within
timber.en.yml
in addition to ERB.
¶ ↑
LicenseTimber is an open source project built by Scholastica under the MIT-LICENSE.