Skip to content

Build a replay UI

Place your application's authorization boundary before rrweb Cloud. A private API key (secret_key_rr_*) has full read and write access. It must remain in a trusted server or secret manager. Never expose a private API key in browser code. A public write key can ingest events, but it cannot fetch recordings.

The pattern below has two parts: your server authenticates the viewer and asks Cloud for a scoped event URL, then the browser downloads that event stream and hands it to rrwebPlayer.

Fetch replay access from a trusted server

Store the private API key as a server secret, apply your own authorization checks, then expose a server route that asks rrweb Cloud for a scoped, signed event URL and returns only the metadata fields the replay UI needs.

Store RRWEB_CLOUD_PRIVATE_API_KEY as a server secret. Its value starts with secret_key_rr_; do not hardcode it. After applying your own authorization checks, expose a server route such as /api/replay-access/:recordingId that runs this function:

javascript
const rrwebCloudApi = 'https://api.rrweb.com';

export async function getReplayAccess(recordingId) {
  const detailUrl = new URL(`/recordings/${recordingId}`, rrwebCloudApi);
  detailUrl.searchParams.set('includeSignedUrls', 'true');

  const response = await fetch(detailUrl, {
    method: 'GET',
    headers: {
      Authorization: `Bearer ${process.env.RRWEB_CLOUD_PRIVATE_API_KEY}`,
      Accept: 'application/json',
    },
  });

  if (!response.ok) {
    throw new Error(`rrweb Cloud returned ${response.status}`);
  }

  const recording = await response.json();
  if (!recording.links.eventsSigned) {
    throw new Error('A signed event URL was not available');
  }

  return {
    recordingId: recording.recordingId,
    metadata: browserSafeMetadata(recording.metadata),
    eventsUrl: new URL(recording.links.eventsSigned, rrwebCloudApi).toString(),
  };
}

// Metadata can carry server-only fields such as support case ids or account
// segments. Return only the keys the replay UI needs.
const browserSafeMetadataKeys = ['environment', 'app_version', 'feature_flag'];

function browserSafeMetadata(metadata = {}) {
  return Object.fromEntries(
    Object.entries(metadata).filter(([key]) =>
      browserSafeMetadataKeys.includes(key),
    ),
  );
}

This uses Get recording. Its normal response is { recordingId, metadata, links }; setting includeSignedUrls=true adds eventsSigned when signing is configured. Return the result only after verifying that the signed-in application user may view that recording, and allowlist the metadata keys you send back: application metadata can include server-only classifications that must not reach the browser.

You may instead fetch Get recording events on the server and proxy the returned top-level event array. A scoped URL avoids routing the event body through your server, while a proxy gives your server tighter response control.

Initialize the player in the browser

Install the player and its stylesheet:

bash
npm install rrweb-player

Add a mount element to the page before the module runs:

html
<div id="replay"></div>

The example renders a 1024 by 576 pixel player. Make sure the surrounding layout can accommodate that size, or choose dimensions that fit your replay view.

Then ask your own authenticated route for replay access, fetch the scoped event array, and initialize the player. This example uses a valid recording UUID and does not use a write credential to read data:

javascript
import rrwebPlayer from 'rrweb-player';
import 'rrweb-player/dist/style.css';

const replayTarget = document.querySelector('#replay');
if (!(replayTarget instanceof HTMLElement)) {
  throw new Error('Replay mount element #replay was not found');
}

const recordingId = '550e8400-e29b-41d4-a716-446655440000';
const accessResponse = await fetch(`/api/replay-access/${recordingId}`, {
  credentials: 'same-origin',
});

if (!accessResponse.ok) {
  throw new Error(`Replay access failed: ${accessResponse.status}`);
}

const { eventsUrl } = await accessResponse.json();
const eventsResponse = await fetch(eventsUrl);

if (!eventsResponse.ok) {
  throw new Error(`Event retrieval failed: ${eventsResponse.status}`);
}

const events = await eventsResponse.json();
if (!Array.isArray(events) || events.length === 0) {
  throw new Error('The recording has no replayable events');
}

const player = new rrwebPlayer({
  target: replayTarget,
  props: {
    events,
    width: 1024,
    height: 576,
    autoPlay: false,
    showController: true,
    skipInactive: true,
    mouseTail: true,
  },
});

Destroy the component with player.$destroy() when its container is removed. For controls, plugins, large event streams, and failure diagnosis, continue to Advanced replay and troubleshooting.