replay-tracker
Version:
A lightweight session replay tracker for websites using rrweb
325 lines • 10.5 kB
JavaScript
// src/index.ts
import {
S3Client,
PutObjectCommand,
GetObjectCommand
} from "@aws-sdk/client-s3";
function createTracker(opts = {}) {
console.log("createTracker", opts);
const options = {
apiKey: "developer1@",
autoUpload: true,
debug: false,
// Default debug to false
...opts
};
let rrwebInstance = options.rrwebInstance;
const log = (message, data) => {
if (options.debug) {
if (data) {
console.log(`%c${message}`, "color: #0066FF; font-weight: bold;", data);
} else {
console.log(`%c${message}`, "color: #0066FF; font-weight: bold;");
}
}
};
const logError = (message, error) => {
if (error) {
console.error(
`%c[Replay Tracker ERROR] ${message}`,
"color: #FF0000; font-weight: bold;",
error
);
} else {
console.error(
`%c[Replay Tracker ERROR] ${message}`,
"color: #FF0000; font-weight: bold;"
);
}
};
log("[Replay Tracker] Creating tracker with options:", opts);
if (typeof window === "undefined") {
log(
"[Replay Tracker] Non-browser environment detected, returning dummy tracker"
);
return {
start: async () => {
},
stop: () => {
},
uploadToWasabi: async () => void 0,
getRecording: async () => void 0,
replay: async () => {
}
};
}
log("[Replay Tracker] Initialized with options:", options);
let stopFn;
const events = [];
let uploadQueue = [];
let isUploading = false;
async function loadRrweb() {
if (rrwebInstance) {
return rrwebInstance;
}
try {
if (typeof window !== "undefined" && window.rrweb) {
rrwebInstance = window.rrweb;
return rrwebInstance;
}
rrwebInstance = await import("rrweb");
return rrwebInstance;
} catch (error) {
logError(
"Failed to load rrweb. Please ensure it's installed in your project:",
error
);
logError("Install it with: npm install rrweb or yarn add rrweb");
logError("Learn more: https://github.com/rrweb-io/rrweb");
throw new Error("rrweb not available. See console for details.");
}
}
function getWasabiConfig() {
return {
accessKeyId: "KZJEBQ80IMI8JH1KNI0X",
secretAccessKey: "Jom6QsacCpLEmVtPyRfLiAdldHkV3lGu8sAoQvzY",
endpoint: "https://s3.ap-southeast-1.wasabisys.com",
bucket: "replays",
region: "ap-southeast-1"
};
}
function createS3Client() {
const wasabiConfig = getWasabiConfig();
return new S3Client({
region: wasabiConfig.region,
endpoint: wasabiConfig.endpoint,
credentials: {
accessKeyId: wasabiConfig.accessKeyId,
secretAccessKey: wasabiConfig.secretAccessKey
},
forcePathStyle: true
// Required for Wasabi
});
}
async function uploadToWasabiImpl(filename = `replay-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.json`) {
log("[Replay Tracker] Preparing to upload to Wasabi, filename:", filename);
if (typeof window === "undefined" || events.length === 0) {
log(
"[Replay Tracker] Upload skipped - non-browser environment or no events"
);
return void 0;
}
try {
const wasabiConfig = getWasabiConfig();
log("[Replay Tracker] Using Wasabi configuration:", {
endpoint: wasabiConfig.endpoint,
bucket: wasabiConfig.bucket,
region: wasabiConfig.region
});
if (!wasabiConfig.accessKeyId || !wasabiConfig.secretAccessKey || !wasabiConfig.bucket) {
logError(
"Missing required Wasabi configuration. Check environment variables or options."
);
return void 0;
}
const recordingData = {
events,
metadata: {
apiKey: options.apiKey,
userAgent: window.navigator.userAgent,
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
url: window.location.href
}
};
log(
`[Replay Tracker] Preparing data for upload. Events count: ${events.length}`
);
const jsonData = JSON.stringify(recordingData);
log("[Replay Tracker] Creating S3 client for Wasabi");
const s3Client = createS3Client();
log("[Replay Tracker] Sending data to Wasabi");
await s3Client.send(
new PutObjectCommand({
Bucket: wasabiConfig.bucket,
Key: filename,
Body: jsonData,
ContentType: "application/json"
})
);
const fileUrl = `https://${wasabiConfig.bucket}.${wasabiConfig.endpoint.replace(/^https?:\/\//, "")}/${filename}`;
log("wasabiConfig", wasabiConfig);
log("filename", filename);
log("fileUrl", fileUrl);
log(`[Replay Tracker] Recording uploaded to Wasabi: ${fileUrl}`);
return fileUrl;
} catch (error) {
logError("Failed to upload to Wasabi:", error);
return void 0;
}
}
async function getRecordingImpl(filename) {
log("[Replay Tracker] Fetching recording from Wasabi:", filename);
try {
const wasabiConfig = getWasabiConfig();
const s3Client = createS3Client();
const response = await s3Client.send(
new GetObjectCommand({
Bucket: wasabiConfig.bucket,
Key: filename
})
);
if (!response.Body) {
throw new Error("No body in response");
}
const streamToString = async (stream) => {
const chunks = [];
return new Promise((resolve, reject) => {
stream.on("data", (chunk) => chunks.push(chunk));
stream.on("error", reject);
stream.on(
"end",
() => resolve(Buffer.concat(chunks).toString("utf-8"))
);
});
};
const bodyContents = await streamToString(response.Body);
const recordingData = JSON.parse(bodyContents);
log("[Replay Tracker] Successfully retrieved recording data");
return recordingData;
} catch (error) {
logError("Failed to get recording from Wasabi:", error);
return null;
}
}
async function replayEvents(events2, container, options2 = {}) {
log("[Replay Tracker] Setting up replay for events:", events2.length);
try {
const rrweb = await loadRrweb();
const defaultOptions = {
speed: 1,
showController: true,
...options2
};
log("[Replay Tracker] Creating replayer with options:", defaultOptions);
const replayer = new rrweb.Replayer(events2, {
...defaultOptions,
target: container
});
replayer.play();
log("[Replay Tracker] Replay started");
} catch (error) {
logError("Failed to initialize replayer:", error);
}
}
async function processUploadQueue() {
log(
`[Replay Tracker] Processing upload queue. Queue length: ${uploadQueue.length}`
);
if (isUploading || uploadQueue.length === 0) {
log(
`[Replay Tracker] Upload queue processing skipped. isUploading: ${isUploading}`
);
return;
}
isUploading = true;
try {
const nextUpload = uploadQueue.shift();
if (nextUpload) {
log("[Replay Tracker] Processing next upload in queue");
await nextUpload;
log("[Replay Tracker] Upload completed");
}
} finally {
isUploading = false;
if (uploadQueue.length > 0) {
log(
`[Replay Tracker] More items in queue (${uploadQueue.length}), continuing processing`
);
processUploadQueue();
} else {
log("[Replay Tracker] Upload queue is now empty");
}
}
}
function send(opts2, event) {
log("[Replay Tracker] Recording event:", {
apiKey: opts2.apiKey,
eventType: event.type,
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
userAgent: typeof window !== "undefined" ? window.navigator.userAgent : "non-browser environment"
});
if (opts2.autoUpload && event) {
const eventType = event.type;
if (eventType === 4 || // Mouse interaction events
events.length % 50 === 0) {
log(
`[Replay Tracker] Auto upload triggered. Event type: ${eventType}, Events count: ${events.length}`
);
uploadQueue.push(uploadToWasabiImpl());
processUploadQueue();
}
}
}
return {
async start() {
log("[Replay Tracker] Starting recording session");
if (stopFn) {
log("[Replay Tracker] Recording already in progress");
return;
}
events.length = 0;
uploadQueue = [];
log("[Replay Tracker] Cleared previous events and upload queue");
try {
const rrweb = await loadRrweb();
log("[Replay Tracker] Initializing rrweb recorder");
stopFn = rrweb.record({
emit: (event) => {
send(options, event);
events.push(event);
},
maskTextSelector: options.maskText ? "*" : void 0
});
log("[Replay Tracker] rrweb recorder initialized successfully");
} catch (error) {
logError("Failed to start recording:", error);
}
},
stop() {
log("[Replay Tracker] Stopping recording session");
if (stopFn) {
stopFn();
stopFn = void 0;
log("[Replay Tracker] Recording stopped");
} else {
log("[Replay Tracker] No active recording to stop");
}
if (options.autoUpload && events.length > 0) {
log(
`[Replay Tracker] Auto uploading final recording. Events count: ${events.length}`
);
uploadQueue.push(uploadToWasabiImpl());
processUploadQueue();
}
},
// Public method kept for compatibility
async uploadToWasabi(filename = `replay-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.json`) {
log(`[Replay Tracker] Manual upload requested. Filename: ${filename}`);
return uploadToWasabiImpl(filename);
},
// Public method to get a recording from Wasabi
async getRecording(filename) {
log(`[Replay Tracker] Retrieving recording: ${filename}`);
return getRecordingImpl(filename);
},
// Public method to replay events
async replay(events2, container, options2 = {}) {
log("[Replay Tracker] Replay method called with events:", events2?.length);
await replayEvents(events2, container, options2);
}
};
}
export {
createTracker
};
//# sourceMappingURL=index.js.map