Skip to content
Scalpels home

Scalpels is officially launching on October 31, 2026. Special early adopter pricing is available now.

← All documentation

Documentation

Reel

Privacy 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, select and textarea elements, and the text of contenteditable elements, are replaced with ***.
  • Media and embedded content. canvas, video, audio, iframe, object and embed elements are replaced by placeholders. Canvas is never recorded.
  • URLs in the page. URL-carrying attributes such as href, src, srcset and action are dropped. Page URLs and request paths are recorded without their query string or fragment.
  • Framework payloads. Inertia's data-page and Livewire's wire:snapshot and wire:initial-data attributes 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-mask replaces the element's text with ***, and applies to everything inside it.
  • data-reel-block replaces 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 contenteditable content 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-mask on an element masks its text in the browser.
  • data-reel-block on an element replaces it with a placeholder in the browser.
  • ->hiddenFromReel() (or the reel.hidden middleware) 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.