Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Filament Activity Log Laravel Package

pxlrbt/filament-activity-log

A Filament plugin that adds a polished activity log viewer to your admin panel. Browse, filter, and inspect model changes, user actions, and audit trails powered by spatie/laravel-activitylog, complete with timeline views and per-record change histories.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install the substrate and the UI layer together. This package is not a logger; it is a viewer for spatie/laravel-activitylog. You need both, and the versions are locked to each other:

composer require spatie/laravel-activitylog pxlrbt/filament-activity-log
Package Filament spatie/laravel-activitylog
^1.x v3 v4
^2.x (2.1+) v4 / v5 v4
^3.x (3.1.2 latest) v5 v5

Pin explicitly in composer.json rather than letting the resolver pick — the v2 → v3 jump is a substrate upgrade, not a cosmetic one.

2. Publish both sets of config/migrations. The activity table belongs to Spatie; only the UI config belongs to this package:

php artisan vendor:publish --provider="Spatie\Activitylog\ActivitylogServiceProvider" --tag="activitylog-migrations"
php artisan vendor:publish --tag="filament-activity-log-config"
php artisan migrate

3. Register the plugin on the panel in app/Providers/Filament/AdminPanelProvider.php:

use Pxlrbt\FilamentActivityLog\FilamentActivityLogPlugin;

public function panel(Panel $panel): Panel
{
    return $panel
        ->plugin(
            FilamentActivityLogPlugin::make()
                ->navigationGroup('System')
                ->navigationLabel('Activity Log')
        );
}

That alone gives you a filterable, searchable timeline page in the sidebar with no custom views. Check the published config file first — it is the authoritative list of extension points (activity_model, datetime formatting, navigation, table behaviour) and it changes between majors.

4. Audit your first model. Nothing appears in the log until a model uses Spatie's trait:

use Spatie\Activitylog\LogOptions;
use Spatie\Activitylog\Traits\LogsActivity;

class Invoice extends Model
{
    use LogsActivity;

    public function getActivitylogOptions(): LogOptions
    {
        return LogOptions::defaults()
            ->logOnly(['status', 'total', 'paid_at', 'assigned_to'])
            ->logOnlyDirty()
            ->dontSubmitEmptyLogs()
            ->useLogName('invoice');
    }
}

First use case to ship: pick the one model where "who changed this and what did it look like before?" is already a support ticket — usually User (roles/permissions), Invoice/Payment (amounts, status), or anything soft-deletable. Change a field in the UI, open the Activity Log page, and confirm the old/new diff renders before you roll the trait out to ten more models.

Implementation Patterns

Model-level logging policy is the real configuration surface. The page is generic; the value comes from what you choose to log. Keep it tight per model:

public function getActivitylogOptions(): LogOptions
{
    return LogOptions::defaults()
        ->logFillable()                  // broad but predictable
        ->logExcept(['last_login_at', 'remember_token', 'password'])
        ->logOnlyDirty()
        ->dontSubmitEmptyLogs()
        ->setDescriptionForEvent(fn (string $event) => match ($event) {
            'created' => 'Invoice created',
            'updated' => 'Invoice updated',
            'deleted' => 'Invoice deleted',
            default   => $event,
        });
}

logOnlyDirty() + dontSubmitEmptyLogs() is the difference between a readable timeline and a wall of no-op rows. Without them, Filament's "save" on an untouched form still writes an activity entry.

Embedding the timeline on a resource view page. Rather than sending admins to the global page and re-filtering, put the per-record log next to the record. v1.1.3 explicitly guarantees the fields needed for both the table and the restore action exist, so the embedded component and the global page stay in sync. Add it as a custom resource page/relation-manager slot on ViewInvoice, then link from the record. The global page stays for cross-record forensics.

Causer-scoped investigation page (v3.1.0). ListActivitiesByCauser is the operationally valuable addition and the one to wire into your UserResource: "everything this account did" in one screen. It maps directly onto impersonation triage, offboarding reviews, and support escalations where a customer says "an admin changed my account." Register it as a resource page bound to the user record so the causer is implied by the URL, not typed into a filter.

Manual logging for non-Eloquent events. Eloquent events cover maybe half of what audit reviewers ask about. Exports, impersonation, impersonation end, bulk imports, and MFA resets are not model updates:

activity('security')
    ->causedBy(auth()->user())
    ->performedOn($targetUser)
    ->withProperties(['impersonated_by' => auth()->id(), 'ip' => request()->ip()])
    ->log('impersonation started');

