Documentation
ReelPrivacy and capture
Updated
Reel records sanitised DOM events, not video. The recorder in the browser removes sensitive content before anything is uploaded, and the Reel server re-validates every chunk against an allowlist and rejects anything that breaks the rules. This page describes what the code enforces today. Every real-user deployment still needs its own privacy review of the pages it records.
Nothing records until you say so
Adding <x-reel::recorder /> to a layout only loads the scripts. Recording starts when your own code calls Reel.start() with consent: true, after your application's consent decision.
const status = await Reel.start({ consent: true, refuseOnGpc: true });
Reel.start() resolves to a status object. Its state tells you what happened:
| State | Meaning |
|---|---|
recording |
The recorder is running. |
awaiting_consent |
You did not pass consent: true. |
refused_gpc |
You passed refuseOnGpc: true and the browser sends Global Privacy Control. |
hidden |
The current page is on a route hidden from Reel. |
stopped |
Recording stopped, for example after Reel.stop() or when the session reached its time limit. |
Read the current status at any time with Reel.status(), and stop recording with Reel.stop(). The status also carries an incomplete flag and a reason when something went wrong, such as an upload origin mismatch.
A recording session lasts at most 30 minutes from the moment it is granted.
What is always masked or removed
These protections are built into the recorder and checked again on the server. No setting turns them off.
- Form values. The values of
input,selectandtextareaelements, and the text ofcontenteditableelements, are replaced with***. - Media and embedded content.
canvas,video,audio,iframe,objectandembedelements are replaced by placeholders. Canvas is never recorded. - URLs in the page. URL-carrying attributes such as
href,src,srcsetandactionare dropped. Page URLs and request paths are recorded without their query string or fragment. - Framework payloads. Inertia's
data-pageand Livewire'swire:snapshotandwire:initial-dataattributes are dropped, so component state does not ride along in the recording. - Event handlers. Inline
on*attributes are dropped. - Network and console data. Request and response bodies, headers and console output are never captured. For a failed request, only the method, path and status are recorded as a marker.
Hiding a whole route
Mark a route or a group with hiddenFromReel(). On that response the recorder component renders nothing, the session-grant endpoint refuses to start a session, and a recording already in progress stops and discards its buffered events when it sees a response from a hidden route.
Route::get('/billing/payment-method', PaymentMethodController::class)->hiddenFromReel();
Route::hiddenFromReel()->group(function (): void {
require __DIR__.'/auth.php';
});
For a route whose registration API cannot use the fluent macro, add the reel.hidden middleware instead.
Masking or blocking part of a page
On a page you do record, add one of these attributes to the element:
data-reel-maskreplaces the element's text with***, and applies to everything inside it.data-reel-blockreplaces the element with an empty placeholder.
<div data-reel-mask>{{ $customer->name }}</div>
<div data-reel-block>@include('partials.payment-panel')</div>
Ordinary rendered text is recorded unless you mask it, hide the route or block the element. Look for names, email addresses, account numbers and anything else personal that your pages render as plain text.
Capture policy
Every setting on an application's Capture policy card is enforced by Reel itself, as each chunk
of a recording arrives. Enforcement does not depend on the browser: a visitor who tampers with the
recorder, replays an old script, or posts hand-written chunks cannot get past it. Protection is
monotonic — these settings can only remove data from a recording, never restore data another rule
removed, and none of them can expose the mandatory baseline (input and contenteditable values are
always masked).
Masking severity
- Inputs baseline (mandatory) — form field values and
contenteditablecontent are masked. This is the floor and cannot be turned off. Ordinary rendered page text is kept, so profile data shown as page text appears in the recording. - All text (stronger) — every rendered text node and every later text change is replaced with
***before the chunk is stored. Stylesheet text is kept so replay still renders layout and colours. Choose this when rendered page text is itself sensitive.
Additional mask selectors
One selector per line. Reel masks the whole subtree of text under each matching element.
Supported form: an optional tag name followed by any number of #id and .class tokens.
.customer-name
span.account-number
#patient-summary
div.invoice.total
Descendant combinators, attribute selectors, and pseudo-classes such as :has() are rejected when
you save, because Reel matches selectors against the serialized DOM it receives rather than running
a browser. Use a class on the element itself, or the data-reel-mask attribute, for anything the
grammar above cannot express.
Additional block selectors
Same form as mask selectors. Each matching element is replaced by an empty placeholder that keeps
only its geometry (class, width, height, and sanitized inline style) and is marked
data-reel-blocked. Its children are dropped entirely. Use this for regions where even the shape of
the content is sensitive — a payment panel, an uploaded document preview.
Excluded paths
One path pattern per line, starting with /, with * as a wildcard. No chunk recorded on a
matching path is stored.
/billing/*
/account/recovery
/billing/* matches /billing/invoices but not /billing itself — list both if you want both.
Exclusion also applies to the rest of a session: once a session's current page matches an excluded
path, Reel refuses its later chunks too, including chunks that carry no page URL of their own. Adding
an exclusion while a session is in flight takes effect on that session's next upload.
Excluded paths are a policy you can change from this screen. For a route that must never be
recordable, mark it in the application instead with ->hiddenFromReel() or the reel.hidden
middleware: that is absolute, stops the recorder before it starts, and no configuration value can
re-enable it.
Sampling percent
The share of sessions Reel keeps, from 0 to 100. The decision is made once per session from the
session id, so a session is recorded in full or not at all — you never get half a session. The same
session always gets the same answer, so a retried or resumed upload cannot slip past a rate it was
already excluded by. At 0 nothing is recorded; at 100 every session is.
What enforcement does and does not promise
Reel enforces these settings on its own servers, before anything is written to storage, so nothing a monitored browser does can defeat them. It is not the same as never transmitting the data: with server-configured masking, blocking, and path exclusion, the recorder still uploads the chunk over TLS and Reel discards or rewrites it on arrival. Data removed this way is never stored, never compacted, and never replayable.
If you need a region never to leave the browser at all, mark it in your own application, where the recorder applies the rule before the event is buffered:
data-reel-maskon an element masks its text in the browser.data-reel-blockon an element replaces it with a placeholder in the browser.->hiddenFromReel()(or thereel.hiddenmiddleware) on a route stops capture for that request entirely.
These three are always available and need no setting on this screen.
Where recordings go
The recorder uploads compressed chunks directly to your Reel deployment, signed with a short-lived grant from your application. Reel accepts an upload only when it comes from one of the application's Allowed origins and carries a valid grant signed by one of the application's active keys. Recordings are stored in the private bucket in your Laravel Cloud account and are served back only through Reel's signed-in player.
Identifying the user
If the request that starts a session has a signed-in user that is an Eloquent model, the client records that model's primary key as the session's application user ID. You can bind your own implementation of ArtisanBuild\ReelClient\Contracts\StableUserIdResolver in the container to supply a different identifier for users that are not Eloquent models. No other user detail is sent. The ID is what user erasure matches on.