# Session Replay

> Record and replay user sessions with privacy-first defaults. Configure sampling, masking, and error-triggered capture. View replays linked to errors and backend traces.

- Canonical: https://tracekit.dev/docs/frontend/session-replay

Record and replay user sessions to understand exactly what happened before, during, and after errors. Privacy-first by default -- all text and inputs are masked until you explicitly unmask them.

## Quick Start

Install the replay package and pass it as an addon to the [browser SDK](/docs/frontend/browser-sdk).

```bash
npm install @tracekit/replay
```

```ts
import { init } from '@tracekit/browser';
import { replayIntegration } from '@tracekit/replay';

init({
  apiKey: 'your-api-key',
  addons: [replayIntegration()],
});
```

Session replay captures DOM changes, network requests, and console output. All text content and input values are masked by default -- no sensitive data leaves the browser unless you explicitly configure unmasking.

### How It Works

1. **Recording starts on init().** The replay integration hooks into the browser SDK lifecycle. When `init()` is called, the recorder captures an initial DOM snapshot and begins observing mutations, scroll, and resize events.
2. **Events are buffered and compressed.** DOM mutations, network requests, and console output are collected in a buffer. A Web Worker gzip-compresses the data in the background (with a main-thread fallback).
3. **Chunks are uploaded periodically.** Every 30 seconds (configurable via `flushInterval`), compressed chunks are uploaded to TraceKit. On tab close, `sendBeacon` is used as a last-resort upload.
4. **Playback reconstructs the session.** The dashboard decompresses chunks and reconstructs the DOM in an iframe. Network requests and console output are displayed in synchronized side panels.

**Performance impact:** Replay uses mutation observers (not polling) and compresses data in a Web Worker. Typical overhead is less than 1% CPU on modern browsers. The SDK never crashes the host application -- all replay code is wrapped in try/catch.

## Configuration Reference

Pass a `ReplayConfig` object to `replayIntegration()`. All options are optional -- defaults are privacy-first and production-ready.

### Annotated Configuration

```ts
import { replayIntegration } from '@tracekit/replay';

const replay = replayIntegration({
  // Sampling
  sessionSampleRate: 0.1,        // 10% of all sessions (default: 0.1)
  errorSampleRate: 0.0,          // Error-triggered capture disabled (default: 0.0)

  // Privacy
  unmask: [],                    // CSS selectors to unmask (default: [])

  // Session lifecycle
  idleTimeout: 1800000,          // 30 minutes before session ends (default: 1800000)

  // Transport
  flushInterval: 30000,          // Upload every 30 seconds (default: 30000)
  maxBufferSize: 24117248,       // 23MB max buffer size (default: 24117248)

  // Media
  inlineImages: false,           // Capture images as base64 data URIs (default: false)
  blockMedia: true,              // Block img/video/canvas/svg/iframe (default: true)
});
```

### Options Reference

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `sessionSampleRate` | number | 0.1 | Percentage of sessions to record (0.0 to 1.0). Set to 0.1 to record 10% of sessions. |
| `errorSampleRate` | number | 0.0 | When an error occurs, capture the last 60 seconds of replay data. Set to 1.0 to capture all error sessions. |
| `unmask` | string[] | [] | CSS selectors for elements to unmask. Only matched elements show real text content; everything else stays masked. |
| `idleTimeout` | number | 1800000 | Milliseconds of inactivity before the session ends. Default is 30 minutes (1,800,000 ms). |
| `flushInterval` | number | 30000 | Milliseconds between automatic uploads of recorded events. Default is 30 seconds (30,000 ms). |
| `maxBufferSize` | number | 24117248 | Maximum buffer size in bytes before oldest events are dropped. Default is 23 MB (24,117,248 bytes). |
| `inlineImages` | boolean | false | Capture images as base64 data URIs in the recording. Increases payload size but makes images available during replay. |
| `blockMedia` | boolean | true | Replace media elements (img, video, canvas, svg, iframe) with placeholders. Set to false to capture media element structure. |

