PHPackages                             elpandape/filament-activitylog - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. elpandape/filament-activitylog

ActiveLibrary

elpandape/filament-activitylog
==============================

An activity log panel for Filament, built on spatie/laravel-activitylog

v1.2.0(today)07↓25%MITPHPPHP ^8.5CI passing

Since Aug 16Pushed todayCompare

[ Source](https://github.com/elpandape/filament-activitylog)[ Packagist](https://packagist.org/packages/elpandape/filament-activitylog)[ Docs](https://github.com/elpandape/filament-activitylog)[ RSS](/packages/elpandape-filament-activitylog/feed)WikiDiscussions main Synced today

READMEChangelog (4)Dependencies (25)Versions (5)Used By (0)

filament-activitylog
====================

[](#filament-activitylog)

An activity log panel for [Filament](https://filamentphp.com), built on [spatie/laravel-activitylog](https://github.com/spatie/laravel-activitylog) v5.

Two screens and a drawer, none of which can write an entry — and two hooks that improve the entries somebody else writes.

- **A listing grouped by day.** Each entry reads as a sentence — "Nayra Condori changed the name of Amaru Quispe" — with the clock and the event beside it. The log, the record and the author are there too, switched off by default: the sentence already names them.
- **The entry itself.** What happened, who did it, and what changed field by field, with the old value and the new one side by side. Sensitive attributes say that they changed and never what to.
- **A record's history, in a drawer.** Hang `ActivityAction` on a table row or a page header and it tells that record's story without leaving the screen. It reads either as a list of rows or as one continuous thread; both are the same sentences.

What it adds to the rows themselves, both switchable:

- **Names are sealed into the row.** `subject_id` and `causer_id` are pointers, and a pointer to a deleted row answers nothing — so an entry keeps the names its parties had at that instant. Delete an administrator and their history still says who they were.
- **Secrets never reach the database.** A masked attribute is replaced before the row is written, so a model can log a password and the log will say it changed without saying to what.

Requirements
------------

[](#requirements)

PHP 8.5 · Laravel 13 · Filament 5.7 · spatie/laravel-activitylog 5.1

Installation
------------

[](#installation)

```
composer require elpandape/filament-activitylog
```

`activitylog` ships its migration as a stub and never loads it from `vendor`, so publish it if you have not already:

```
php artisan vendor:publish --tag=activitylog-migrations
php artisan migrate
```

Register the plugin on your panel:

```
use ElPandaPe\FilamentActivitylog\FilamentActivitylogPlugin;

$panel->plugin(FilamentActivitylogPlugin::make());
```

Publish what you want to change:

```
php artisan vendor:publish --tag=filament-activitylog-config
php artisan vendor:publish --tag=filament-activitylog-translations
php artisan vendor:publish --tag=filament-activitylog-views
```

There are no assets to publish. The package ships no stylesheet: every colour it paints comes from the panel's own palette variables, so it follows your theme in light and dark without a build step.

Authorization
-------------

[](#authorization)

The model is `Spatie\Activitylog\Models\Activity`, which lives in vendor: Laravel's policy guessing never finds a policy for it, so the consuming application must register one.

```
Gate::policy(Activity::class, ActivityPolicy::class);
```

Declare `viewAny` and `view` and nothing else. Under Filament's `->strictAuthorization()` an absent method is not a denied permission but an impossible action — which is exactly what keeps an audit trail from being created, edited or deleted from a screen. Entries leave by retention, with `activitylog:clean`.

The package takes no stance on how that policy decides; pair it with whatever authorization layer the application already uses. The drawer asks the same gate: somebody who may see your users but not the log is not offered it. If you set `activitylog.activity_model` to a model of your own, register the policy against that class — the resource and the drawer both follow that configuration.

The drawer
----------

[](#the-drawer)

```
use ElPandaPe\FilamentActivitylog\Filament\Actions\ActivityAction;

// in a table
->recordActions([ActivityAction::make()])

// in a page header
protected function getHeaderActions(): array
{
    return [ActivityAction::make()];
}
```

It is a glance, not an investigation: it shows the most recent `timeline.limit` entries and then says how many there are in total. `timeline.style` picks how it reads — `classic` gives each entry a row with its medallion and its clock, `thread` tells it as one continuous line where a large node marks what opens or closes a record's life and a small one an attribute change.

Each entry offers a link to its own page when the plugin is registered on that panel and the reader is allowed to open it; otherwise the entry number is shown without one.

What the package writes into a row
----------------------------------

[](#what-the-package-writes-into-a-row)

Both hooks hang off `beforeLogging`, which runs before the row is saved. Neither replaces `activitylog.actions.log_activity`, so an application that already swapped that class keeps it — as long as the subclass does not redeclare `$beforeLoggingCallbacks`, which would empty the list and silently stop both.

**`logging.seal_actors`** writes `properties.actors`:

```
{"actors": {"subject": "Amaru Quispe", "causer": "Nayra Condori", "causer_role": "Super admin"}}
```

The names the subject and the causer had at that instant, and the role the causer acted with. Proper names go in the row; the grammar stays in the code, so fixing a typo never means rewriting the past. A missing key is information — it tells "there was no author" apart from "this row predates the seal", which falls back to the relation. That shape is public API from 1.0.0.

The role is read by default from a `roles` relation on the causer's own model, and whatever it finds there is named the same way every other record is: through `records`. So a role called by something other than `name` is declared once —

```
'records' => [Role::class => ['name' => 'title']],
```

— and a model with no such relation answers nothing. It costs one query per entry written; `'causer_role' => null` turns it off.

Roles that are not a relation — a column, a service, a claim in a token — need a class of your own:

```
use ElPandaPe\FilamentActivitylog\Contracts\ResolvesCauserRole;
use Illuminate\Database\Eloquent\Model;

final class RoleOfCauser implements ResolvesCauserRole
{
    public function __invoke(Model $causer): ?string
    {
        return $causer->getAttributes()['role'] ?? null;
    }
}
```

```
'logging' => ['causer_role' => RoleOfCauser::class],
```

**`logging.mask_secrets`** replaces the value of every attribute in `masked`, in both halves of the change set, before the row is written. Without it `masked` is a screen-only measure and the hash still lands in your database. Adding a sensitive attribute to a model's `logOnly()` means adding it to `masked` as well; nothing enforces that pairing for you. It covers the change set only — anything you pass to `withProperties()` is written as given.

Configuration
-------------

[](#configuration)

KeyWhat it decides`navigation`Icon, group, sort and slug of the resource. Heroicon by default; swap in whichever family your panel uses.`formats`How times, dates and timestamps are written.`per_page_options`The page sizes the listing offers.`masked`Attributes whose values are never written and never shown, whatever model they belong to.`logging.seal_actors`Whether the names of the parties are written into the row.`logging.mask_secrets`Whether masked values are replaced before the row is written.`logging.stamp_request`Whether the entry records the address, method and path, and agent of the request that wrote it.`logging.causer_role`Who answers which role the author acted with. Defaults to reading a `roles` relation on the causer and naming it through `records`; null asks nobody.`records`Per record type: the icon and colour it shows with, and the attribute it is named by — roles included. Keyed by whatever your log stores in `subject_type` — the class name, or the alias if you use a morph map.`descriptions`How a stored description is said on screen. Translating on read keeps the row honest.`timeline.limit` · `timeline.style`How many entries the drawer shows, and whether it reads as rows or as a thread.The context rail
----------------

[](#the-context-rail)

The entry page shows where a request came from, reading `properties.ip`, `properties.via` and `properties.agent` — and **`logging.stamp_request` fills them for you**, on every entry, whoever wrote it. Reading those keys and never writing them was the one thing this package asked of an application in every place it logged.

What decides whether there is a request to describe is the client address, not `runningInConsole()`: a test suite runs under the CLI, so asking about the SAPI would take the empty branch in every test and never in production. A console command still carries a synthesised request whose method and path read as a bare `GET /`, so the address is the one part only a real caller brings — without it, nothing is written and the rail says so.

Keys you set yourself are left alone, so `withProperties(['ip' => …])` still wins:

```
'logging' => ['stamp_request' => false],   // and the rail is yours to fill again
```

Languages
---------

[](#languages)

Ships English and Spanish, and every label, column and filter is translated. Two things are not, and both are deliberate:

- **The sentence is composed in English.** `Narrative` builds its grammar in code — "changed the name of", the passive for an entry with no author — and word order does not survive translation by string replacement. A Spanish panel reads its own labels and English sentences until that is designed properly.
- **Dates follow Carbon's locale, not Laravel's.** `App::setLocale()` does not reach Carbon; call `Carbon::setLocale()` too if you want day separators in your language.

Not here yet
------------

[](#not-here-yet)

Restoring a record from an entry, and grouping related entries into one block. The second one needs the application to stamp a property of its own: `activitylog` v5 removed batches, and nothing else says two entries belong to the same event.

Search runs over the stored `description` column, not over the composed sentence — so a name you can read on screen is not necessarily a name you can search for.

License
-------

[](#license)

MIT. See [LICENSE.md](LICENSE.md).

###  Health Score

44

—

FairBetter than 90% of packages

Maintenance100

Actively maintained with recent releases

Popularity6

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity54

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

4

Last Release

0d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/b270ce37c9e50f3be130225b85b7b8df6c52f9bc0395555610f23c951299ef79?d=identicon)[makoto2805](/maintainers/makoto2805)

---

Top Contributors

[![carlos-mayorga](https://avatars.githubusercontent.com/u/4119035?v=4)](https://github.com/carlos-mayorga "carlos-mayorga (4 commits)")

---

Tags

laravelAuditfilamentfilament-pluginactivitylog

###  Code Quality

TestsPest

Static AnalysisPHPStan, Rector

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/elpandape-filament-activitylog/health.svg)

```
[![Health](https://phpackages.com/badges/elpandape-filament-activitylog/health.svg)](https://phpackages.com/packages/elpandape-filament-activitylog)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3345.4M354](/packages/psalm-plugin-laravel)[laravel/cashier

Laravel Cashier provides an expressive, fluent interface to Stripe's subscription billing services.

2.6k31.8M160](/packages/laravel-cashier)[laravel/pulse

Laravel Pulse is a real-time application performance monitoring tool and dashboard for your Laravel application.

1.7k16.3M154](/packages/laravel-pulse)[api-platform/laravel

API Platform support for Laravel

58190.1k21](/packages/api-platform-laravel)[moonshine/moonshine

Laravel administration panel

1.3k268.2k89](/packages/moonshine-moonshine)[roots/acorn

Framework for Roots WordPress projects built with Laravel components.

9922.4M146](/packages/roots-acorn)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
