Privacy and masking
Session replay records what a person is looking at. It is the most sensitive telemetry you will ever collect, and the defaults are set accordingly — every one of them errs toward recording less.
What is recorded by default
| Captured | Not captured |
|---|---|
| DOM structure and mutations | Input values — masked unless you opt out |
| Mouse position and clicks | Request and response bodies |
| Request method, URL path, status, timing | Request headers, including cookies and tokens |
| Query parameter names | Query parameter values |
| Viewport size, user agent | URL fragments, where implicit-flow tokens live |
A recorded URL looks like /search?token&page — enough to see the shape of the request, not enough to leak a session token, an email address, or what someone searched for.
Masking specific elements
Two class names, applied by you, on the page:
<!-- structure recorded, text replaced -->
<div class="syncline-mask">…</div>
<!-- subtree never recorded at all -->
<section class="syncline-block">…</section>Use syncline-block for anything you would not want appearing in a screenshot pasted into a support ticket: payment fields, identity documents, medical detail, private messages. Masking is not a substitute for blocking — a masked field still reveals that it was filled, how long it took, and where the cursor went.
Errors and console output
Uncaught errors and unhandled promise rejections are recorded by default: the message, the constructor name, the script and line, and the stack. That is the one capture switched on for you, because a recording of a session that broke with no record of what broke answers nothing — and because an error is something your application already surfaced rather than data it was holding.
Console output is not recorded unless you set captureConsole. The arguments to a console call are whatever your code decided to print, which across most codebases includes tokens, request bodies, and personal data. When you do turn it on, arguments are rendered one level deep and truncated rather than serialized: a nested object becomes [Object], a DOM node becomes <div>, and nothing descends into a response body.
Turning masking off
maskAllInputs: false exists, and it records what people type. Read that sentence again before setting it. If you need one field visible, block the surrounding area and leave that field unblocked, rather than unmasking everything.
What your database spans contain
Syncline stores span attributes as your instrumentation emits them. If db.statement contains literal values rather than placeholders, those values are stored and displayed. That is your instrumentation’s choice, and worth checking before pointing this at production.
Where the data lives
In your Postgres and your object store. Syncline is self-hosted; recordings never leave your infrastructure. That is a large part of why the project exists — the alternative is shipping video of your users’ sessions to someone else.
Access control
Recordings are readable only by members of the organization that owns the project they were recorded into. The dashboard resolves your session on the server and scopes every query by organization. A recording belonging to someone else is not forbidden, it is simply not found — a 403 would confirm it exists.
The ingest API’s read endpoints require a project’s secret key and return only that project’s recordings. They were briefly protected by nothing more than session ids being unguessable, which meant the data path went around the dashboard entirely.
What is not built yet: invitations, roles beyond owner and member, and audit logging. Anyone you add to an organization can watch every recording in it.