# Session replay plugin

An opt-in plugin for the [widget](/docs/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.

```ts title=Vanilla
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,
		}),
	],
})
```

```tsx title=React
import { UseroFeedbackWidget } from '@usero/sdk/react'
import { sessionReplay } from '@usero/sdk/plugins/session-replay'

;<UseroFeedbackWidget clientId='YOUR_CLIENT_ID' plugins={[sessionReplay({ bufferSeconds: 30 })]} />
```

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 title=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`.

## How it links to feedback

When a recording exists, the widget sends `sessionReplayId` and `replayOffsetMs` alongside the feedback submission. If you submit
feedback through the [API](/docs/api/feedback) yourself, those two fields are how a replay gets attached.

## Next

- [Widget options](/docs/widget)
- [POST /api/feedback reference](/docs/api/feedback)
