Configuration#
php artisan session-replay:install publishes config/session-replay.php; php artisan vendor:publish --tag=session-replay-config does the same on its own. Your file is merged over the package defaults key by key, so it may contain only what you change. A list you set (except, except_routes, capture.console, size.strip_attributes, size.keep_attributes, snapshots.share_routes, snapshots.share_paths, snapshots.volatile_ids, ingest.middleware, viewer.middleware) replaces the default list as a whole, so [] really turns a list off.
Recording#
| Key |
Default |
Meaning |
enabled |
env('SESSION_REPLAY_ENABLED', true) |
The master switch. Off: the directive renders nothing and ingest refuses uploads. |
sample_rate |
env('SESSION_REPLAY_SAMPLE_RATE', 1.0) |
Share of recordings kept, decided once per recording in the browser. |
guests |
env('SESSION_REPLAY_GUESTS', false) |
Record people who are not signed in. |
except |
password-reset and verification pages |
Request paths (Str::is patterns) that never get the recorder. The package's own pages are always left out. The default list covers *password-reset*, *reset-password*, *forgot-password*, password/*, *email-verification*, *verify-email* and email/verify*; a list you set replaces it, so keep those in yours. |
except_routes |
password-reset and verification routes |
Route names (Str::is patterns) that never get the recorder, whatever their URL. The default list covers password.* and verification.* (Laravel's starter kits) and filament.*.auth.password-reset.*, filament.*.auth.email-verification.* and filament.*.auth.email-change-verification.* (Filament panels). |
idle_timeout |
30 |
Minutes without activity after which a tab starts a new recording. |
flush_interval |
5000 |
Milliseconds between uploads while the page is open (minimum 1000). |
Mode#
| Key |
Default |
Meaning |
mode |
env('SESSION_REPLAY_MODE', 'session') |
session: a recorded tab uploads from its first page. on_error: the browser keeps the last moments in memory and uploads only when something goes wrong. See Recording only when something goes wrong. |
on_error.buffer_seconds |
60 |
Seconds kept before the trigger, 5 to 600. The window starts at a full page snapshot, so a replay may start up to half a window (at most 30 s) earlier. |
on_error.max_buffer_mb |
5 |
The most the window may take in memory, in MB of events as the recorder estimates them, 1 to 50. A page that changes a lot reaches it before buffer_seconds; the window then shrinks to its newest page snapshot. |
on_error.triggers |
['error', 'request'] |
Marker types that upload the window: error, request, console, rage-click, custom. A type also has to be captured (capture.*). |
on_error.ask |
false |
Ask the person before anything is sent: true (or dialog) in a small modal dialog the recorder draws, toast in a card at the bottom corner that leaves the page usable. See Privacy. |
on_error.ask_again |
false |
After "Don't send" (or a toast left unanswered): false leaves the tab alone until its session ends; true asks again on the next trigger, at most once per window, and keeps the window going meanwhile. |
on_error.ask_timeout |
60 |
Seconds the toast waits for an answer before it counts as "Don't send", 5 to 600; 0 keeps it up until it is answered. The dialog never times out. |
on_error.livewire_error_modal |
production |
What happens to Livewire's error modal when a failed request puts the question up: keep shows the question over it, replace shows the question instead of it, production replaces it only while app.debug is off. See Privacy. |
on_error.keep_pending |
true |
Keep the uploaded window in the browser (IndexedDB) while its upload fails, so a reload during the outage does not lose it. See When the upload fails. |
Consent#
| Key |
Default |
Meaning |
consent |
env('SESSION_REPLAY_CONSENT', 'always') |
always or opt-in (waits for SessionReplay.consent(true)). |
respect_gpc |
false |
Do not record browsers that send Global Privacy Control. |
Privacy#
| Key |
Default |
Meaning |
privacy.mask_all_inputs |
true |
Mask every input's value. Password inputs are masked regardless. |
privacy.mask_all_text |
false |
Mask every text node: layout-only recordings. |
privacy.mask_text_selector |
[data-replay-mask], [data-replay-mask] *, [contenteditable], [contenteditable] * |
Elements whose text is replaced with asterisks. Rich editors are in the default, so what people type there is masked like an input. |
privacy.unmask_text_selector |
[data-replay-unmask] |
With mask_all_text: text inside matching elements stays readable, unless mask_text_selector matches it. Input values are never affected. |
privacy.block_selector |
[data-replay-block] |
Elements recorded as an empty box of the same size. |
privacy.ignore_selector |
[data-replay-ignore] |
Elements whose input events are not recorded. |
privacy.anonymous |
false |
Record signed-in people without storing who they are (no person, no impersonator; the workspace stays). Uploads are still limited per person. See Privacy. |
privacy.store_user_agent |
true |
false drops the user agent; the device class is kept. |
privacy.redact_query |
token, access_token, refresh_token, id_token, signature, code, state, password, secret, key, api_key, email |
Query parameters whose value is replaced with redacted in every URL the recorder sends. |
Capture#
| Key |
Default |
Meaning |
capture.console |
['error'] |
Console levels recorded; console.error also becomes a console marker. Other levels you add are kept in the recording's events without a marker. [] turns the console off. |
capture.errors |
true |
Uncaught errors and unhandled rejections as error markers. |
capture.server_errors |
'class' |
Exceptions Laravel reports during a recorded request as error markers: class, status, method and path. 'message' adds the exception's message (URLs redacted); it may carry personal data. false turns it off. Needs context.enabled. |
capture.livewire |
true |
Failed Livewire requests as request markers, wire:navigate page views as navigation markers. |
capture.vitals |
true |
LCP, INP and CLS as vital markers. |
capture.requests |
500 |
Other same-origin requests (fetch, XMLHttpRequest) answered with this status or higher (400 to 599), or not at all, as request markers: method, path, status and duration, never headers or bodies. false turns it off. |
capture.rage_clicks |
true |
Three or more clicks on one spot within 700 ms as rage-click markers. |
Size#
| Key |
Default |
Meaning |
size.dedupe_stylesheets |
true |
Store stylesheets once per SHA-256 instead of inside every snapshot. |
size.dedupe_min_bytes |
2048 |
Smaller stylesheets stay inline. |
size.strip_attributes |
['wire:*', 'x-*', '@*', ':*', 'ax-load*'] |
Attribute names dropped before upload; * is a trailing wildcard. |
size.keep_attributes |
['x-cloak', 'wire:loading*', 'wire:offline*', 'wire:dirty*'] |
Names (or prefix*) that stay even when a pattern matches: what stylesheets select on. |
size.sampling |
['mousemove' => 50, 'scroll' => 150, 'media' => 800, 'input' => 'last'] |
rrweb's sampling options: milliseconds between recorded mouse moves, scrolls and media events; input: 'last' keeps the final value of a burst. |
Shared snapshots#
| Key |
Default |
Meaning |
snapshots.share_routes |
[] |
Route names (Str::is patterns) whose page snapshot is stored once per SHA-256 of its content and shared by every recording that saw the same page. Keyed entries are read like plain ones. |
snapshots.share_paths |
[] |
Request paths (Str::is patterns), the same way. A page matching either list shares; nothing does by default. |
snapshots.volatile_ids |
['fi-dropdown-panel-*'] |
Ids a script makes up on every page load (prefix*). In a shared snapshot they are renamed in page order, together with every aria-controls, aria-labelledby, for and the like that points at them, so two loads of the same page still match. |
Share pages that look the same for everyone who opens them: a pricing page, the docs, a sign-in form. A page that shows the person's name, a table of their data or a flash message gets a new snapshot for every difference, so sharing it only saves space when the same person reloads it. See Shared snapshots.
Routes#
| Key |
Default |
Meaning |
path |
session-replay |
Prefix of every package route. |
domain |
null |
Restrict the routes to one domain. |
ingest.middleware |
[] |
Extra middleware for the upload routes. None is needed: identity comes from the signed token. |
ingest.throttle |
240 |
Uploads per minute per person (for guests, per rendered page: a value in the signed token, not one the browser picks). null turns it off. |
ingest.daily_mb |
250 |
Megabytes one person may upload per day, as sent. A day of steady work in a panel is well under 100 MB. null turns it off. |
ingest.guest_daily_mb |
2048 |
Megabytes all guests together may upload per day. Guests cannot be told apart, so this is the cap that keeps a script from filling the disk. null turns it off. |
ingest.max_batch_kb |
1536 |
Largest upload accepted, as sent; a shared snapshot gets the same limit. |
ingest.max_session_mb |
50 |
A recording stops growing here and is marked truncated. |
ingest.max_asset_kb |
1536 |
Largest stylesheet accepted, as sent (compressed). |
ingest.token_days |
7 |
Days the token a page was rendered with is accepted. Keep it longer than your full-page cache (see Cached pages). Tokens signed with a key in app.previous_keys are accepted too, so rotating APP_KEY keeps open tabs recording. |
ingest.guest_tokens_expire |
true |
false: a token that names nobody (no person, no workspace, no impersonator) never expires. |
Storage#
| Key |
Default |
Meaning |
storage.disk |
env('SESSION_REPLAY_DISK', 'local') |
Filesystem disk for chunks, stylesheets and shared snapshots. |
storage.directory |
session-replay |
Folder on that disk. |
storage.connection |
env('SESSION_REPLAY_DB_CONNECTION') |
Database connection of the package's tables; null is the default connection. |
run_migrations |
true |
Run the package migrations with php artisan migrate. |
retention.days |
30 |
What session-replay:prune keeps. |
Viewer#
| Key |
Default |
Meaning |
viewer.enabled |
true |
The built-in list and player page. The data routes stay either way. |
viewer.middleware |
['web', 'auth'] |
Runs before the viewSessionReplay gate on pages and data routes. |
viewer.guard |
null |
The guard whose user is handed to the gate. |
viewer.per_page |
25 |
Recordings per list page. |
viewer.user_label |
email |
Attribute shown for a person in the list. |
Log context#
| Key |
Default |
Meaning |
context.enabled |
true |
Write the cookie and add session_replay / session_replay_url to Laravel's Context. |
context.cookie |
session_replay_id |
The cookie's name. |
What is registered in code#
| Call |
Purpose |
Gate::define('viewSessionReplay', fn ($user, ?ReplaySession $session = null) => ...) |
Who may watch. |
SessionReplay::recordWhen(fn ($user, $request) => ...) |
Narrow who is recorded. |
SessionReplay::userUsing(fn ($request) => ...) |
The signed-in person, when not the default guard's. |
SessionReplay::tenantUsing(fn ($request) => ...) |
The workspace of a recording. |
SessionReplay::impersonatorUsing(fn ($request) => ...) |
The impersonator's key. |
SessionReplay::propertiesUsing(fn ($request) => [...]) |
Anything else to keep on the recording. |
SessionReplay::urlUsing(fn (ReplaySession $session) => ...) |
Where replays are watched, when not the built-in viewer. |
SessionReplay::logUrlUsing(fn (ReplayMarker $marker) => ...) |
Where a marker's log lines are read: the player's "Open in logs" button. See Error tracking. |
SessionReplay::visibleUsing(fn (Builder $query, $viewer) => ...) |
Narrow the recordings a viewer finds in a list. Runs as a group of its own, so an orWhere() only narrows. |
Languages#
The viewer and the player ship in English, German, Spanish, Romanian and Russian and follow the app's locale. To change a string or add a language, publish the files:
php artisan vendor:publish --tag=session-replay-translations
They land in lang/vendor/session-replay/{locale}/viewer.php, player.php and recorder.php (the dialog of on_error.ask). The player reads its strings from the <x-session-replay::player> component and the recorder from the page that renders @sessionReplay, so both follow the app's locale.
Commands#
| Command |
Purpose |
session-replay:install |
Publish the config, offer to migrate, publish the provider with the gate. |
session-replay:prune {--days=} |
Delete old recordings and unreferenced stylesheets. |