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 Exceptions Laravel Package

bezhansalleh/filament-exceptions

A Filament plugin that logs Laravel exceptions to your database and adds a searchable admin panel to browse, filter, and inspect them, with stack traces, request context, and user details, so you can debug production issues without digging through log files.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire it up (5 minutes).

composer require bezhansalleh/filament-exceptions
php artisan vendor:publish --tag="filament-exceptions-migrations"
php artisan vendor:publish --tag="filament-exceptions-config"
php artisan migrate

Register the plugin on the panel that should host the viewer — not necessarily every panel:

// app/Providers/Filament/AdminPanelProvider.php
use BezhanSalleh\FilamentExceptions\FilamentExceptionsPlugin;

public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->plugin(FilamentExceptionsPlugin::make());
}

Capture hooks into Laravel's exception reporting automatically, so there is nothing to add to your exception handler. Confirm that assumption early if your app overrides report(), uses a custom handler, or has a populated $dontReport array — anything suppressed there never reaches the table.

Where to look first, in this order:

  1. config/filament-exceptions.php — read it top to bottom before writing any code. Navigation group/sort, the model class, and the resource class are the extension points you'll touch most; key names shift between minors, so trust the file on disk over any blog post.
  2. The plugin's migration — this tells you the real column names (headers, request, body, resolved or similar) and their types. Every customization below depends on knowing these exactly.
  3. vendor/bezhansalleh/filament-exceptions/src/Resources/ExceptionResource.php — the list/filter/action shape you'll be extending.

First use case — close the loop end to end. Hit a throwing route, then walk the full triage cycle once:

Route::get('/boom', fn () => throw new RuntimeException('Testing exception viewer'));

Open the new navigation item, confirm the badge shows 1, open the record, read the stack trace and request context, click resolve, confirm the badge drops to 0. That's the entire product. Everything else is customization or operational hardening.

Implementation Patterns

The daily loop: badge → unresolved filter → resolve after deploy. Treat the navigation badge as the team's "does anyone need to look at this?" signal and the unresolved filter as the work queue. The workflow that actually holds up: after shipping a fix, filter unresolved, confirm the error has stopped recurring (watch the occurrence count on grouped duplicates), then bulk-resolve. Resolving is a manual claim that code changed — it is not a health check, and nothing will un-resolve a row if the bug comes back. It just reappears.

Gate it with a policy — on list and view. Exception rows routinely contain tokens, emails, cookies, and internal paths. Filament falls back to the model policy for viewAny/view, but policy auto-discovery won't find a policy for a vendor model, so register it explicitly:

// app/Policies/ExceptionPolicy.php
use App\Models\User;
use BezhanSalleh\FilamentExceptions\Models\Exception as ExceptionRecord;

class ExceptionPolicy
{
    public function viewAny(User $user): bool { return $user->hasRole('super_admin'); }
    public function view(User $user, ExceptionRecord $record): bool { return $user->hasRole('super_admin'); }
    public function update(User $user, ExceptionRecord $record): bool { return $user->hasRole('super_admin'); }
}
// AppServiceProvider::boot()
Gate::policy(ExceptionRecord::class, ExceptionPolicy::class);

update matters: the resolve action is an update, so gating it hides the button. Log in as a non-admin and verify both the list page and a direct detail URL are blocked — a gate that only hides the nav item is not a gate.

Redact on write, not at read time. Once a secret is in the table it's in your backups too. Scrub at the model event:

ExceptionRecord::creating(function (ExceptionRecord $record) {
    $redact = ['authorization', 'cookie', 'password', 'password_confirmation', 'token', 'secret'];

    foreach (['headers', 'request'] as $attribute) {          // confirm names against the migration
        $payload = $record->getAttribute($attribute);

        if (is_array($payload)) {
            $record->setAttribute($attribute, collect($payload)
                ->map(fn ($value, $key) => in_array(strtolower((string) $key), $redact, true) ? '[redacted]' : $value)
                ->all());
        }
    }
});

