waseet/filament-activity-log
A Filament plugin that adds an activity log to your admin panel, automatically tracking model changes and user actions with a searchable timeline, diff views, and per-record history so you can audit what changed, when, and by whom. Built on spatie/laravel-activitylog.
Prerequisites. This is a thin Filament layer over spatie/laravel-activitylog, so install the underlying package first and get it working before adding the UI. You want the activity_log table and the LogsActivity trait functioning, otherwise the Filament page will render an empty table with no obvious error.
composer require spatie/laravel-activitylog
php artisan vendor:publish --provider="Spatie\Activitylog\ActivitylogServiceProvider" --tag="activitylog-migrations"
php artisan migrate
composer require waseet/filament-activity-log
Where to look first. Open the package's src/ directory and its service provider — this package is small (72 stars, single-purpose), so the plugin class, page/resource, and any config file are the whole surface area. Check the README for the exact plugin class name and whether it targets Filament v3 or v4; the composer.json constraints tell you immediately whether it will even boot on your panel.
Register the plugin in your panel provider (app/Providers/Filament/AdminPanelProvider.php):
public function panel(Panel $panel): Panel
{
return $panel
// ...
->plugin(\Waseet\FilamentActivityLog\ActivityLogPlugin::make());
}
Confirm the namespace/class from src/ — the vendor name (waseet) and repo owner (noxoua) differ, so don't guess.
First use case. Pick one high-value model — Order, Invoice, User — add Spatie's trait, and browse its history in the panel:
use Spatie\Activitylog\Traits\LogsActivity;
use Spatie\Activitylog\LogOptions;
class Order extends Model
{
use LogsActivity;
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()
->logOnly(['status', 'total', 'assigned_to'])
->logOnlyDirty()
->dontSubmitEmptyLogs()
->useLogName('order');
}
}
Change a field in the admin UI, then open the activity page. That end-to-end loop (trait → write → Filament table) is the fastest way to validate the install.
Segment with log names. useLogName('order') is the single most useful convention. It gives you a stable filter value in the Filament table and stops your audit view from becoming an undifferentiated firehose of every model in the app. Adopt a naming scheme early (order, user, billing, auth) and keep it consistent.
Manual business events. The trait only covers CRUD. Log domain events explicitly — this is usually where the real audit value lives:
activity('order')
->performedOn($order)
->causedBy(auth()->user())
->withProperties(['reason' => $reason, 'previous_status' => $old])
->event('refunded')
->log('Order refunded');
Per-record timeline. The most-requested feature from stakeholders is "show me the history of this record" rather than a global log. Embed an activity table on the resource's view/edit page — either through a relation manager on the subject model or by adding a table to a custom page that scopes to subject_type + subject_id. This is where users actually consume the log; the global page is mostly for auditors and support.
Causer discipline. Spatie resolves the causer from auth()->user() by default. That breaks in queued jobs, console commands, seeders, and impersonation. Set it explicitly:
activity()->causedBy($user)->performedOn($order)->log('...');
For impersonation, consider logging both the impersonator and the impersonated user in properties — otherwise support staff actions look like customer actions in the audit trail.
Authorization. A global activity log is sensitive. Gate the page: implement canViewAny() or wire it to a permission (Spatie Permission, Filament Shield). By default Filament pages are visible to any panel user who can reach the panel — do not ship the log page to a customer-facing panel without a policy.
Multi-tenancy. If you use tenancy, add a tenant_id to activity_log (custom migration) and populate it via a tapActivity() hook on your models, then scope the Filament query. The package won't do this for you, and retrofitting it after the table has millions of rows is painful.
Enrichment with tapActivity. Add IP, user agent, request ID, or source channel to every activity row:
protected function tapActivity(Activity $activity, string $eventName): void
{
$activity->properties = $activity->properties->merge([
'ip' => request()->ip(),
'source' => app()->runningInConsole() ? 'console' : 'web',
]);
}
Retention. Schedule pruning from day one:
// routes/console.php or Kernel
Schedule::command('activitylog:clean')->daily();
Configure config/activitylog.php (delete_records_older_than_days) and, for regulated environments, don't prune — archive to cold storage instead.
Testing. There's no fake for this; assert against the table:
$this->assertDatabaseHas('activity_log', [
'log_name' => 'order',
'event' => 'updated',
'subject_id' => $order->id,
]);
Wrap noisy setup in Activity::disableLogging() / Activity::enableLogging() if your seeders or factories bloat the table during tests.
activity_log row count first.logOnlyDirty + dontSubmitEmptyLogs are non-negotiable for day-to-day sanity. Without them, every save() produces a row, including no-op saves from Filament forms that re-submit unchanged fields.logOnly() as an allowlist rather than logAll() as a denylist — a denylist fails silently the moment someone adds a sensitive column. Also check $hidden interactions if you log full attribute arrays.null and render a fallback.activity_log is append-only and grows fast. Ensure indexes on subject_type/subject_id, causer_type/causer_id, log_name, and created_at — Spatie's default migration covers some but not necessarily the composite you filter on in Filament. Paginate and add date-range filters rather than letting users load "all".null in queues. This is the most common audit defect. If a job modifies data, the log row will have no causer unless you pass one through the job payload.created/updated/deleted events blur together with custom events (approved, refunded). Standardize on a small vocabulary and document it — filters and reports rely on exact strings.created_at in the app timezone; if your DB stores UTC and users think in local time, audit queries will be off by hours in exactly the situations where they matter (incident review).composer.json constraints and last release date (2025-05-11) against your Filament version before committing. Budget for the possibility of forking — the surface area is small enough that a fork is cheap.vendor/. If the package doesn't expose a publishable view, a small custom Filament page querying Spatie\Activitylog\Models\Activity directly is often faster than fighting the abstraction.How can I help you explore Laravel packages today?