Fast and unobtrusive activity tracking of ActiveRecord models using Redis and DataMapper.
Redcrumbs is designed to make it easy to start generating activity feeds in your application using Redis as a back-end.
Introducing activity feeds can come at significant cost, increasing the number of writes to your primary datastore across many controller actions - sometimes when previously only reads were being performed. Activity feeds have their own characteristics too; they're often not mission critical data, expirable over time and queried in predictable ways.
It turns out Redis is an ideal solution. Super fast to write to and read from and with Memcached-style key expiration built in, leaving your primary database to focus on the business logic.
You'll need access to a Redis server running locally, remotely or from a managed service; such as Redis Labs.
Add the Gem to your Gemfile:
gem 'redcrumbs'
Then run the generator to create the initializer file.
$ rails g redcrumbs:install
Done! Look in config/initializers/redcrumbs.rb
for customisation options.
Start tracking a model by adding redcrumbed
to the class:
class Game < ActiveRecord::Base
redcrumbed :only => [:name, :highscore]
validates :name, :presence => true
validates :highscore, :presence => true
end
That's all you need to get started. Game
objects will now start generating activities when their name
or highscore
attributes are updated.
game = Game.last
=> #<Game id: 1, name: "Paperboy" ... >
game.update_attributes(:name => "Paperperson")
=> #<Game id: 1, name: "Paperperson" ... >
Activities are objects of class Crumb
and contain all the data you need to find out about what has changed in the update.
crumb = game.crumbs.last
=> #<Crumb id: 53 ... >
crumb.modifications
=> {"name" => ["Paperboy", "Paperperson"]}
The .crumbs
method shown here is available to any class that is redcrumbed
. It is just a DataMapper collection and you can use it to construct any queries you like. For example, to get the last 10 activities on game
:
game.crumbs.all(:order => :created_at.desc, :limit => 10)
Redcrumbs doesn't provide any helpers to turn crumbs into translated text or HTML views but this is extremely easy to do once you're set up and creating activities.
Now that we know how to get the most recent activities associated with an object we just need to create a helper to translate these into readable text or HTML. Crumbs have a subject
association that gives you access to the original object. This is useful when you need access to attributes that aren't in the modifications hash.
Here's an example of a simple text helper:
module ActivityHelper
def activity_text_from(crumb)
modifications = crumb.modifications
message = 'Someone '
fragments = []
fragments << "set a highscore of #{modifications['highscore'][1]}" if modifications.has_key?('highscore')
if modifications.has_key?('name')
fragments << "renamed #{modifications['name'][0]} to #{modifications['name'][1]}"
else
fragments[0] += " at #{crumb.subject.name}"
end
message += fragments.to_sentence
message += '.'
end
end
And examples of its output:
"Someone renamed Paperboy to Paperperson."
"Someone set a highscore of 19840 at Paperperson."
"Someone set a highscore of 21394 at Paperperson and renamed Paperperson to I WIN NOOBS."
Simply reporting that 'Someone did xyz' isn't very useful, so Redcrumbs has user context baked in.
Crumbs can track the user that made the change (or any object really) as creator
, and even a secondary user affected by the change as target
. You simply define methods called creator
and target
on the subject class that return the corresponding object:
class Game < ActiveRecord::Base
redcrumbed :only => [:name, :highscore]
has_one :high_scorer, class_name: 'Player'
def creator
high_scorer
end
end
To get the creator and target of a crumb:
crumb.creator
=> #<Player id: 394 ...>
crumb.target
=> #<ComputerPlayer id: 3 ...>
As you'd expect you can also grab all the activities affecting a user.
# Activities created by a user
player.crumbs_by
# Activities targeting a user
player.crumbs_for
# All activities affecting a user
player.crumbs_as_user
You can pass :if
and :unless
options to the redcrumbed method to control when an action should be tracked in the same way you would for an ActiveRecord callback. For example, if you only want to track activity after a game has been created:
class Game < ActiveRecord::Base
redcrumbed :only => [:name, :highscore], :unless => :new_record?
#...
end
In many cases to assemble your feed you'll only ever need the modifications
made to an object plus a couple of common attributes; such as name
or id
. When this is the case you can avoid loading the subject from the database entirely by storing those attributes on the crumb itself.
class Game < ActiveRecord::Base
redcrumbed :only => [:name, :highscore], :store => {:only => [:id, :name]}
#...
end
Now when you call crumb.subject
you will get an instance of Game
with only the :id
and :name
attributes set. If you need the full object you can always load it fully from the database by calling crumb.full_subject
.
Note: Be careful using this. The tradeoff is bloat. You will get fewer Redis keys per megabyte. An :except option is available instead of :only but its use is not advised.
Similarly to attribute storage above, you can store properties of the creator
and target
on the crumb to avoid having to load them from the database. These attributes can only be set globally in the initialization file. Since these objects can differ wildly from model to model this only works when they share some common attributes.
For example a photo might be created by a User
or an event by a UserGroup
. If both objects had :id
and :name
attributes, for example, you could store these.
The usual warnings apply. However, by combining this with attribute storage it's possible to return multiple activity feeds without touching the primary datastore!
- Key mortality works.
- No longer adding
creator
according to initializer options. - More robust Redis assignment.
- Support for Redis Namespaces.
- Major refactor.
- Decent test coverage.
## Compatibility Tested against:
- Ruby 1.9.3 to 2.1.0
- ActiveRecord 3.1 to 4.1
Allow swapping out the backend to mongo or other key value stores.
Running tests requires a redis server to be running on the local machine with access over port 6379.
Run tests with bundle exec rspec
.
Created by John Hope (@midhir) for Project Zebra (@projectzebra) (c) 2014. Released under MIT License (http://www.opensource.org/licenses/mit-license.php).