Who did that? What did they update? When did they update it?
You often want to track what user's are doing throughout your app. There are some gems that help you do this. For example, activerecord_activity_tracker or public_activity. However, gems like these sometimes utilize polymorphic relationships, which don't scale well, or they're very opinionated about the setup. When your requirements differ from the default setup, it can become cumbersome to implement.
I've landed on a pretty flexible way to store audit trail data in your Rails application. Toward the end of this post, I'll also show you how this can integrate nicely with Graphql so that the data is accessible via your API.
Let's get started!
First, we'll create a model called TrackedEvent
. This will store the information we want to track along with the associated user who did the action. Here's what the schema looks like:
create_table "tracked_events", force: :cascade do |t|
t.bigint "user_id"
t.string "type"
t.jsonb "metadata"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
What's interesting to see here is the JSON metadata
field and the user foreign key for the User
. The metadata
field can eventually store any additional fields you find relevant. You can potentially leave the user_id
field optional because sometimes the event may not be user generated.
Our model looks like the following:
# app/models/tracked_event.rb
class TrackedEvent < ApplicationRecord
scope :reverse_chronological, -> { order(created_at: :desc) }
end
Now, imagine we have a Blog
and we want to track any time a user updates a Post
. In order to track the update action, we can use a new class which inherits from the TrackedEvent
class:
# app/models/update_post_event.rb
class UpdatePostEvent < TrackedEvent
jsonb_accessor :metadata,
from: :string,
to: :string
validates_presence_of :from, :to
end
We use the nifty JSONb Accessor gem to create accessor methods for the relevant fields. You can also use anything in ActiveModel including validations. This takes advantage of ActiveRecord's single table inheritance features where the model name, in this case update_post
is stored in the type
field in TrackedEvent.
Now, we can easily create a record by calling:
UpdatePostEvent.create!(user: user, from: "Hello", to: "Hello World!")
In order to add a new type of event, for example DeletePostEvent
you can add a new class which inherits from the TrackedEvent
class.
# app/models/delete_post_event.rb
class DeletePostEvent < TrackedEvent
end
So, now that you're able to easily create events you may want to access them via GraphQL. It's a good idea to create a new interface to store common fields.
module Types::TrackedEvent
include Types::BaseInterface
description 'Interface to track events in the app'
field :id, ID, null: false
field :user, UserType, null: true
field :created_at, Types::ISO8601Date, 'When this event occurred', null: false
def self.resolve_type(object, context)
case object.type
when 'UpdatePostEvent'
Types::UpdatePostEvent
when 'DeletePostEvent'
Types::DeletePostEvent
else
raise "Unexpected tracked event #{object.type}"
end
end
end
You'll be able to add additional types in Graphql easily:
module Types
class UpdatePostEvent < Types::BaseObject
description 'Tracks when a user updates a post'
implements Types::TrackedEvent
field :from, String, 'Original content', null: false
field :to, String, 'Changed to this new content', null: false
end
end
module Types
class DeletePostEvent < Types::BaseObject
description 'Tracks when a user deletes a post'
implements Types::TrackedEvent
end
end
Now you can easily and flexibly add audit trail information to your app. Happy coding! 😀
Top comments (0)