Skip to content

feat(audit_trail): add audit trail plugin gem#320

Open
bexchauveto wants to merge 6 commits into
mainfrom
feat/audit-trail-plugin
Open

feat(audit_trail): add audit trail plugin gem#320
bexchauveto wants to merge 6 commits into
mainfrom
feat/audit-trail-plugin

Conversation

@bexchauveto

@bexchauveto bexchauveto commented Jun 18, 2026

Copy link
Copy Markdown
Member

What

Adds forest_admin_audit_trail, a new datasource-agnostic plugin gem that captures who changed what (before/after diff) for every change Forest performs through its data layer, persisting it to a pluggable store. This is the Ruby counterpart of the audit trail plugin being released as an npm package on the Node agent v2.

Why

The audit trail is shipped as a separate package (not folded into forest_admin_datasource_customizer) because it carries its own heavy dependencies (activerecord, activesupport, zeitwerk) that shouldn't be forced on users running non-Rails datasources. This keeps parity with the Node decision to ship it as its own npm package.

Contents

New gem — packages/forest_admin_audit_trail

  • Plugin — registers hooks on collections to capture changes
  • Diff — computes before/after field-level diffs
  • Stores: InMemoryStore, LogStore, SqlStore (+ SQL migrator / connection base)
  • Full rspec coverage

Agent / Rails wiring

  • CorrelationId + CorrelationIdMiddleware — one id per request, propagated to the caller as request_id and echoed back to the client via a response header (mirrors the Node correlationIdMiddleware)
  • Record-history route /_audit-trail/:collection/:id, reading from a configurable store (ForestAdminRails.audit_trail setting)
  • Caller gains request_id to correlate operations triggered by a single request

Release plumbing

  • Registered in .releaserc.js (version stamping, gem build && gem push, git asset)
  • version.rb excluded from rubocop Style/MutableConstant and Style/StringLiterals, matching every other package (semantic-release rewrites it with double quotes / no .freeze at release time)
  • gemspec MFA requirement set to false, consistent with sibling gems

Notes

  • version.rb carries 1.33.1 (current monorepo version); semantic-release re-stamps it to the next release version on merge.

🤖 Generated with Claude Code

Note

Add audit trail plugin gem for capturing and querying create/update/delete history

  • Introduces the forest_admin_audit_trail gem with a Plugin class that hooks into Forest Admin datasource customizer lifecycle events (create/update/delete) to emit structured AuditRecord entries with field diffs, user identity, and correlation keys.
  • Adds three store backends: SqlStore (persistent, with auto-migration), InMemoryStore (ephemeral, for testing), and LogStore (write-only, JSON to logger/stderr).
  • Adds GET /_audit-trail/:collection/:id and GET/POST /_audit-trail/correlations agent routes, gated on a configured store, with pagination, timezone-aware date filters, user id filters, and read permission checks.
  • Adds per-request correlation id tracking via CorrelationIdMiddleware and CorrelationId, exposing x-forest-correlation-id in response headers and wiring the id into Caller#request_id for audit attribution.
  • Extends ForestAdminRails::Engine to install the correlation id middleware and expose the header via CORS during agent setup.
  • Risk: The SQL migrator acquires a Postgres advisory lock and creates schema/tables at runtime on first use; misconfigured DB credentials or permissions will raise at request time rather than boot time.

Macroscope summarized 5af078c.

Introduce the forest_admin_audit_trail package: a datasource-agnostic
plugin that captures who changed what (before/after diff) for every
change Forest performs through its data layer, with pluggable storage
(in-memory, log, SQL).

Supporting agent/rails wiring:
- correlation id generated per request, propagated to the caller as
  request_id and echoed back via response header (CorrelationIdMiddleware)
- record-history route (/_audit-trail/:collection/:id) reading from a
  configurable store
- register the gem in the semantic-release pipeline (.releaserc.js) and
  exclude its version.rb from rubocop, matching the other packages

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@qltysh

qltysh Bot commented Jun 18, 2026

Copy link
Copy Markdown

Qlty


⚠️ Comments skipped @bexchauveto doesn't have a Qlty seat in ForestAdmin.

Qlty doesn't post analysis or coverage comments for contributors without a seat. An authorized user can grant @bexchauveto a seat from this pull request's page in Qlty.

Matches the other packages, which disable MFA and are excluded from the cop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread packages/forest_admin_audit_trail/lib/forest_admin_audit_trail/sql/migrator.rb Outdated
bexchauveto and others added 4 commits June 18, 2026 17:55
Add GET /_audit-trail/correlation/:correlation_key (one key) and the batch
GET/POST /_audit-trail/correlations (correlationKeys list, POST body to dodge
URL limits), both scoped to a record via collection/recordId params, sharing
the per-record auth and store gate. Back them with list_by_correlation /
list_by_correlations on the stores (SQL + in-memory + log no-op).

Register the correlation source before audit_trail so /_audit-trail/correlation
matches it instead of the per-record /_audit-trail/:collection_name/:id (Rails
matches in definition order).

Mirror the Node README: Rails configuration process plus docs for the
record-history, correlation and batch correlation routes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Multiple SqlStore instances mutated the shared Sql::AuditLog.table_name,
so stores against different Postgres schemas clobbered each other. Make
AuditLog an abstract template and build a per-instance concrete subclass
bound to the store's own qualified table.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Without the Postgres advisory lock, two instances booting at once can both
see a migration as pending and both run its create_table/add_index, crashing
the loser with "already exists". Add if_not_exists: true to the table and
index DDL.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
An empty-string schema made schema? true, producing invalid identifiers
like ".audit_migrations". Use present? so nil, "" and whitespace all mean
no schema qualification.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant