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 rejectsrecording_startrequests when this is off. - You're already running the main analytics tracker (
/js/main.js). Recording uses the samedata-fl-website-id, the same visitor and session cookies, and the same/eventsendpoint 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. Pour n'enregistrer qu'une partie des sessions, ajoutez recordingSampleRate, et pour n'analyser par IA qu'une partie de celles-ci, ajoutez aiSampleRate (tous deux 0–100, par défaut 100): consultez la section Échantillonnage. Everything below, the dashboard prerequisite, privacy modes, gates, and browser support, applies the same way.
Reverse-proxied setup (recommended for production)
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 path | Origin |
|---|---|
/js/main.js | https://cdn.flowsery.com/main.js |
/js/recording.js | https://cdn.flowsery.com/recording.js |
/api/track | https://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 (facultatif)
N'enregistre qu'un pourcentage des sessions. Accepte un nombre entier 0–100.
- Par défaut :
100: enregistre chaque session. Omettez l'attribut pour un enregistrement complet. - Exemple :
data-recording-sample="30"enregistre ~30 % des sessions.
<script defer data-fl-website-id="flid_******" data-recording-sample="30" src="https://cdn.flowsery.com/recording.js"></script>La décision est déterministe pour chaque session : un visiteur est enregistré ou ignoré de façon cohérente pendant toute sa session, d'un rechargement à l'autre et d'une navigation à l'autre, de sorte que vous n'obtenez jamais de visites à moitié enregistrées. Les sessions non échantillonnées ne téléchargent pas du tout l'enregistreur. Consultez la section Échantillonnage pour savoir comment cela se combine avec l'échantillonnage de l'analyse IA.
data-ai-sample (facultatif)
Parmi les sessions que vous enregistrez, n'en envoie qu'un pourcentage à l'analyse de session par IA. Accepte un nombre entier 0–100.
- Par défaut :
100: analyse chaque session enregistrée. - Exemple :
data-ai-sample="50"envoie ~50 % des sessions enregistrées à l'analyse IA.
<script defer data-fl-website-id="flid_******" data-recording-sample="30" data-ai-sample="50" src="https://cdn.flowsery.com/recording.js"></script>L'enregistreur transmet ce taux à Flowsery dans la poignée de main recording_start ; le serveur le stocke sur l'enregistrement et l'applique au moment où l'analyse est mise en file d'attente (le navigateur est alors parti depuis longtemps). Consultez la section Échantillonnage.
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.
Échantillonnage
Sur les sites à fort trafic, il est rarement nécessaire d'enregistrer chaque visite. Flowsery échantillonne en deux étapes indépendantes, toutes deux définies sur le script (ou le SDK npm): il n'y a rien à configurer dans le tableau de bord :
- Taux d'échantillonnage de l'enregistrement: la part des sessions qui sont réellement enregistrées. Se définit avec
data-recording-sample(ou l'option SDKrecordingSampleRate),0–100, par défaut100. C'est le navigateur qui décide, si bien que les sessions non échantillonnées ne téléchargent jamais l'enregistreur. - Taux d'échantillonnage de l'analyse IA: la part des sessions déjà enregistrées qui sont envoyées à l'analyse de session par IA. Se définit avec
data-ai-sample(ou l'option SDKaiSampleRate),0–100, par défaut100. L'enregistreur transmet ce taux à Flowsery dans la poignée de mainrecording_startet le serveur l'applique par enregistrement, car l'analyse IA s'exécute après la fermeture de la page.
Les deux se multiplient. Avec un taux d'enregistrement de 30 % et un taux d'IA de 50 %, ~30 % des sessions sont enregistrées et ~15 % (30 % × 50 %) obtiennent un verdict d'IA, les autres restent entièrement rejouables, mais ne sont simplement pas analysées par IA.
L'échantillonnage est déterministe, et non aléatoire : la décision d'enregistrement dépend de la session, si bien qu'un visiteur est enregistré (ou non) de façon cohérente pendant toute sa session et d'un rechargement à l'autre. Les valeurs par défaut de 100 conservent le comportement d'origine, tout enregistrer et tout analyser, de sorte que rien ne change tant que vous n'abaissez pas un taux.
How it works
- The script reads
data-fl-website-idfrom its own tag and resolves the visitor and session UIDs from the cookies set by the main tracker (_fs_vid,_fs_sid). - It POSTs
recording_startto/events. If the server rejects (recording disabled, quota exceeded, etc.) the script stops. - On accept, it loads rrweb and starts capturing events into an in-memory buffer.
- 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. - Lors de l'événement
pagehide/beforeunload, un dernier fragment est envoyé avecfinalize: truevia un transport résistant au déchargement de la page,navigator.sendBeaconpour les configurations proxifiées de même origine, ou une requêtefetchaveckeepalivepour les configurations directes (cross-origin): afin que le dernier fragment survive à la destruction de la page. The server flips the recording status toreadyso 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:
sessionStorageis per-tab; a new recording UID is generated on every tab open. Otherwise works. - Cookieless mode (
data-cookielesson 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
keepaliveconnection or via Beacon API on unload. - CPU: rrweb runs an idle MutationObserver: measurable but well under 1% CPU on typical pages. Offload via
requestIdleCallbackis built into rrweb 2.x. - Privacy: the recorder respects
Do Not Trackonly whendata-respect-dnt="true"is set. Otherwise it records all visitors who hitrecording_start. Configure in your dashboard.
Disable recording
Three options:
- Globally: turn off recording in the website settings. The server will reject
recording_startand 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_startPOST followed by periodicrecording_chunkPOSTs. - Check the response of
recording_start: ifaccepted: 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.
CompressionStreamwas 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.