#406 Public Activity
Feb 13, 2013 | 10 minutes | Active Record, Plugins
Learn how to easily add a user activity feed using the public_activity gem. Here I show both the default setup using model callbacks and a manual way to trigger activities.
- Download:
- source code
- mp4
- m4v
- webm
- ogv
Great episode thank you but I have a question about listing activities. Why are some activities duplicated like update?
If you edit the object multiple times, then multiple Activities are created each time.
+1
Public_activity looks like a great gem but we had some trouble with this approach in production. When tracking a number of changes on a number of models it bumped writes to the database quite substantially. Then when drawing back the associated objects on multiple activities there's a performance hit again.
We build our own solution for this called Redcrumbs.
It works in a very similar way but uses Redis as a backend. The benefits include superfast reads and writes but also Redis has an option to simply expire old keys, similar to memcached.
It also allows common attributes to be cached on the activity object, so that in 90% of cases the associated object doesn't have to be drawn from the primary database.
We've been using it in production to provide news feeds in our games for at least 6 months without a hitch.
Public_activity looks great for smaller sites though and there's an argument for keeping everything in the main database and on the same database structure.
You make a good point about performance and storage. Redis is a good idea as backend - which I will look into.
No sweat, let me know if I can help!
Yeah redis is incredibly fast and fits pretty well in storing non critical data like user activities. I have already read some of the documentation and played a bit with it in development but haven't yet used it in a production environment. Could you recommend some resources where i can learn more about Redis? Which path did you follow?
Would you like to discuss this further on Github Issues? This is really interesting, but querying for Activities with Redis requires a whole different process.
I love the idea of using redis as the backend too!
I was just about to ask Ryan if he could do a cast on how to achieve this from scratch... with something like Redis :)
Could you write a blog post about implementing a simple activity tracker from scratch Jon? Interested to see how you approach it.
+1 This is a good idea :) Thanks for your sharing.
Yep, using activerecord/sql-db will definitely be an issue. I wonder whether Ryan is planning on doing a "from scratch" implementation using redis. I will check out your gem.
Redcrumbs looks good... I tried it on one of my app and while overall it works good, BUT It relies on callback methods.. So lets say I have a post (created by User B) which a moderator (User A) edits... The creator of that crumb would still be the original post owner(User B) and not the moderator(User A)...
How to make it reference the moderator as the creator of the crumb/change since he was the one to edit the post?
As Ryan rightly pointed out, using callbacks becomes messy...
Used similar gem for my app called 'timeline_fu'. In rails 3.2 found working only this one - github.com/crftr/timeline_fu.git
Looks like the gem provides a lot of features out of the box but I think for any serious social network app, activity feeds are quite important so my personal preference is to build this something from scratch and that is what we have done in MeroCampus.com
But for simpler app it is the way to go. Thanks.
How does this handle fetching the associated models? This feels like an n+1 query to me, no?
All associations are polymorphic, so it is a hard topic. I personally cache whole pages of activities.
It's at least going to involve a select on every corresponding table included in the activities, there's really no way around that.
That's why I suggest saving an arbitrary hash of attributes on the activity object itself. 9 times out of 10 you only want, say, the ID and Name of the associated object in order to generate a link to it.
Exactly, that is the main reason
params
exists, to cache important attributes of associated objects. See http://rubydoc.info/gems/public_activity/PublicActivity/Tracked/ClassMethods#tracked-instance_methodGreat gem.
Params seem very helpful. From the controller, is it possible to filter public activity by params (for example, storing the recipe in the params and only showing public activity for a recipe on that recipe's page)?
I see in the episode that Ryan filtered directly by one of the attributes (owner_id):
I see that I can store the recipe_id from within the recipe controller similar to what was mentioned in the episode:
I see that it's possible to filter this way in the view (replacing "123" below with the particular recipe's id, but I'm assuming it's better to do this in the controller, yes?
Is there a way to track views? e.g., steve viewed ryan's lasagne recipe.
Create a new public activity in the show action?
Interesting timing, as I've been working on a couple different approaches for activity feeds this month. My current process goes like this:
By doing the latter, there are no lookups required for the trackable objects when generating your activity feed. Its super fast. Activity streams can be generated purely with the denormalized data, but we have the trackable references in case we need them. Also, you don't lose the data (post name, etc) when an object is deleted, and can instead defer handling of clickthroughs to deleted objects.
That's exactly what public_activity does, minus delaying. Also using delayed_job makes no sense, because it still has writes to the db, so you might as well write the activity. Better option is something redis based.
Interesting, as from watching this video it looked like the trackable objects had to be loaded.
The reason to use DJ is if you're writing the activity from a model/observer callback, to defer a few additional SQL calls required to populate the denormalized data. If you have everything you need at the time of writing, its unnecessary.
Ah yes, that makes sense.
+1 on that refactoring at 9:00, seems essential. I cringed seeing "controller" in that model, too!
+1
Yes, this is ugly, but completely optional. You can do creation manually using #create_activity and only use #tracked to set defaults, if for example you know you will always need to cache recipe's name.
Is there a way to set a column in the Activities table to attr_accessible? (no model?) I've added a custom field to query against but need it to be accessible? - Thanks!
Hi @iashraf. I'll prepare a wiki page for this and post the link here :)
https://github.com/pokonski/public_activity/wiki/%5BHow-to%5D-Use-custom-fields-on-Activity
In some cases, you can help the performance by pushing the assets to a different database. We have to keep detailed logs on a web application to track back changes made and can do this through having a beefier database server to handle the queries of the live logs.
Then in your special model:
To keep those pesky credentials from being in your application code.
If you want to reuse this connection in multiple models, you should create a new abstract class and inherit from it, because connections are tightly coupled to classes (as explained here, here, and here), and new connections will be created for each class.
If that is the case, set things up like so:
I had to so something like that too in my own app.
The simplest solution was to unload all the publishing of activities into a background job and cache the view of all activities. App response is around 150-200ms so I am happy. Activities are generally published within minutes so that is good enough.
I don't really like when a solution/gem outgrows the problem in complexity, plus callbacks are evil.
PS: I don't want to know how complex this would get if you also want comments on an activity to be published on the 'activity item'. So comment on 'A commented on picture of B' -> creates comment on picture of B, which is what I had to do in my app.
Good idea about delaying the activity creation!
I as an admin have 1200 friends. Delaying the activity creation is the only real option even if it only takes a few seconds.
+1
Hi - Does anyone know how to group the output of the activity list by date in the view? So it looks something like:
Thanks!
There's even a Railscast about it, though quite old, but still applies http://railscasts.com/episodes/29-group-by-month
Thanks Ryan for a great (as usual) screencast, and Piotrek for a nice looking gem.
My main problem with the polymorphic approach is that we loose information when models get destroyed. For instance, using the example in the screencast, I'd like to display the name of a recipe that got deleted.
This obviously means that the activity has to duplicate all the data it wants be able to display after model destruction.
Do you know of any work-around or non-polymorphic gem that does that?
Why not just mark the model as 'deleted' instead of actually destroying it?
+1
I really love this gem, It works just perfect for my current project(still in development) except sooner or later I am going to have to replace it with alternative or write from scratch as I hit the performance issue:(
Hi!
Can you tell what performance issues you have?
I've been using Streama with Mongoid, which has a nice DSL for logging activity stream and specify cached attributes. https://github.com/christospappas/streama The only thing missing is auto expire old activities.
Might be a basic Rails question but:
In
@activities = PublicActivity::Activity.order("created_at desc").where(owner_id: current_user.friend_ids, owner_type: "User")
where did
current_user.friend_ids
come from?friend_id is an attribute of Friendship, not User, right? And you can just pluralize an attribute to get an array?
friend_id comes from the sample "Recipe Application" which has "friends".
Ryan, what do you think of audited gem - https://github.com/collectiveidea/audited. This seems to be another solution to provide similar functional.
This is more of a papertrail, not activity tracking.
Yes, but still...
Does anyone know how to group similar activities and shows as one? at the same time that others activities shows normally, Just like these:
Jhon removed a post
Jhon updated Post 1
Jhon created 3 posts:
-Post 1
-Post 2
-Post 3
PD: Thanks for the gem, so useful!
Did you figure out how to do this?
Did you manage to implement this?
Group the activities by User and Action.
Is it possible to import past activities? My app already has a lot of data generated by the User. Curious if there is a migration or some sort of import that exists with this for implementing it into an already existing app with a lot of data.
Did I miss something or is there a reason why you can't simply use the model's user for the owner?
In the comment model isn't the comment.user association the owner? Does the gem allow you to simply say
or the like? Seems like accessing the controller is over-kill in most cases.
You can very much do this. We have a symbol syntax for this:
this will execute the
user
association on the tracked object.so why on earth Ryan did all that work? seriously...
It doesn't appear that
include PublicActivity::Common
works in the current release of the gem, so not sure how this appeared to work in the episode without using a pre-release version of the gem.See https://github.com/pokonski/public_activity/issues/62 for workarounds.
Just to reply, it works for a couple of months now on stable :)
Is there any way to only call activities which have an owner in the controller as opposed to the view?
Not sure what you mean. Do you want to find activities with a specific owner?
is there a way to use this with the acts_as_commentable gem?
Yes, you can. You can freely extend the Activity model with additional columns and methods from other gems. See the Common examples section in README.
I'm creating a CRM app where I'd like to have a dashboard that displays a feed of customer notes that were added and when. Looks like this gem will be helpful. I can see it being a lot of hits to the db if activity is heavy but I think it's worth a try.
Thanks for another great Railscast!
Obviously don't forget about fragment caching. This will save a lot of time on rendering and database hits.
Is there a way to not show a feed like "Ryan deleted a question" (Can't show the question title anyway because the record was destroyed) ? I tried doing this by checking if activity.trackable is false but it didn't work.
You can write JOIN clauses that filter the activities which do not have the
trackable
association. This is more of a database question, than Ruby.You can still display attributes this way:
params
hash to cache attributes you want to display even when tracked object is removedremoved
to true (more refactoring required)Does anyone know how to get this working with nested routes? I'm having a lot of issues in my link_to code
Don't forget
.includes(:owner, :trackable)
Thanks Ray Zane.
Calling
@comment.create_activity :destroy, owner: current_user
afterif @comment.destroy
gives the following error:You cannot call create unless the parent is saved
.Any ideas?
@coredevs did you find an answer for this question? I ran into the same issue. Thanks =)
Yep, it was due to calling the create_activity method after the object had been destroyed.
According to the gem maintainers, you simply have to assume the record will be destroyed, and call create_activity before the destroy
it's possible add more than one trackable to one activity?
i.e if i want something like [one user] add [other user] in [one group]
or [one user] add [user1 ... userx] in [one group]
You can only set one the recipient and owner on the actual column. Any other dat you want to store could be added in params:
@other_user.create_activity key: 'user.added_to_group', owner: current_user, params: { group_id: group.id }
You could also store multiple users_ids in an array in params to denote which users were added.
{ group_id: group.id, user_ids: [user_ids] })
How can use the gem with ransack ?
At trying to build a search_form_for with the
owner_name_cont
, I keep getting the error:uninitialized constant PublicActivity::ORM::ActiveRecord::Activity::Owner
This doesn't work.
when i rake db:migrate, it does not work, nothing happens
Can I use render_activities(@activities) in rabl to display json content, otherwise any approach of rendering activities in json format
I configured my model with tracke like the following ..
tracked owner: ->(controller, model) { controller && controller.current_user}
and when I try to update my model from admin section (I am using active admin) , it does not update current user as owner in the database .
I found the issues is controller is having nil value. how to fix this issue
have you found solution to this? can you send how you integrted it with active admin? thanks!
Having conflicts between current controllers and active admin controllers.. when I sing in both admin section and normal sections, public activity is taking normal section current user though I updated through admin section.
How to fix this
how can I use will_paginate with ajax for this.