Every layer of your stack, folded onto one timeline.
Syncline replays a user session as video and the backend distributed trace and the SQL queries that ran — on one synchronized scrubber.
I.The problem
Three tools. One clock.
Shows you the browser. The click, the hesitation, the field they gave up on — and then it stops at the network boundary.
Shows you the backend. Spans, services, the query that took 1.8 seconds — with no idea which person was sitting there waiting on it.
Neither shows you this. So “checkout felt slow” is still archaeology: eyeball a video, guess a timestamp, go hunting in another tool with a different clock.
II.How the stitch works
Four steps. None of them yours.
- 01The browser mints the trace id. The SDK patches fetch and XHR and attaches a W3C traceparent to requests bound for origins you list — never to anyone else’s.
- 02It writes that id into the recording. An rrweb custom event lands at the exact frame the request fired, so the replay carries its own index.
- 03Your backend does nothing special. Standard OpenTelemetry reads the header and continues the trace. Database spans inherit it.
- 04The viewer resolves the seam. Player time, to trace id, to span tree — drawn beneath the video on the same clock.
III.The whole integration
No backend SDK. Ever.
If your services already emit OpenTelemetry, the backend change is two environment variables.
import { startRecording } from 'syncline-browser';
startRecording({
key: 'pk_live_...',
endpoint: 'https://syncline.example.com',
traceOrigins: ['https://api.acme.com'],
});IV.What makes it different
Six decisions. Each one load-bearing.
The link is an id, not a timestamp
The browser mints a W3C traceparent and writes it into the replay stream at the frame the request fired. Clock skew can misdraw a lane by milliseconds; it can never attribute a request to the wrong trace.
Sampling runs backwards
The browser decides. A recorded session forces sampled=1, so you never open the replay of a slow request whose spans were thrown away — which is exactly the request you wanted.
No backend SDK, ever
Syncline is a plain OTLP sink. Point any OpenTelemetry exporter at it and the trace continues. It can sit beside your existing tracing vendor through a collector fan-out instead of replacing it.
The recording explains itself
Trace ids live inside the rrweb stream, not in a side table. Export a session to a file, hand it to someone else, and it still resolves to its spans.
Masked by default
Inputs are masked unless you opt out. Query values are stripped from recorded URLs and fragments dropped entirely. Headers and request bodies are never captured at all.
Your infrastructure
Postgres, Redis, and any S3-compatible store. Session recordings are the most sensitive telemetry you will ever collect, and none of it leaves your network.
V.In the viewer
Drag to 00:42. See all of it at once.
The user clicked Checkout, that fired POST /api/checkout, which fanned out to four spans, one of which was a query that took 1.8 seconds. One scrubber, one answer.
Three strata, one clock
Network, backend and database lanes are drawn beneath the replay and read the player’s own time every frame. The video is the master clock, so nothing can drift out from under it.
Click a request to zoom
Selecting a bar focuses the timeline on that request with room either side, and one control returns you to the whole recording.
The full span, not a summary
Every attribute the span carried — db.statement, the HTTP status, the service that served it, the trace id — is there to read, not rolled up into an average.
An honest error bar
When the round trip to the browser was slow enough for the two clocks to disagree, the viewer draws the uncertainty band rather than a confidently wrong line.
Running in about five minutes.
Docker for Postgres, Redis and MinIO. Three Node processes. One seeded project key.