getting-started

Session Recording with the Flowsery script

Session recording lets you replay what real visitors did on your site, mouse movement, clicks, form input, page transitions, by dropping a small script into your HTML. The recorder is a separate JS bundle from the main analytics tracker, so it only loads when you opt in.

The recorder is built on rrweb and ships its DOM mutation events to Flowsery as gzip-compressed chunks. The first chunk is a full snapshot; subsequent chunks are incremental.

Prerequisites

  • Recording must be enabled on the website in your Flowsery dashboard (Website → Settings → Recording). The server rejects recording_start requests when this is off.
  • You're already running the main analytics tracker (/js/main.js). Recording uses the same data-fl-website-id, the same visitor and session cookies, and the same /events endpoint as the main tracker.

Quick start (CDN)

Drop the script tag right after your main analytics tracker:

<script defer data-fl-website-id="flid_******" src="https://cdn.flowsery.com/main.js"></script>
<script defer data-fl-website-id="flid_******" src="https://cdn.flowsery.com/recording.js"></script>

The recorder runs the same gates as the main tracker (bot detection, localhost, iframe, file://, self-exclude) and bails out cleanly when any apply.

Prefer a single tag? Add the data-recording attribute to your main tracker and Flowsery injects the recorder for you, no second <script> needed:

<script defer data-fl-website-id="flid_******" data-recording src="https://cdn.flowsery.com/main.js"></script>

Enable via npm SDK

If you initialize Flowsery through the npm SDK, turn recording on with a single flag instead of a script tag:

import { initFlowsery } from 'flowsery';
 
await initFlowsery({
  websiteId: 'flid_******',
  recording: true,
});

The SDK lazy-loads the recorder bundle from Flowsery's CDN on first init, so rrweb never ships in your application bundle. Tracking events still flow through your apiBase when set. Um nur einen Teil der Sitzungen aufzuzeichnen, fügen Sie recordingSampleRate hinzu, und um nur einen Teil davon per KI zu analysieren, fügen Sie aiSampleRate hinzu (beide 0100, Standard 100): siehe Sampling. Everything below, the dashboard prerequisite, privacy modes, gates, and browser support, applies the same way.

Adblockers and tracking-protection extensions block third-party scripts and analytics.flowsery.com directly. Routing both the script and the events endpoint through your own domain works around that, visitor IPs and accuracy stay intact.

The recorder auto-derives the API base from its own <script src>, the same way the main tracker does. As long as the recording bundle is served under /js/recording.js on your domain, no extra configuration is needed.

Next.js

// next.config.js
async rewrites() {
  return [
    { source: '/js/main.js', destination: 'https://cdn.flowsery.com/main.js' },
    { source: '/js/recording.js', destination: 'https://cdn.flowsery.com/recording.js' },
    { source: '/api/track', destination: 'https://analytics.flowsery.com/analytics/events' },
  ];
}
<script defer data-fl-website-id="flid_******" src="/js/main.js"></script>
<script defer data-fl-website-id="flid_******" src="/js/recording.js"></script>

Other proxies

The same shape works for any reverse proxy. Map all three paths:

Public pathOrigin
/js/main.jshttps://cdn.flowsery.com/main.js
/js/recording.jshttps://cdn.flowsery.com/recording.js
/api/trackhttps://analytics.flowsery.com/analytics/events

For framework-specific proxy guides (Astro, Caddy, Express, FastAPI, Nginx, PHP, Vue, etc.), see the proxy support guides.

Configuration

The recorder reads its configuration from the same <script> tag attributes as the main tracker. Most options are inherited automatically; the ones below are recording-specific.

data-rrweb-src (optional)

Override the URL the recorder uses to load rrweb. Defaults to https://cdn.jsdelivr.net/npm/rrweb@2/dist/rrweb.min.js.

<script defer data-fl-website-id="flid_******" data-rrweb-src="/js/rrweb.min.js" src="/js/recording.js"></script>

Useful when your CSP forbids cdn.jsdelivr.net, or when you want to self-host rrweb to avoid third-party requests.

data-recording-sample (optional)

Zeichnet nur einen Prozentsatz der Sitzungen auf. Akzeptiert eine ganze Zahl 0100.

  • Standard: 100: jede Sitzung aufzeichnen. Lassen Sie das Attribut weg, um alles aufzuzeichnen.
  • Beispiel: data-recording-sample="30" zeichnet ~30 % der Sitzungen auf.
<script defer data-fl-website-id="flid_******" data-recording-sample="30" src="https://cdn.flowsery.com/recording.js"></script>

Die Entscheidung ist pro Sitzung deterministisch: Ein Besucher wird während seiner gesamten Sitzung: über Neuladen und Seitenwechsel hinweg, konsistent aufgezeichnet oder übersprungen, sodass Sie nie halb aufgezeichnete Besuche erhalten. Nicht ausgewählte Sitzungen laden den Recorder gar nicht erst herunter. Unter Sampling erfahren Sie, wie sich dies mit dem KI-Analyse-Sampling kombiniert.

data-ai-sample (optional)

Von den Sitzungen, die Sie aufzeichnen, wird nur ein Prozentsatz an die KI-Sitzungsanalyse gesendet. Akzeptiert eine ganze Zahl 0100.

  • Standard: 100: jede aufgezeichnete Sitzung analysieren.
  • Beispiel: data-ai-sample="50" sendet ~50 % der aufgezeichneten Sitzungen an die KI-Analyse.
<script defer data-fl-website-id="flid_******" data-recording-sample="30" data-ai-sample="50" src="https://cdn.flowsery.com/recording.js"></script>

Der Recorder sendet diese Rate im recording_start-Handshake an Flowsery; der Server speichert sie auf der Aufzeichnung und wendet sie an, wenn die Analyse in die Warteschlange gestellt wird (der Browser ist zu diesem Zeitpunkt längst weg). Siehe Sampling.

Privacy levels

The privacy mode is decided server-side per website (visible in the dashboard) and pushed to the recorder in the recording_start response. Available modes:

  • balanced (default), Masks password, email, and tel inputs. Other inputs and text are recorded as-is.
  • strict: Masks all inputs, masks all text content, blocks <video>, <audio>, and <canvas>, disables canvas recording.
  • relaxed: No masking. Only use for internal apps where you control the input.

To override the dashboard setting per page, you'd need to post-process recordings server-side; the script honors whatever the server returns.

Sampling

Auf Websites mit hohem Traffic müssen Sie selten jeden Besuch aufzeichnen. Flowsery samplet in zwei unabhängigen Stufen, die beide auf dem Script (oder über das npm-SDK) festgelegt werden, im Dashboard ist nichts einzustellen:

  1. Aufzeichnungs-Sampling-Rate: der Anteil der Sitzungen, die überhaupt aufgezeichnet werden. Festgelegt mit data-recording-sample (oder der SDK-Option recordingSampleRate), 0100, Standard 100. Der Browser entscheidet, sodass nicht ausgewählte Sitzungen den Recorder nie herunterladen.
  2. KI-Analyse-Sampling-Rate: der Anteil der bereits aufgezeichneten Sitzungen, die an die KI-Sitzungsanalyse gesendet werden. Festgelegt mit data-ai-sample (oder der SDK-Option aiSampleRate), 0100, Standard 100. Der Recorder sendet diese Rate im recording_start-Handshake an Flowsery, und der Server wendet sie pro Aufzeichnung an, da die KI-Analyse läuft, nachdem die Seite geschlossen wurde.

Beide werden multipliziert. Bei einer Aufzeichnungsrate von 30 % und einer KI-Rate von 50 % werden ~30 % der Sitzungen aufgezeichnet und ~15 % (30 % × 50 %) erhalten ein KI-Urteil: der Rest bleibt vollständig abspielbar, wird nur nicht per KI analysiert.

Das Sampling ist deterministisch, nicht zufällig: Die Aufzeichnungsentscheidung basiert auf der Sitzung, sodass ein Besucher während seiner gesamten Sitzung und über Neuladen hinweg konsistent aufgezeichnet wird (oder nicht). Die Standardwerte von 100 behalten das ursprüngliche Verhalten bei, alles aufzeichnen und analysieren, sodass sich nichts ändert, bis Sie eine Rate senken.

How it works

  1. The script reads data-fl-website-id from its own tag and resolves the visitor and session UIDs from the cookies set by the main tracker (_fs_vid, _fs_sid).
  2. It POSTs recording_start to /events. If the server rejects (recording disabled, quota exceeded, etc.) the script stops.
  3. On accept, it loads rrweb and starts capturing events into an in-memory buffer.
  4. Every 15 seconds (or sooner if the buffer fills past 500 events / 256 KB), the buffer is JSON-Lined → gzipped → base64-encoded → POSTed as a recording_chunk. The server stores chunks in R2 and rolls up metrics (event counts, click/input/error/rage-click flags, duration) on the recording row.
  5. Bei pagehide / beforeunload wird ein letzter Chunk mit finalize: true über einen entlade-sicheren Transport gesendet, navigator.sendBeacon für per Proxy geleitete Setups mit gleichem Origin, oder eine fetch-Anfrage mit keepalive für direkte (Cross-Origin-)Setups, sodass der letzte Chunk den Seitenabbau übersteht. The server flips the recording status to ready so it shows up in the dashboard's recordings list.

Browser support

  • Chrome/Edge 80+, Firefox 113+, Safari 16.4+: fully supported, gzip compression via CompressionStream.
  • Older Safari / Firefox: the recorder falls back to non-gzipped chunks. They're stored, but playback in the dashboard requires the gzip encoding, so older-browser recordings won't replay until decoder support catches up.
  • Safari Private Mode: sessionStorage is per-tab; a new recording UID is generated on every tab open. Otherwise works.
  • Cookieless mode (data-cookieless on the main tracker): recording is disabled because there's no visitor or session UID to attach.

Cost and performance

  • Network: typical session is 50–500 KB compressed; far less than a video. Chunks are sent every 15s on a keepalive connection or via Beacon API on unload.
  • CPU: rrweb runs an idle MutationObserver: measurable but well under 1% CPU on typical pages. Offload via requestIdleCallback is built into rrweb 2.x.
  • Privacy: the recorder respects Do Not Track only when data-respect-dnt="true" is set. Otherwise it records all visitors who hit recording_start. Configure in your dashboard.

Disable recording

Three options:

  • Globally: turn off recording in the website settings. The server will reject recording_start and the script will stop.
  • Per page: don't include the <script src="/js/recording.js"> tag.
  • Per visitor: set localStorage.setItem('flowsery_ignore', 'true'): same opt-out used by the main tracker.

Troubleshooting

Recordings tab is empty

  • Confirm recording is enabled on the website.
  • Open DevTools → Network. You should see a successful recording_start POST followed by periodic recording_chunk POSTs.
  • Check the response of recording_start: if accepted: false, the script stops; the response also tells you why (quota exceeded, recording disabled).

"This recording is too short to replay"

The recording exists but has fewer than 2 events. Common causes:

  • The visitor closed the tab before the first chunk could ship.
  • An adblocker blocked rrweb from loading. Check the Console for CSP or network errors.
  • CompressionStream was missing and the fallback encoding stored a chunk that the dashboard's player can't decode yet.

Recording loads but my reverse proxy bypass doesn't kick in

Make sure the script is served from /js/recording.js exactly. The auto-derive regex matches script.js, main.js, and recording.js (with optional .hash.js suffix); other paths fall through to the hardcoded analytics host. If you must serve from a different path, set data-api="https://yourdomain.com/api" explicitly.