Browser SDK

syncline-browser records the session with rrweb and mints the trace context that makes the stitch possible. It is written on the assumption that it is a guest in someone else’s page.

Setup

import { startRecording } from 'syncline-browser';

const recording = startRecording({
  key: 'pk_live_...',
  endpoint: 'https://syncline.example.com',
  traceOrigins: ['https://app.acme.com', 'https://api.acme.com'],
  release: 'web@2.4.1',
  user: { id: currentUser.id },
});

Options

OptionDefaultWhat it does
keyPublic project key. Safe to ship in a bundle; gated by the origin allowlist
endpointBase URL of your Syncline API
traceOriginspage originThe only origins that receive a traceparent
releaseTies a recording to a deploy
user{ id }, so you can find one person’s session
maskAllInputstrueMasks every input, textarea and select
captureErrorstrueUncaught errors and unhandled promise rejections
captureConsolefalsetrue for error and warn, or a list of levels
debugfalseSDK diagnostics to the console

Rules the SDK will not break

It will not break your page

Every patched path is wrapped, and any internal failure falls through to the original fetch or XMLHttpRequest. There is a test asserting that when both instrumentation hooks throw, the request still completes normally. A recording tool that takes down checkout is worse than no recording tool.

It will not inject cross-origin

traceparent goes only to origins on traceOrigins. Sending it to a third party would leak internal trace ids and — worse — add a header their CORS policy does not allow, turning a working request into a failed preflight. Subdomains do not match either: a third-party widget can be parked on one.

It will not trace itself

window.fetch is captured before the patch is installed, so the SDK’s own uploads and clock probes carry no header and never appear in their own recording.

What you have to do

Your API must allow the header:
Access-Control-Allow-Headers: traceparent
This is the single most common integration failure. Without it every traced request fails preflight, and it looks like the SDK broke your site rather than like a CORS setting.

How it behaves

  • Flush cadence. Every 5 seconds or 64 KB, whichever comes first. The final flush uses fetch with keepalive on pagehide, which survives the page closing and still sets headers.
  • Session identity. A ULID in sessionStorage with a 30-minute idle timeout. It survives navigation within a tab but not a new tab — two tabs are two recordings, and merging them would produce a replay whose DOM jumps between windows.
  • Requests in flight at a flush boundary roll into a later chunk rather than being reported with a guessed duration.
  • Compression. CompressionStream where available; an uncompressed body is a supported outcome, not a failure.

Sampling, inverted

A recorded session always sets sampled=1 on the traceparent, and standard OTel parent-based sampling honours it. You can never open the replay of a slow request whose spans were sampled away — which would be exactly the request you wanted.

Next: what gets recorded and how to mask it.