Error tracking
While a recording runs, every log line and error report of that person's requests can point at the replay.
How it works
- The recorder writes the recording's id into a plain cookie (
context.cookie, defaultsession_replay_id;SameSite=Lax,Secureon HTTPS, expires withidle_timeout). It holds a UUID and nothing else. - The browser sends the cookie with every request to your app: page loads, Livewire updates,
fetchcalls. AddReplayContext, a global middleware the package registers, reads it, checks that it is a UUID and adds two keys to Laravel'sContext:
| Key | Value |
|---|---|
session_replay |
The recording's id |
session_replay_url |
Where to watch it: the built-in viewer's page, or what SessionReplay::urlUsing() returns. Left out when there is nowhere to link to. |
session_replay_moment |
The same page with ?at= and the request's time, so it opens the replay a few seconds before this request. Left out with session_replay_url. |
session_replay_report |
Added when an exception is reported during the request (capture.server_errors): the id of that report, the same the error marker on the replay carries in its payload as report. See From the replay to the logs. |
The middleware runs no query.
From the replay to the logs
The keys above take you from a log line to the replay. The other way round starts at the marker: when Laravel reports an exception during a recorded request, the package gives the report an id (a ULID), puts it on the error marker's payload as report and adds it to Context as session_replay_report before Laravel logs the exception, so the log lines of exactly that exception, and the event your error tracker receives, carry the same id. The marker's details in the player show the id with a Copy report id button, and an Open in logs button when your app says where to look:
// app/Providers/SessionReplayServiceProvider.php, boot()
use Packstub\SessionReplay\Facades\SessionReplay;
use Packstub\SessionReplay\Models\ReplayMarker;
SessionReplay::logUrlUsing(function (ReplayMarker $marker): ?string {
$report = $marker->payload['report'] ?? null;
return $report ? url('/log-viewer?query='.$report) : null;
});
The closure receives every marker the player shows and returns a URL or null. Three tools, by example: Log Viewer searches the files for ?query=<report id>; Telescope's exceptions page lists the exception by class and time (url('/telescope/exceptions')); Sentry and Flare take the id as a search on the session_replay_report context key in their own URLs. The button opens in a new tab and is only ever shown to someone the viewSessionReplay gate let in.
Where it shows up
Laravel attaches Context to everything it logs, and carries it into queued jobs dispatched during the request:
[2026-09-18 10:57:12] production.ERROR: Undefined array key "total"
{"exception":"…"} {"session_replay":"0b1c6a0e-…","session_replay_url":"https://app.test/session-replay/0b1c6a0e-…"}
Error trackers that read Laravel's Context, such as Flare, Sentry and Nightwatch, receive the keys as context on the report, next to whatever else your app adds. From a report, open session_replay_moment to land just before the failing request. The player turns ?at= into a position, three seconds early, because the server's clock and the browser's rarely agree to the second. ?t= with seconds into the recording works on session_replay_url as before.
In your own code:
use Illuminate\Support\Facades\Context;
$replay = Context::get('session_replay_url');
Server errors on the replay
The replay shows a failed request from the browser's side ("Livewire request failed (500)"). With capture.server_errors (default 'class'), an exception Laravel reports during one of the recording's requests is put on the replay too, as an error marker:
| Label | Server error: QueryException |
| Payload | source: server, the exception's class, the status (500, or an HTTP exception's own), the request's method and path (never its query) |
| When | The time Laravel reported it |
- What counts: what Laravel reports. Exceptions it doesn't report (validation errors, 404s, whatever is in your
dontReport) don't appear. - Whose recording: only the recording of the person who made the request. The marker is skipped when the request is signed in as someone else. Recordings that name nobody (guests,
privacy.anonymous) take it on their id alone, a random UUID only that browser and your logs know. - Recordings not stored yet: in mode
on_errorthe failed request is what makes the browser upload, so the recording often isn't stored yet. The marker then waits in your cache foridle_timeout, and the recording's next batch takes it. - Requests only: queued jobs carry the Context keys along but are never put on a timeline. The package's own routes (uploads, the viewer) are left out too.
- Pages the app does not record: a request to an
exceptpath or anexcept_routesname carries none of the keys and raises no marker, even while the tab's recording runs around it (a page reached withwire:navigateinside a recorded session still sends the cookie). Livewire's update requests from such a page keep the keys: their path is not the page's. - The message: left out by default, because it often carries personal data (a query's values, an email address).
'message'adds it to the label, with the URLs in it redacted byprivacy.redact_query.falseturns server errors off.
It needs context.enabled: the recording's cookie is how a request is matched to its recording.
Link to your own page
When replays are watched somewhere other than the built-in viewer:
use Packstub\SessionReplay\Facades\SessionReplay;
use Packstub\SessionReplay\Models\ReplaySession;
SessionReplay::urlUsing(fn (ReplaySession $session): string => route('support.replays.show', $session->id));
For the context the closure receives an unsaved model that carries only the id, so build the URL from $session->id and do not read other attributes there.
Turning it off
'context' => ['enabled' => false],
No cookie is written and the middleware is not registered. Rename the cookie with context.cookie. If your app lists cookies for a consent banner, this one is functional: it identifies a recording, not a person, and is only set while recording runs.