### Option Details

**sessionSampleRate** -- Controls the percentage of all sessions that are recorded from the very start. The sampling decision is made once when the session begins and does not change during the session. Valid range: `0.0` (no sessions) to `1.0` (all sessions). Values outside this range are clamped with a console warning.

**errorSampleRate** -- When set above 0, the SDK maintains a 60-second ring buffer of replay events. When an error is captured by `@tracekit/browser`, the buffer contents are flushed and uploaded. This means you get replay data for error sessions even if `sessionSampleRate` is low. Valid range: `0.0` (disabled) to `1.0` (all error sessions). The combined sum of sessionSampleRate + errorSampleRate is clamped to 1.0.

**unmask** -- An array of CSS selectors that identify elements whose text content should be visible in the replay. By default, all text is masked (replaced with asterisks). Use this to unmask public-facing content. Accepts any valid CSS selector: class (`.product-title`), ID (`#hero`), tag (`h1`), or attribute (`[data-public]`).

**idleTimeout** -- Milliseconds of user inactivity before the session is considered ended. After this timeout, the current recording is flushed and a new session starts on the next user interaction. The default of 30 minutes (1,800,000 ms) matches typical web session definitions.

**flushInterval** -- Milliseconds between automatic uploads of buffered replay events. Lower values reduce the risk of data loss on tab close but increase network requests. The default of 30 seconds (30,000 ms) provides a good balance.

**maxBufferSize** -- Maximum memory in bytes for the replay event buffer before oldest events are evicted. Controls memory usage on the client. The default of 23 MB (24,117,248 bytes) is sufficient for most sessions, including those with inline images enabled. Reduce on memory-constrained devices.

**inlineImages** -- When enabled, images are captured as base64 data URIs and embedded in the recording. This makes images visible during replay but significantly increases payload size. Best combined with a higher `maxBufferSize`.

**blockMedia** -- When enabled (default), media elements like images, videos, canvases, SVGs, and iframes are replaced with placeholders during recording. Set to `false` to capture their DOM structure. Combine with `inlineImages: true` to also capture image content.

## Privacy Controls

Session replay is privacy-first by design. All text content, input values, and media are masked by default. You opt in to showing content, not opt out.

### Default Behavior

- **All text content is masked.** Text nodes are replaced with asterisks of the same length in the recording.
- **All input values are masked.** Form fields, textareas, and selects show placeholder content only.
- **Media elements are blocked.** Images and videos are replaced with placeholder blocks in the recording.

### Unmasking Specific Elements

Use CSS selectors in the `unmask` config option to reveal specific content that is safe to record.

```ts
replayIntegration({
  unmask: [
    '.public-content',    // Marketing copy, product titles
    '.product-title',     // Product names on e-commerce pages
    '.nav-label',         // Navigation menu labels
    'h1', 'h2', 'h3',    // All headings
  ],
})
```

You can also unmask individual elements with an HTML attribute:

```html
<p data-tracekit-unmask>This text will be visible in the replay.</p>
```

### Always Masked (No Override)

**Password fields** and **credit card inputs** are always masked regardless of the `unmask` configuration or `data-tracekit-unmask` attribute. These elements cannot be unmasked for security reasons.

### Recommended Approach

**Start fully masked** (the default), then unmask only public-facing content like product names, navigation labels, and headings. Never unmask user-generated content, form inputs, or account-specific data without reviewing your privacy policy.

## Sampling

Session replay provides two independent sampling modes. A session can be captured by either mode -- they do not conflict.

**Session Sampling** -- `sessionSampleRate` controls what percentage of all sessions are recorded from the start. The decision is made when the session begins. Set to `0.1` for 10% of sessions. Set to `1.0` to record every session.

**Error Sampling** -- `errorSampleRate` enables a ring buffer that keeps the last 60 seconds of replay data. When an error is captured, the buffer is flushed and uploaded. Set to `1.0` to capture replay data for every session that encounters an error.

### Example Configurations