Because performedOn() sets subject_type/subject_id, these entries show up in the same subject-filtered view as model changes — that is the payoff for using one substrate instead of a second bespoke table.

Restore is a product decision, authorize it separately. v1.1.0 extracted restore authorization into a dedicated hook precisely because "may view audit history" and "may rewind this record" are different permissions. Decide consciously: compliance officers, support agents, and engineers should not all get rewind rights. Gate restore on a policy ability on the subject model (or the activity), and leave the audit trail read-only for everyone else. Also decide whether restored changes are themselves logged — if restore writes a new activity row, your timeline stays honest; if not, you have an unaudited mutation path.

Custom activity model for presentation. Point the config at a subclass of Spatie\Activitylog\Models\Activity when you need appended attributes (e.g. a human-readable subject_label, or a resolved causer name fallback) so the table shows friendly text instead of morph FQCNs. Do this via the config, not by editing vendor views.

i18n and formatting are free — use them. The package ships ~15 locales (v3.1.2 added Slovak) and locale-aware datetime defaults (French formatting landed in v1.1.4). Set the panel locale and the config's datetime format instead of hardcoding dates in custom columns.

Gotchas and Tips

Morph maps will break your subject filter. If you call Relation::enforceMorphMap(), new activities store subject_type = 'invoice' instead of the FQCN. Existing rows keep the old FQCN. Result: your subject-type filter dropdown lists one and the table renders the other, and half the history looks unresolvable. Enforce the morph map before you start logging, or write a one-off migration to rewrite subject_type/causer_type on historical rows.

Multi-guard panels silently produce null causers. Spatie resolves the causer from the default auth user at log time. If your panel authenticates against an admin guard that isn't the default, every row gets causer_id = null and the causer filter and ListActivitiesByCauser page become useless. Fix by calling Auth::shouldUse('admin') in panel middleware, or by always passing ->causedBy(auth()->user()) explicitly on manual logs. Verify with one real edit before trusting the log.

Restore is an attribute rewind, not an undelete. It replays old values onto the subject. It is not a substitute for soft deletes, it doesn't resurrect relations, and it can trample concurrent edits — the "old" values in an activity are a snapshot from whenever that row was written, which might be many edits ago. Guard the action and pair it with soft deletes for anything destructive.

Arrays, booleans, and relations have all had rendering defects. v0.1.3 added array support and required the model trait to be configured for it, v1.1.5 fixed relational attribute handling, and v1.1.10 fixed boolean display. If you're on an older 1.x, expect raw JSON blobs or 1/0 in the diff column. Stay on a current patch, and test a cast-array attribute and a boolean attribute on day one — these are the two shapes that break rendering.

The activities table grows without bound. Add composite indexes on (subject_type, subject_id), (causer_type, causer_id), and created_at, plus one on event. Then schedule Spatie's cleanup command (activitylog:clean) with a retention window that matches your actual compliance obligation — 90 days for operational debugging, longer only where a regulation names it. Note that pruning destroys the audit trail; if retention is a compliance requirement, archive to cold storage before deleting.

Never log secrets. logFillable() will happily capture an invite token or a partially-masked card field if it's fillable. Use logExcept([...]) as a belt-and-braces rule and keep password, remember_token, and API keys out of fillable arrays entirely. This is the single most common way an audit log becomes a liability.

Debugging empty timelines, in order: (1) does the model actually use LogsActivity — the package note from v0.1.3 still bites people; (2) does logOnly/logFillable include the attribute you changed; (3) is logOnlyDirty suppressing it because nothing actually changed; (4) is the causer null because of guards; (5) does the row exist in activity_log at all (select * from activity_log order by id desc limit 10). Check the DB before you suspect the UI.

Testing. Because activities are just rows, assert them directly rather than through Livewire: hit the Filament action, then assertDatabaseHas('activity_log', ['event' => 'updated', 'subject_id' => $invoice->id]). Truncate activity_log in test setup — leftovers from other tests will make subject/causer filters flaky and produce false-positive diffs.

Extension points, in order of preference: the model's getActivitylogOptions() (cheapest, per-model), the published config (activity_model, datetime, navigation), a Activity subclass for appended presentation attributes, and only then a custom resource page. Teams that start by overriding the page end up re-implementing filters the package already ships.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log
hsluv/hsluv
seampact/codegen
seampact/hooks
seampact/ir
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk