Snippets use your anonymous clientIdSign up to keep it.

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)

Vanilla
import { sessionReplay } from '@usero/sdk/replay'

sessionReplay({ clientId: 'client_0be4c23952704e6f' }).start()

That is the whole integration. If you know who the user is, pass getUser so replays show up under the right person:

Vanilla
const replay = sessionReplay({
	clientId: 'client_0be4c23952704e6f',
	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

React
import { useSessionReplay } from '@usero/sdk/replay/react'

function App() {
	useSessionReplay({ clientId: 'client_0be4c23952704e6f' })
	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_0be4c23952704e6f',
	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:

HTML
<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.

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.

Traffic source: where the visitor came from

Every recording stores where the visitor arrived from, captured automatically when the session starts. Nothing to configure, and no campaign parameters required.

Field Stored on What it holds
referrer SessionReplay Raw document.referrer at the moment recording starts, or null if empty.
startUrl SessionReplay The full URL of the page the visitor landed on.
referrer Feedback Raw document.referrer at the moment the report was submitted.
pageUrl Feedback The full URL of the page the report was submitted from.

All four are stored as raw URLs, so the path and query string survive, not only the domain.

The session detail page renders the referrer as a friendly source label beside the timestamp and duration. Hover it to see the raw referrer URL. The label is derived like this:

  • A known host shows its brand name (news.ycombinator.com renders as "Hacker News", any google.* regional domain renders as "Google").
  • An unrecognised host shows the bare host, with www. stripped.
  • Your own host renders as "Direct / Internal". Usero derives your host from the session's startUrl, so a visitor clicking from one of your pages to another is not logged as a traffic source. Without this collapsing, your own domain would show up as your top referrer.
  • No referrer, or an unparseable one, renders as "Direct". That covers typed URLs, bookmarks, links opened from native apps and chat clients, and sites sending a strict Referrer-Policy.

For what you can do with these fields, see traffic source capture.

Next