**Production (recommended)**

```ts
replayIntegration({
  sessionSampleRate: 0.1,   // Record 10% of sessions
  errorSampleRate: 1.0,     // Always capture replay for errors
})
```

Best balance of coverage and cost. You get a representative sample of all sessions, plus full replay for every error.

**Development**

```ts
replayIntegration({
  sessionSampleRate: 1.0,   // Record every session
  errorSampleRate: 0.0,     // No error-only capture needed
})
```

Full recording for testing and QA. Not recommended for production due to storage cost.

**High-traffic Production**

```ts
replayIntegration({
  sessionSampleRate: 0.01,  // Record 1% of sessions
  errorSampleRate: 1.0,     // Always capture replay for errors
})
```

Minimal session recording with full error coverage. Good for high-volume sites where 1% still gives thousands of replays per day.

### Sampling Interaction

Session sampling and error sampling are **independent**. When a new session starts, the SDK makes a sampling decision:

- **Session mode:** If the random value is below `sessionSampleRate`, the session is recorded from the start. All events are uploaded immediately.
- **Buffer mode:** If the session was not selected by session sampling but `errorSampleRate` is above 0, events are buffered in a 60-second ring buffer. If an error occurs, the buffer is flushed and recording continues.
- **Off:** If neither mode selects the session, no recording happens.

The combined sum of `sessionSampleRate` + `errorSampleRate` is clamped to 1.0. If the sum exceeds 1.0, `errorSampleRate` is reduced and a console warning is logged.

## API Reference

All exports are available from `@tracekit/replay`.

### `replayIntegration(config?: ReplayConfig): Integration & { flush(): void; getSessionId(): string }`

Create a session replay integration for @tracekit/browser. Pass the returned object in the addons array of init(). The integration starts recording immediately when the SDK initializes.

```ts
import { replayIntegration } from '@tracekit/replay';

const replay = replayIntegration({
  sessionSampleRate: 0.1,
  errorSampleRate: 1.0,
});

init({ apiKey: 'your-api-key', addons: [replay] });
```

### `flush(): void`

Force an immediate upload of pending replay events. Fire-and-forget -- does not return a promise. Useful for ensuring data is captured before a page transition or critical user action.

```ts
const replay = replayIntegration();
init({ apiKey: 'your-api-key', addons: [replay] });

// Later, on a critical action:
document.getElementById('checkout')?.addEventListener('click', () => {
  replay.flush();
});
```

### `getSessionId(): string | undefined`

Get the current replay session ID. Returns an empty string if replay is not active (e.g., the session was not sampled). Useful for custom integrations or logging.

```ts
const replay = replayIntegration();
init({ apiKey: 'your-api-key', addons: [replay] });

const sessionId = replay.getSessionId();
if (sessionId) {
  console.log('Replay session:', sessionId);
}
```

### ReplayConfig Type

| Field | Type | Description |
| --- | --- | --- |
| `sessionSampleRate` | number? | Session sample rate from 0.0 to 1.0. Default: 0.1 (10%) |
| `errorSampleRate` | number? | Error sample rate from 0.0 to 1.0. Default: 0.0 (disabled) |
| `unmask` | string[]? | CSS selectors for elements to unmask. Default: [] (all masked) |
| `idleTimeout` | number? | Idle timeout in milliseconds. Default: 1800000 (30 minutes) |
| `flushInterval` | number? | Upload interval in milliseconds. Default: 30000 (30 seconds) |
| `maxBufferSize` | number? | Maximum buffer size in bytes. Default: 10485760 (10 MB) |

## Playback UI

The replay viewer in the TraceKit dashboard provides a full-featured playback experience for recorded sessions.

