DEV Community

Sankalp Kulshreshtha
Sankalp Kulshreshtha

Posted on • Updated on

How to Create A Flexible, Performant Audit Trail In Ruby on Rails

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!")
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
module Types
  class DeletePostEvent < Types::BaseObject
    description 'Tracks when a user deletes a post'

    implements Types::TrackedEvent
  end
end
Enter fullscreen mode Exit fullscreen mode

Now you can easily and flexibly add audit trail information to your app. Happy coding! πŸ˜€

Top comments (0)