UNPKG

replay-tracker

Version:

A lightweight session replay tracker for websites using rrweb

325 lines 10.5 kB
// 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