- **Timeline Scrubber** -- Play, pause, and scrub through the session timeline. Click anywhere on the timeline to jump to a specific moment.
- **Speed Controls** -- Adjust playback speed: 0.5x, 1x, 2x, or 4x. Useful for quickly reviewing long sessions or slowing down to catch details.
- **Inactivity Skipping** -- Periods of user inactivity are automatically skipped during playback so you only see the moments that matter.
- **Network Tab** -- View network requests synchronized with the replay: method, URL, status code, duration, and timing relative to the session.
- **Console Tab** -- Console output (log, warn, error) is synchronized with the playback position. Expand objects and view stack traces inline.
- **Backend Traces** -- Network requests include "View Backend Trace" links that open the distributed trace waterfall for that specific API call.

### Session Card Metadata

Each session card in the replay list shows enriched metadata automatically collected by the SDK and backend:

| Indicator | Source | Description |
| --- | --- | --- |
| Country flag | CF-IPCountry header / GeoIP | Flag derived from the user's IP country code |
| Browser icon | User-Agent | Chrome, Firefox, Safari, or Edge icon parsed from User-Agent |
| Device icon | User-Agent | Desktop, mobile, or tablet icon based on User-Agent device detection |
| OS icon | User-Agent | macOS, Windows, Linux, iOS, or Android icon |
| Click count | SDK (rrweb events) | Total mouse clicks recorded during the session |
| Keypress count | SDK (rrweb events) | Total input events (keypresses) recorded during the session |
| Error dot | Error span linkage | Red indicator shown when errors were captured during the session |

Country detection uses the `CF-IPCountry` header when behind Cloudflare, with a MaxMind GeoLite2 fallback for other deployments. If no geo database is available, the country field is simply omitted.

### Network Request Details

The network tab in the replay viewer shows all HTTP requests made during the session, synchronized with the playback timeline:

| Column | Description |
| --- | --- |
| Method | HTTP method (GET, POST, PUT, DELETE, etc.) |
| URL | Request URL (truncated in the list, full URL in detail view) |
| Status | HTTP status code, color-coded (green for 2xx, yellow for 3xx/4xx, red for 5xx) |
| Duration | Request duration in milliseconds |
| Timeline | Visual position of the request relative to the replay timeline |

When distributed tracing is enabled (via `tracePropagationTargets` in the browser SDK config), network requests that include a `traceparent` header show a "View Backend Trace" link. Clicking it opens the distributed trace waterfall for that exact API call, connecting the frontend replay to the backend execution.

### Console Output

The console tab displays all `console.log`, `console.warn`, and `console.error` output, synchronized with the replay playback position. Error-level entries include expandable stack traces. Object arguments are displayed as expandable trees.

## Error Integration

When both `@tracekit/browser` and `@tracekit/replay` are installed, errors and replays are automatically linked. No additional configuration is required.

### How It Works

1. **Error captured.** When `@tracekit/browser` captures an error (via `captureException`, global error handler, or unhandled rejection), the replay integration intercepts the capture.
2. **replay_id attached.** The current session ID is automatically attached to the error event as a `replay_id` span attribute (using `setTag`).
3. **Ring buffer flushed.** If the session is in buffer mode (error sampling), the last 60 seconds of replay data are flushed and uploaded.

### "View Replay" Button

On the error detail page in the dashboard, a "View Replay" button appears when a `replay_id` is present on the error. Clicking it opens the session replay positioned 5 seconds before the error occurred (pre-roll), so you can see exactly what the user was doing leading up to the error.

**No additional configuration needed.** Just having both `@tracekit/browser` and `@tracekit/replay` installed enables the error-replay linkage. The `replay_id` is stored as a direct OTLP span attribute (not nested under "extra"), making it directly queryable in the dashboard.

### Error Sampling Flow

When `errorSampleRate` is greater than 0, the SDK operates in "buffer" mode for sessions that were not selected by session sampling. In buffer mode:

- Replay events are captured into a 60-second ring buffer (older events are evicted by timestamp).
- No data is uploaded until an error occurs.
- When an error is captured, the ring buffer contents are flushed and uploaded as a compressed chunk.
- After the flush, the session transitions to full recording mode for the remainder of the session.

This approach gives you full replay context for error sessions with minimal storage cost -- you only pay for sessions where errors actually occurred.

