Skip to content

Migrating from rrweb to rrweb cloud

This guide shows you how to migrate an existing rrweb installation and switch to using rrweb cloud's ingestion APIs.

If you don't already have rrweb in production, or if your rrweb customisations are all expressable through the rrweb recording config, you'll find it easier to follow our JavaScript SDK. If you are maintaining custom code which has been published as a PR, then it might be possible for us to pull in those modifications to produce a custom build of the client code for you, or work with you in getting those modifications upstreamed into rrweb itself.

The code samples given below are meant as a sketch only in order to give an idea how to adapt existing code which already imports and uses rrweb to update it to emit to our websocket and POST endpoints. For a more in-depth look at our current best practices, we recommend that you check out the production code in use by our official client, otherwise read on!

Sample Installation

you might have the rrweb package installed as follows:

bash
npm install @rrweb/record

Or included via CDN:

html
<script src="https://cdn.jsdelivr.net/npm/@rrweb/record@latest/dist/record.umd.min.cjs"></script>

Basic Manual Recording Setup

Reminder: this is already done if you use the JavaScript SDK, e.g. you can call rrwebCloud.getRecordingId() and rrwebCloud.start()

Initialize Recording

Start recording user interactions and send them to rrweb cloud:

javascript
// Generate a unique recording ID.  You should store this in
// sessionStorage as this will ensure each browser tab gets
// it's own id and events aren't interleaved between tabs.
// Keeping in sessionStorage allows recording of multiple
// consecutive page views to the same recording
const recordingId = window.crypto.randomUUID();

let wsReady = false;

// Buffer to store events before sending
let eventBuffer = [];

function bufferOrSendEvent(event) {
  if (wsReady) {
    sendEvent(event); // defined below
  } else {
    eventBuffer.push(event);
  }
}

const stopFn = rrweb.record({
  emit: (event) => {
    //
    // ... any existing event processing you might have
    // then:
    bufferOrSendEvent(event);
  },
  // Any custom rrweb config you have
  recordCanvas: true,
  inlineStylesheet: true,
  maskAllInputs: false,
  blockClass: "rr-block",
  ignoreClass: "rr-ignore",
  maskTextClass: "rr-mask",
});

Send Events to rrweb cloud

rrweb cloud provides two methods for ingesting events: WebSocket (recommended) and HTTP POST.

Reminder: this is already implemented for you in the JavaScript SDK

WebSocket ingestion is more efficient for real-time streaming:

javascript
ws = new WebSocket(
  `wss://api.rrwebcloud.com/recordings/${recordingId}/ingest/ws?contentType=application%2Fx-ndjson`,
);

function sendEvent(event) => {
  const eventStr = JSON.stringify(event) + "\n";
  if (eventStr.length < 1e6) {
    // size should be under websockets packet limit
    ws.send(eventStr);
  } else {
    // fallback method, defined below
    sendToRrwebCloud(eventStr)
  }
}

ws.addEventListener("open", () => {
  console.log("Connected to rrweb cloud");

  while (eventBuffer.length) {
    sendEvent(eventBuffer.shift()); // FIFO
  }
  wsReady = true;
});

ws.addEventListener("message", (event) => {
  const payload = JSON.parse(event.data);
  if (payload.type === "upstream-result") {
    console.log("Events ingested successfully:", payload.status);
  }
});

ws.addEventListener("error", (error) => {
  console.error("WebSocket error:", error);
  stopFn();
});

async function sendToRrwebCloud(body) {

  try {
    const response = await fetch(
      `https://api.rrwebcloud.com/recordings/${recordingId}/ingest`,
      {
        method: "POST",
        headers: {
          "Content-Type": "application/x-ndjson",
        },
        body: body,
        keepalive: body.length < 65000, // don't abort POST after end of recording (must be under the limit)
      },
    );
    return response;
  } catch (error) {
    console.error("Error sending events:", error);
    return false;
  }
}

// Clean up on page unload (not reliable on mobile)
window.addEventListener("beforeunload", () => {
  // if websocket never got established
  const eventStr = eventBuffer.map(JSON.stringify).join("\n");
  const response = await sendToRrwebCloud(eventStr);
  ws.close();
  stopFn();
});

HTTP POST Ingestion

For environments where WebSocket isn't available or desirable, you can periodically buffer and emit the events:

javascript

function sendEventsToRrwebCloud() {
  if (eventBuffer.length === 0) {
    return;
  }
  const eventStr = eventBuffer.map(JSON.stringify).join("\n");
  const eventBufferSize = eventBuffer.length;
  const response = await sendToRrwebCloud(eventStr); // defined above
  if (response.ok) {
    console.log("Events ingested successfully");
    eventBuffer = eventBuffer.slice(eventBufferSize); // Clear sent events
  } else {
    console.error("Failed to ingest events:", response.status);
  }
}
// Send events every 10 seconds
setInterval(sendEventsToRrwebCloud, 10 * 1000);

// Send events immediately if page is likely to be unloaded
document.addEventListener("visibilitychange", () => {
  if (document.hidden) {
    sendEventsToRrwebCloud();
  }
});

Recording Metadata

Attach searchable metadata to your recordings:

javascript
async function sendRecordingMetadata(metadata) {
  try {
    const response = await fetch(
      `https://api.rrwebcloud.com/recordings/${recordingId}/meta`,
      {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify(metadata),
      },
    );

    if (response.ok) {
      console.log("Metadata attached successfully");
    }
  } catch (error) {
    console.error("Error sending metadata:", error);
  }
}

// Send metadata when recording starts
sendRecordingMetadata({
  userId: "user-123",
  orgId: "org-456",
  plan: "pro",
  environment: "production",
  deviceType: /Mobi/.test(navigator.userAgent) ? "mobile" : "desktop",
});

// Update metadata when user logs in
function onUserLogin(user) {
  sendRecordingMetadata({
    userId: user.id,
    userEmail: user.email,
    userName: user.name,
    loginTime: new Date().toISOString(),
  });
}

Next Steps