Auditing (Custom-Action Audit Metadata)
Overviewâ
cm-admin extends PaperTrail to record two first-class audit dimensions on every version record:
action_typeâ the category of operation (default,custom,bulk_action, ortab)action_nameâ the specific action (create,update,destroy, or the name of a custom action likeapprove)
These are stored as queryable columns on the PaperTrail versions table, so you can filter and report on audit history by what kind of action triggered each change â not just who did it (whodunnit) and what changed (object / object_changes).
Best of all, developers keep writing bare has_paper_trail â the engine auto-injects the metadata configuration. No per-model setup is required.
Quick Startâ
1. Add the columns to your versions tableâ
Run the generator and migrate:
rails g cm_admin:add_audit_metadata_to_versions
rails db:migrate
The generator creates a migration that adds action_type and action_name (both string) to versions, plus indexes on each for fast filtering:
class AddAuditMetadataToVersions < ActiveRecord::Migration[8.1]
def change
add_column :versions, :action_type, :string
add_index :versions, :action_type
add_column :versions, :action_name, :string
add_index :versions, :action_name
end
end
If only one of the columns is missing, the generator only adds the missing one.
2. That's itâ
Any model that already declares has_paper_trail will now automatically record action_type and action_name on each version â no changes to the model are needed.
How It Worksâ
CmCurrent(cm-admin'sActiveSupport::CurrentAttributesstore) gains two attributes:action_typeandaction_name.CmAdmin::ResourceControllersets them at the start of each mutating request:cm_createâaction_type: 'default',action_name: 'create'cm_updateâaction_type: 'default',action_name: 'update'cm_destroyâaction_type: 'default',action_name: 'destroy'cm_custom_methodâaction_type: 'custom',action_name: <the custom action's name>cm_bulk_actionâaction_type: 'bulk_action',action_name: <the bulk action's name>They are reset tonilat the end of the request.
has_paper_trailis auto-wrapped by the engine. When called on a model, it injects:but only if themeta: {
action_type: ->(_record) { CmCurrent.action_type },
action_name: ->(_record) { CmCurrent.action_name }
}versionstable actually has those columns (so apps that haven't run the generator are not broken).
Querying Audit Historyâ
Because action_type and action_name are real columns, you can filter and aggregate efficiently:
# All versions created by custom actions
record.versions.where(action_type: 'custom')
# All versions of a specific custom action across all records
PaperTrail::Version.where(action_type: 'custom', action_name: 'approve')
# Every update made through the standard edit form
record.versions.where(action_type: 'default', action_name: 'update')
# All bulk action mutations
record.versions.where(action_type: 'bulk_action')
# Audit history for a single custom action
record.versions.where(action_name: 'approve').order(created_at: :desc)
History Page Displayâ
The cm-admin history tab automatically renders the audit metadata. For standard CRUD operations it shows the usual text (e.g. "Jane updated the Demo Form 2 minutes ago"). For custom and bulk actions it appends via the {display name} {action type}:
Jane updated the Demo Form via the Approve Custom Action 2 minutes ago John updated the Demo Form via the Approve Bulk Action 5 minutes ago
The display name is resolved from the cm-admin action's display_name (falling
back to the action name humanized), so it matches what users see on buttons and
modals â no extra configuration needed. The action type label distinguishes
whether the change was made through a custom action or a bulk action.
Values Referenceâ
action_type | action_name | When |
|---|---|---|
default | create | Record created via cm-admin new/create |
default | update | Record updated via cm-admin edit/update |
default | destroy | Record destroyed via cm-admin destroy |
custom | <action name> | A cm-admin custom_action mutated the record |
bulk_action | <action name> | A cm-admin bulk_action mutated the record |
nil | nil | Change made outside a cm-admin request (e.g. background job, console, seeds) |
Overriding / Extendingâ
If a model supplies its own meta: for action_type or action_name, cm-admin respects it and does not clobber it:
class Order < ApplicationRecord
has_paper_trail meta: { action_type: ->(_) { 'order_specific' } }
end
# Order's `action_type` will be 'order_specific', not 'default'/'custom'/'bulk_action'.
# `action_name` is still auto-injected from CmCurrent.
To opt out entirely for a model, pass both keys:
has_paper_trail meta: {
action_type: ->(_) { nil },
action_name: ->(_) { nil }
}
Requirementsâ
- The
paper_trailgem must be installed andhas_paper_traildeclared on the model. - The
versionstable must haveaction_typeandaction_namecolumns (added by the generator above). Current.usermust be set in the request lifecycle (already required by cm-admin forwhodunnitandTrackable).
Notesâ
- Historical versions: Existing
versionsrows will haveNULLforaction_type/action_name. Only versions created after the migration is run get populated. - Background jobs:
CmCurrentis request-scoped, so changes made in background jobs recordNULLfor both fields â which is correct, since no HTTP action triggered them. - The
has_paper_trailwrapper is global: it affects everyActiveRecordmodel in the host app, not just cm-admin DSL models. This is intentional and harmless â models without theversionscolumns simply skip meta injection, and developer-suppliedmeta:is always respected.