Session replay
Record what your users did in the browser and watch it back in the Usero dashboard. As of @usero/sdk v1.2.0 you do not need the
feedback widget: one import and one .start() call records the session, with no UI and nothing added to the DOM. If you also run
the widget, each feedback submission deep-links to the exact moment in the recording where the user hit submit.
Recording uses rrweb and streams events to Usero as gzipped chunks while the user is on the page, so you capture the whole session rather than only the moments around a feedback submission.
Install
rrweb ships inside the replay chunk, so npm install @usero/sdk is the only install step. Replay lives in its own subpath
export (@usero/sdk/replay); consumers who never import it pay zero rrweb bytes. Even after you import it, rrweb lazy-loads at
runtime via dynamic import() only once a recording starts.
Standalone (no widget)
import { sessionReplay } from '@usero/sdk/replay'
sessionReplay({ clientId: 'client_71a4726fca9c4f13' }).start()That is the whole integration. If you know who the user is, pass getUser so replays show up under the right person:
const replay = sessionReplay({
clientId: 'client_71a4726fca9c4f13',
getUser: () => (auth.user ? { id: auth.user.id, email: auth.user.email } : null),
})
replay.start()
// Optional: end the recording early. Flushes buffered events and
// finalises the session server-side.
replay.stop()getUser is re-invoked at session start and at every chunk boundary, so a login that happens mid-session is picked up without
extra wiring. Returning null after a user was identified is treated as a logout.
.start() is idempotent: calling it while a recording is live anywhere on the page is a no-op. It is also a no-op during
server-side rendering, and logs an error if no clientId was provided. .stop() flushes, finalises, and tears down listeners;
calling .start() again afterwards begins a new replay session.
React
import { useSessionReplay } from '@usero/sdk/replay/react'
function App() {
useSessionReplay({ clientId: 'client_71a4726fca9c4f13' })
return <Routes />
}The hook is SSR-safe (a no-op on the server) and StrictMode-safe (the dev-mode double effect starts exactly one recording).
Recording is page-scoped: it survives the component unmounting on client-side route changes and ends when the page is hidden or
closed. The hook returns the replay instance, so you can call .stop() to end a recording early. Options are captured on first
render; to track a user who logs in mid-session, pass a getUser callback rather than changing options.
With the feedback widget
Pass the same factory to the widget's plugins array. Each feedback submission then carries a link to the exact
moment in the recording where the user hit submit, so you watch the bug instead of asking for reproduction steps. The replay also
feeds Usero's AI-drafted fix PRs: the recording is the context the AI reads before opening a pull
request.
import { initUseroFeedbackWidget } from '@usero/sdk'
import { sessionReplay } from '@usero/sdk/replay'
initUseroFeedbackWidget({
clientId: 'client_71a4726fca9c4f13',
plugins: [
sessionReplay({
// Wait 3s after load before starting. If the user navigates away
// first, rrweb is never fetched and no session is created.
startAfterMs: 3000,
// Record 50% of sessions. Decided once at init.
sampleRate: 0.5,
}),
],
})In plugin mode you do not pass clientId, user, or getUser: the widget's own configuration is authoritative.
Building your own feedback UI instead of the widget? The same plugins: [sessionReplay()] works with
the headless SDK, and submissions from your custom UI deep-link into the recording the same way.
The legacy import path @usero/sdk/plugins/session-replay still works and resolves to the same module. New code should import
from @usero/sdk/replay.
One recording per page
At most one replay recording runs per page, whichever way it was started:
- If a recording was started standalone and the feedback widget mounts later with a
sessionReplay()plugin, the widget does NOT start a second recorder. It links to the running recording: feedback submissions deep-link into it, and the widget takes over user resolution while mounted. - A widget unmount never kills a recording it merely adopted. Recordings are page-scoped.
.start()while any recording is live is a no-op, which is also what makes the React hook StrictMode-safe.
Options and privacy defaults
| Option | Default | What it does |
|---|---|---|
maskAllInputs |
true |
Mask <input> and <textarea> values in the recording. |
maskTextSelector |
'[data-usero-mask]' |
Mask text content of any node matching this selector. |
blockSelector |
'[data-usero-block]' |
Skip recording matching subtrees entirely. |
inlineStylesheet |
true |
Inline external stylesheets so replays render without network access. |
sampling |
{ mousemove: 50, scroll: 100 } |
Throttle high-frequency events. |
startAfterMs |
0 |
Delay in milliseconds before loading rrweb and starting the session. |
sampleRate |
1 |
Probability (0 to 1) that a given session records at all. |
Standalone-only options: clientId (required for .start()), user / getUser (identify the current user), and apiUrl
(override the API host, defaults to https://usero.io). In plugin mode these three are ignored. Advanced chunking knobs
(chunkSeconds, chunkMaxEvents, chunkMaxBytes, chunkMaxAttempts, checkoutEveryMs) are documented on the
SessionReplayOptions type.
Earlier SDK versions had a bufferSeconds option for a rolling in-memory buffer. It no longer exists: recording now streams
chunks continuously, and v1.2.0's strict option typing rejects it. Remove it from your config when upgrading.
Masking sensitive content
Tag nodes at the source. Text inside data-usero-mask is masked; subtrees inside data-usero-block are never recorded:
<div data-usero-mask>jane@example.com</div>
<section data-usero-block>
<!-- billing details, never recorded -->
</section>Inputs and textareas are masked by default via maskAllInputs.
How it links to feedback
When a recording is live at the moment a user submits feedback through the widget, the widget sends sessionReplayId and
replayOffsetMs alongside the submission. This works whether the recording was started by the plugin or adopted from an earlier
standalone .start(). In the dashboard, the feedback gets a "Watch session replay" link that opens the player at that offset, not
at the session start.
If you submit feedback through the API yourself, those two fields are how a replay gets attached.