Own retention. Assume pruning is yours to schedule — verify whether your version ships a prune command, but write the command either way so the ownership is explicit and alerting is on your schedule:

protected $signature = 'exceptions:prune {--days=30}';

public function handle(): int
{
    $deleted = ExceptionRecord::query()
        ->where('resolved', true)          // confirm column name
        ->where('created_at', '<', now()->subDays((int) $this->option('days')))
        ->delete();

    $this->info("Pruned {$deleted} exception records.");
    return self::SUCCESS;
}

Schedule it, and alert if it stops running — a silently failing prune is how the table becomes a database problem.

Build the digest the package doesn't. This is a viewer, not an APM; nothing pages anyone. A scheduled query plus a mail/Slack notification buys you most of the alerting value for ~15 lines:

$new = ExceptionRecord::query()
    ->where('resolved', false)
    ->where('created_at', '>=', now()->subHour())
    ->count();

Tenant scoping, if you run multi-tenant panels. Exceptions aren't tenant-tagged. Add the column, stamp it from filament()->getTenant() in a creating hook (guard for console/queue contexts where there is no tenant), then scope the query in a resource subclass and point the config's resource key at it:

class TenantExceptionResource extends ExceptionResource
{
    public static function getEloquentQuery(): Builder
    {
        return parent::getEloquentQuery()
            ->where('tenant_id', filament()->getTenant()?->getKey());
    }
}

Query the table like any Eloquent model. The biggest underrated win: exception records are rows, so "top error types this week", "errors by authenticated user", and "which release introduced this" are plain queries in Tinker or a custom widget. Pair a created_at pivot with deploy timestamps and you get rudimentary release correlation without any APM.

Gotchas and Tips

  • It is not incident detection, and positioning it as such is the classic failure. No alerting, no tracing, no trend dashboards, no regression detection on its own. Pair it with a real tracker or build the digest above; don't cancel Sentry because this exists.
  • Test the write path fails open. Capture runs inside the exception handler. Drop the table in a sandbox and throw — confirm the original exception surfaces normally and doesn't get masked or recurse. Do this before rollout, not after.
  • The source-code snippet needs the file to exist at that path on the running host. Optimized containers, build/runtime path mismatches, or vendor-stripped images will show the trace but no highlighted source. Don't debug that at 2 a.m. — confirm it works in your actual deploy target.
  • $dontReport and custom handler paths silently drop records. If a class of error never appears, check the handler before blaming the plugin.
  • Console and queue exceptions have no request context. Headers/request panes will be empty — expected, not broken. Anywhere the payload is thin, correlate by trace instead.
  • The navigation badge runs a count query on every panel render. On a fat, unpruned table this shows up as panel latency, not as an exception page problem. Prune first, then consider making the badge conditional.
  • If you override getNavigationBadgeColor() on the resource, re-verify after 4.2.1 — badge color resolution now delegates to the plugin. Your override should still win, but this was the one behavioral change in the release and it's exactly the kind of thing that silently stops applying.
  • Grouped duplicates and "resolved" don't compose. A recurrence after a deploy appears as new activity on the same group; make sure the team resolves after confirming the count has flatlined, or you'll resolve the same bug weekly.
  • Search and sort over JSON columns degrade fast. Exception payloads are fat. Keep the list view filtered by status/date rather than free-text searching across bodies.
  • Disable capture in local and test environments. Exceptions thrown by your test suite get persisted, which slows tests and pollutes the table you're using to reason about production. Set the env/config toggle in phpunit.xml and in your local .env.
  • Expect a two-package upgrade. The 4.x line tracks Filament v4; every Filament major is a coordinated upgrade of both. Budget it as a recurring cost rather than being surprised by it, and check any customized resource subclass against the new base class each time.
  • Lock-in is genuinely low — it writes to an app-owned table, so exit is "drop plugin, drop table." That's the argument for trying it; just don't let that fact talk you out of the policy gate and the prune job.
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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource