replay-tracker
Version:
A lightweight session replay tracker for websites using rrweb
356 lines (355 loc) • 12.1 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
createTracker: () => createTracker
});
module.exports = __toCommonJS(index_exports);
var import_client_s3 = require("@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 import_client_s3.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 import_client_s3.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 import_client_s3.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);
}
};
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
createTracker
});
//# sourceMappingURL=index.cjs.map