How to Create A Flexible, Performant Audit Trail In Ruby on Rails
Tags: rails, graphql
Published: 2021-04-08 09:35:10

> 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](https://github.com/chaps-io/public_activity) or [public_activity](https://github.com/chaps-io/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:
```ruby
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:
```ruby
# 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:
```ruby
# 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](https://github.com/madeintandem/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](https://api.rubyonrails.org/classes/ActiveRecord/Inheritance.html) 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:
```ruby
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.
```ruby
# 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](https://graphql-ruby.org/type_definitions/interfaces) to store common fields.
```ruby
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:
```ruby
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
```
```ruby
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! 😀