Migrating from rrweb to rrweb Cloud
This guide shows you how to migrate an existing rrweb installation and switch to using rrweb Cloud's recording 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 Browser Client. 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, use the Browser Client or inspect the @rrweb/browser-client package, otherwise read on!
Sample Installation
you might have the rrweb package installed as follows:
npm install @rrweb/recordOr included via CDN:
<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 Browser Client, e.g. you can call getRecordingId() and start()
Initialize Recording
Start recording user interactions and send them to rrweb Cloud:
// 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 appending events: WebSocket (recommended) and HTTP POST.
WebSocket event streaming (Recommended)
Reminder: this is already implemented for you in the Browser Client
WebSocket event streaming is more efficient for real-time streaming:
ws = new WebSocket(
`wss://api.rrweb.com/recordings/${recordingId}/events/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 accepted successfully:", payload.status);
}
});
ws.addEventListener("error", (error) => {
console.error("WebSocket error:", error);
stopFn();
});
async function sendToRrwebCloud(body) {
try {
const response = await fetch(
`https://api.rrweb.com/recordings/${recordingId}/events`,
{
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 event collection
For environments where WebSocket isn't available or desirable, you can periodically buffer and emit the events:
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 accepted successfully");
eventBuffer = eventBuffer.slice(eventBufferSize); // Clear sent events
} else {
console.error("Failed to append 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:
async function sendRecordingMetadata(metadata) {
try {
const response = await fetch(
`https://api.rrweb.com/recordings/${recordingId}/metadata`,
{
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(),
});
}