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

CapturedNot captured
DOM structure and mutationsInput values — masked unless you opt out
Mouse position and clicksRequest and response bodies
Request method, URL path, status, timingRequest headers, including cookies and tokens
Query parameter namesQuery parameter values
Viewport size, user agentURL 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.

Retention is not implemented. Nothing expires or is deleted automatically, and there is no per-user deletion endpoint. If you have obligations under GDPR, CCPA or similar, they are yours to meet by hand until this exists. Treat it as disqualifying for production use.

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.