Low commit activity in last 3 years
An extension to PaperTrail that allows you to retrieve the ActiveRecord object (actor) that was responsible for creating or modifying records, enabling better audit trails and change attribution.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
 Dependencies

Development

Runtime

>= 11.0.0
 Project Readme

PaperTrail-Actor

Track changes in your Rails application with precision. PaperTrail-Actor extends PaperTrail to store full ActiveRecord objects (not just IDs) as the actor responsible for changes.

Problem it solves: When both Admin and User models can modify records, storing only an ID makes it impossible to know which model type made the change. PaperTrail-Actor stores GlobalIDs to uniquely identify actors across different model types.

Table of Contents

  • Features
  • Requirements
  • Installation
  • Usage
    • Basic Setup
    • Supported Object Types
    • Controller Integration
    • Migrating Existing Data
  • How It Works

Features

  • Store any ActiveRecord object as the actor who made changes
  • Retrieve the full object with a simple #actor method
  • Backward compatible with existing PaperTrail versions
  • Flexible - works with strings, unpersisted records, and non-ActiveRecord objects

Requirements

  • Ruby 2.7+
  • Rails 6.0+
  • PaperTrail 11.0+

Installation

  1. Add to your Gemfile:
gem "paper_trail-actor"
  1. Install the gem:
bundle install

Usage

Basic Setup

Set any ActiveRecord object as the actor responsible for changes:

admin = Admin.find(1)
PaperTrail.request.whodunnit = admin

# Now all changes will be attributed to this admin
# See Supported Object Types section below for detailed examples

Supported Object Types

The gem handles various object types gracefully:

Persisted ActiveRecord objects:

admin = Admin.find(1)
product = Product.find(42)

PaperTrail.request.whodunnit = admin
PaperTrail.request.whodunnit # => "gid://app/Admin/1"
PaperTrail.request.actor     # => #<Admin id: 1>

# Make changes - PaperTrail will track the actor
product.update(name: "Ice cream")
product.versions.last.whodunnit # => "gid://app/Admin/1"
product.versions.last.actor     # => #<Admin id: 1>

Strings:

PaperTrail.request.whodunnit = "Alex the admin"
PaperTrail.request.whodunnit # => "Alex the admin"
PaperTrail.request.actor     # => "Alex the admin"

New/unpersisted ActiveRecord objects:

PaperTrail.request.whodunnit = Admin.new(name: "New Admin")
# Cannot get a globalid for an unpersisted object. So we use the `#to_s` for the object.
PaperTrail.request.whodunnit # => "#<Admin:0x00007f8e8c0a0b80>"
PaperTrail.request.actor     # => "#<Admin:0x00007f8e8c0a0b80>"

Non-ActiveRecord objects:

class BackgroundJob
  def to_s
    "BackgroundJob-#{object_id}"
  end
end

PaperTrail.request.whodunnit = BackgroundJob.new
PaperTrail.request.whodunnit # => "BackgroundJob-12345"
PaperTrail.request.actor     # => "BackgroundJob-12345"

Controller Integration

Set the actor automatically in your controllers using PaperTrail's built-in callback:

class ApplicationController < ActionController::Base
  before_action :set_paper_trail_whodunnit
end

If your controller has a #current_user method, PaperTrail-Actor will automatically use it and attribute all changes within the request cycle to that user.

# In your CRUD actions
def update
  # Nothing else required here to attribute the user as the actor.
  @product.update(product_params)
  @product.versions.last.actor # => #<User id: 42> (full User object)
end

Before PaperTrail-Actor:

# whodunnit was just a string like "42"
version.whodunnit # => "42"
# You couldn't tell if this was User 42 or Admin 42

After PaperTrail-Actor:

# whodunnit stores the full GlobalID
version.whodunnit # => "gid://app/User/42"
version.actor     # => #<User id: 42>
# Now you know exactly who to attribute the change to

Override #user_for_paper_trail to customize the actor selection logic:

class ApplicationController < ActionController::Base
  before_action :set_paper_trail_whodunnit

  private

  def user_for_paper_trail
    logged_in? ? current_admin : "Public user"
  end
end

Migrating Existing Data

When to migrate: Install PaperTrail-Actor on an existing app with PaperTrail data.

How to migrate: Convert existing string IDs to GlobalIDs:

# Find a version with a plain ID
version = product.versions.first
# => #<PaperTrail::Version id: 1, whodunnit: "123">

# Convert to GlobalID
admin = Admin.find_by(id: version.whodunnit)
if admin.present?
  version.update!(whodunnit: admin)
  version.whodunnit # => "gid://app/Admin/123"
  version.actor     # => #<Admin id: 123>
end

How It Works

PaperTrail-Actor serializes ActiveRecord objects using GlobalID and stores them in PaperTrail's whodunnit column. This enables:

  • Type-safe actor identification: gid://app/Admin/1 vs gid://app/User/1
  • Automatic deserialization: Call #actor to get back the original ActiveRecord object
  • Graceful fallback: Non-ActiveRecord objects are stored as strings

For example, if you have both Admin and User models that can make changes, this gem ensures you can clearly identify who made the change instead of using only an ID without knowing the model type.