Packstub

Guides

Session replay in Laravel and Filament, on your own server

Record sessions with rrweb, keep them on your own disk and database, and watch them in your Laravel app or Filament panel behind your own gate.

Published 6 min read

"The form did nothing when I clicked save." Every support inbox knows the sentence, and every developer knows what comes next: which browser, which page, what did you click, can you send a screenshot. A session replay answers all of it in under a minute, because you watch the forty seconds before the message was written, with the failed request and the JavaScript error marked on the timeline.

Hosted replay products are a great fit for product analytics, funnels and marketing sites. This guide is about the other case: replay as a support and debugging tool inside your own app, where the recordings sit on your own disk and database, link to your own users and workspaces, and open behind your own authorization. It uses two free MIT packages, Session Replay for Laravel for any Laravel app and Filament Session Replay for a Filament v5 panel. Both are built on rrweb, the open source recorder behind many replay tools.

How a replay works

rrweb does not film the screen. It serializes the DOM once (a snapshot), then records every change to it, plus mouse, scroll and input events. The player rebuilds the page from those events in a sandboxed iframe that runs no scripts. That is why a recording of a 40-second visit is tens of kilobytes instead of a video file, and why text stays sharp at any size.

The package adds the parts rrweb leaves to you:

  • a recorder script served from your own origin, with privacy defaults switched on;
  • an ingest endpoint that trusts only a token your server signed when it rendered the page;
  • gzip chunks on any filesystem disk and an index in four database tables;
  • a player component and a small viewer, both behind one gate;
  • the recording's id and link in Laravel's log context, so an error report points at the replay.

Install and record

composer require packstub/session-replay
php artisan session-replay:install

The install command publishes the config, runs the migrations and publishes App\Providers\SessionReplayServiceProvider with the gate in it. Put the directive in the layouts you want recorded:

    @sessionReplay
</body>

Signed-in people are recorded by default and guests are not (SESSION_REPLAY_GUESTS=true turns them on). Narrow it per request when you need to:

use Packstub\SessionReplay\Facades\SessionReplay;

SessionReplay::recordWhen(fn ($user, $request): bool => ! $user?->is_staff);

In a Filament panel there is no directive. Require the plugin and register it, and the panel's pages are recorded with the panel's own guard, the current tenant and the panel id:

composer require packstub/filament-session-replay
use Packstub\SessionReplay\SessionReplayPlugin;

return $panel
    // ...
    ->plugin(SessionReplayPlugin::make());

Decide who may watch

One gate guards the list, every replay and every file behind a replay. It receives the recording, so the rule can be as narrow as you like:

use Illuminate\Support\Facades\Gate;
use Packstub\SessionReplay\Models\ReplaySession;

Gate::define('viewSessionReplay', function ($user, ?ReplaySession $session = null): bool {
    // Support watches customers, never other staff.
    return $user->is_support && ! $session?->user?->is_staff;
});

Until the gate is defined, only the local environment is let in, so a forgotten setup never exposes a recording. In Filament, a policy registered for ReplaySession takes over inside the panel, which is how Filament Shield permissions plug in.

Privacy is the default, not a setting to find

A replay tool is only welcome in a product when the people in it are safe. The defaults are strict, and they only ever get stricter inside a major version:

  • every input is masked in the browser before anything is uploaded, and password inputs always are;
  • data-replay-mask replaces displayed text with asterisks, data-replay-block records an empty box of the same size, data-replay-ignore drops input events;
  • Livewire component snapshots (wire:snapshot) never enter a recording, because attributes the player does not need are stripped;
  • consent can be opt-in (window.SessionReplay.consent(true)), and Global Privacy Control can be respected;
  • a recording belongs to one person in one workspace: signing in, switching account, switching workspace or starting an impersonation starts a new one.

In Filament you declare masking where the field is:

TextColumn::make('customer')->maskInReplay(),
TextEntry::make('iban')->maskInReplay(),
Section::make('Payout details')->blockInReplay(),

The privacy guide has wording for your privacy policy. The store you are reading this on uses it: its developer portal is recorded, and the privacy policy says so.

What it costs

Measured on a Filament 5 panel with headless Chrome, stored as gzip:

What happens on the page Stored
The first snapshot of a table page with 50 rows 34 KB
The Filament stylesheet, once for every recording 65 KB
An open page nobody touches 0.2 KB per minute
The same table polling every 2 s 8 KB per minute
A search or a sort that re-renders the table 10 to 30 KB
An edit modal opened, filled in and saved about 16 KB

Two decisions keep it there. A server-rendered page's snapshot is mostly stylesheet, so stylesheets are stored once per SHA-256 of their content and shared by every recording. And attributes the player never uses (wire:*, x-*) are dropped before upload. With the CPU slowed down four times, the same scripted clicks measured the same interaction latency with and without the recorder. retention.days and sample_rate set the total; session-replay:prune runs daily and leaves pinned recordings alone.

From a log line to the replay

While a recording runs, its id and viewer URL are in Laravel's Context. Every log line and every error report from that request carries them, whatever tracker you use:

[2026-09-21 10:20:11] production.ERROR: Undefined array key "total"
{"session_replay":"9e62c402-…","session_replay_url":"https://app.test/admin/session-replays/9e62c402-…"}

Append ?t=83 and the replay opens at 1:23, which makes a good link for a ticket.

Around your users, in Filament

The plugin hangs recordings off the people they belong to: a relation manager for a user's page, a "Watch last session" action for a row, a stats widget for the dashboard.

use Packstub\SessionReplay\Filament\Actions\WatchLastSessionAction;
use Packstub\SessionReplay\Filament\Resources\ReplaySessions\RelationManagers\ReplaysRelationManager;

public static function getRelations(): array
{
    return [ReplaysRelationManager::class];
}

// On a row or in a page header:
WatchLastSessionAction::make(),

In a multi-tenant panel the list shows the current workspace's recordings; an operator panel shows every workspace with a Workspace column, and sessions made while impersonating are flagged with who was behind them.

When another tool is the right one

For funnels, heatmaps, marketing-site analytics or mobile apps, a hosted product such as PostHog or Microsoft Clarity, or a self-hosted platform such as OpenReplay, is the right choice, and they run happily next to this package. Session Replay for Laravel is one Composer package for the moment a person says "it did nothing" and you want to see what they saw, without the recording leaving your infrastructure.

Start with the installation guide, or with the Filament plugin when your app is a panel.