Session replay plugin

An opt-in plugin for the widget that attaches a rolling recording (the last 30 seconds by default) to each feedback submission. When a user reports a bug, you watch exactly what they did instead of asking for reproduction steps.

Recordings use rrweb and are gzipped in the browser via the native CompressionStream API. In the dashboard, each feedback with a replay gets a "Watch session replay" link that opens the player at the moment the feedback was given.

Install

rrweb ships inside the plugin chunk, so npm install @usero/sdk is the only install step. The plugin lives in a subpath export; consumers who never import it pay zero rrweb bytes.

import { initUseroFeedbackWidget } from '@usero/sdk'
import { sessionReplay } from '@usero/sdk/plugins/session-replay'

initUseroFeedbackWidget({
	clientId: 'YOUR_CLIENT_ID',
	plugins: [
		sessionReplay({
			bufferSeconds: 30,
			// Wait 3s of engagement before loading rrweb. If the user navigates
			// away first, rrweb is never fetched.
			startAfterMs: 3000,
			// Record 50% of sessions. Decided once at init.
			sampleRate: 0.5,
		}),
	],
})

Even when the plugin is imported, rrweb itself lazy-loads at runtime via dynamic import() the first time the engagement gate elapses, so opted-in consumers do not pay rrweb's bytes upfront either.

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 offline.
sampling { mousemove: 50, scroll: 100 } Throttle high-frequency events.
bufferSeconds 30 Length of the rolling in-memory buffer in seconds.
startAfterMs 0 Engagement gate in milliseconds before loading rrweb.
sampleRate 1 Probability (0 to 1) that a given session records at all.

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 exists, the widget sends sessionReplayId and replayOffsetMs alongside the feedback submission. If you submit feedback through the API yourself, those two fields are how a replay gets attached.

Next