## Troubleshooting

### Privacy masking too aggressive

By default, all text is masked. To show specific content in replays, add CSS selectors to the `unmask` config:

```ts
replayIntegration({
  unmask: ['.public-content', '.product-title', 'h1', 'h2'],
})
```

You can also add `data-tracekit-unmask` to individual HTML elements.

### Sampling rate confusion

`sessionSampleRate` and `errorSampleRate` are independent. A session can be captured by either mode. If you set `sessionSampleRate: 0.1` and `errorSampleRate: 1.0`, 10% of sessions are recorded normally and 100% of error sessions have at least 60 seconds of replay data.

### Large recording sizes

Reduce `maxBufferSize` to limit memory usage, or lower `flushInterval` for more frequent uploads of smaller chunks. Replay data is gzip-compressed before upload, so network usage is typically 5-10x smaller than the raw buffer size.

### Web Worker compatibility

Replay compression runs in a Web Worker for zero-impact on the main thread. If Web Workers are not available (e.g., in certain sandboxed environments), the SDK falls back to main-thread gzip compression via `fflate`. Performance impact is minimal in either case.

### No replays appearing

Check these common causes:

- Verify `replayIntegration()` is passed in the `addons` array to `init()`.
- Check that `sessionSampleRate` or `errorSampleRate` is greater than 0.
- Ensure your API key has permissions for replay data ingestion.
- Look for `[TraceKit Replay]` warnings in the browser console.
- Confirm the browser SDK `enabled` flag is not set to `false`.

### Replay not linked to errors

The `replay_id` is attached via `setTag()` when an error is captured. If you are not seeing the "View Replay" button on error detail pages, check that: (1) the replay integration was passed in `addons` before the error occurred, (2) the session was sampled (either session or error mode), and (3) the replay data was successfully uploaded.

### Memory usage on long sessions

If users remain continuously active, reduce `maxBufferSize` to limit memory usage, or lower `flushInterval` to upload data more frequently.

## Complete Example

A production-ready setup with `@tracekit/browser` and `@tracekit/replay` together.

```ts
import { init, captureException, setUser } from '@tracekit/browser';
import { replayIntegration } from '@tracekit/replay';

// 1. Create the replay integration with production config
const replay = replayIntegration({
  sessionSampleRate: 0.1,       // Record 10% of sessions
  errorSampleRate: 1.0,         // Always capture replay for errors
  unmask: [
    '.public-content',          // Marketing copy
    '.product-title',           // Product names
    'h1', 'h2', 'h3',          // Headings
    '.nav-label',               // Navigation labels
  ],
});

// 2. Initialize the browser SDK with replay as an addon
init({
  apiKey: process.env.TRACEKIT_API_KEY!,
  release: process.env.APP_VERSION || '0.0.0',
  environment: process.env.NODE_ENV || 'production',
  tracePropagationTargets: [
    'https://api.myapp.com',
    /^https:\/\/.*\.myapp\.com/,
  ],
  addons: [replay],
});

// 3. Set user context after authentication
async function onLogin(email: string, password: string) {
  const user = await api.login(email, password);
  setUser({ id: user.id, email: user.email });
}

// 4. Manual error capture -- replay_id is attached automatically
async function loadDashboard() {
  try {
    const data = await api.fetchDashboard();
    renderDashboard(data);
  } catch (err) {
    captureException(err as Error);
    showErrorFallback();
  }
}

// 5. Force flush on critical user actions
document.getElementById('checkout')?.addEventListener('click', () => {
  replay.flush();
});

// 6. Access the session ID for custom integrations
const sessionId = replay.getSessionId();
if (sessionId) {
  analytics.track('session_started', { replayId: sessionId });
}
```

**Note on tab close:** At most 30 seconds of replay data may be lost when a user closes the tab, due to the flush interval. The SDK uses `sendBeacon` as a fallback on `visibilitychange` to minimize this window, but some data loss is possible in aggressive browser shutdown scenarios.
