UNPKG

@perceptr/web-sdk

Version:

Perceptr Web SDK for recording and monitoring user sessions

238 lines (237 loc) 9.72 kB
import { EventType, record } from "rrweb"; import { getRecordConsolePlugin } from "@rrweb/rrweb-plugin-console-record"; import { ACTIVE_SOURCES, INCREMENTAL_SNAPSHOT_EVENT_TYPE, } from "./common/defaults"; import { sessionRecordingUrlTriggerMatches } from "./utils/sessionrecording-utils"; import { MutationRateLimiter } from "./common/services/MutationRateLimiter"; import { logger } from "./utils/logger"; export class SessionRecorder { constructor(config = {}) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y; this.events = []; this._isRecording = false; this._isPaused = false; this._isUrlBlocked = false; this.config = { staleThreshold: (_a = config.staleThreshold) !== null && _a !== void 0 ? _a : 3600000, // 1 hour default console: { lengthThreshold: (_c = (_b = config.console) === null || _b === void 0 ? void 0 : _b.lengthThreshold) !== null && _c !== void 0 ? _c : 1000, level: (_e = (_d = config.console) === null || _d === void 0 ? void 0 : _d.level) !== null && _e !== void 0 ? _e : ["log", "info", "warn", "error"], logger: (_g = (_f = config.console) === null || _f === void 0 ? void 0 : _f.logger) !== null && _g !== void 0 ? _g : "console", stringifyOptions: (_j = (_h = config.console) === null || _h === void 0 ? void 0 : _h.stringifyOptions) !== null && _j !== void 0 ? _j : { stringLengthLimit: 1000, numOfKeysLimit: 100, depthOfLimit: 10, }, }, urlBlocklist: (_k = config.urlBlocklist) !== null && _k !== void 0 ? _k : [], maxEvents: (_l = config.maxEvents) !== null && _l !== void 0 ? _l : 10000, sampling: { mousemove: (_o = (_m = config.sampling) === null || _m === void 0 ? void 0 : _m.mousemove) !== null && _o !== void 0 ? _o : 50, scroll: (_q = (_p = config.sampling) === null || _p === void 0 ? void 0 : _p.scroll) !== null && _q !== void 0 ? _q : 50, input: (_s = (_r = config.sampling) === null || _r === void 0 ? void 0 : _r.input) !== null && _s !== void 0 ? _s : "all", }, blockClass: (_t = config.blockClass) !== null && _t !== void 0 ? _t : "perceptr-block", ignoreClass: (_u = config.ignoreClass) !== null && _u !== void 0 ? _u : "perceptr-ignore", maskTextClass: (_v = config.maskTextClass) !== null && _v !== void 0 ? _v : "perceptr-mask", blockSelector: (_w = config.blockSelector) !== null && _w !== void 0 ? _w : "", maskTextSelector: (_x = config.maskTextSelector) !== null && _x !== void 0 ? _x : "", idleTimeout: (_y = config.idleTimeout) !== null && _y !== void 0 ? _y : 10000, // 10 seconds default }; // Internal mutation throttling configuration this._mutationConfig = { enabled: true, bucketSize: 100, refillRate: 10, }; // Initialize the mutation rate limiter this.mutationRateLimiter = new MutationRateLimiter(record, { bucketSize: this._mutationConfig.bucketSize, refillRate: this._mutationConfig.refillRate, onBlockedNode: (id, node) => { logger.debug(`Throttling mutations for node ${id}`, node); }, }); } startSession() { var _a, _b, _c, _d; if (this._isRecording) { return; } this.stopFn = record({ emit: (event) => { // Apply mutation rate limiting before processing the event if (this._mutationConfig.enabled) { const throttledEvent = this.mutationRateLimiter.throttleMutations(event); // If the event was completely throttled, don't process it if (throttledEvent) return; } this._canAddEvent(event); }, checkoutEveryNms: 10000, // takes a snapshot every 10 seconds event 2 plugins: [ // event type === '6' is console log getRecordConsolePlugin({ lengthThreshold: (_a = this.config.console) === null || _a === void 0 ? void 0 : _a.lengthThreshold, level: (_b = this.config.console) === null || _b === void 0 ? void 0 : _b.level, logger: (_c = this.config.console) === null || _c === void 0 ? void 0 : _c.logger, stringifyOptions: (_d = this.config.console) === null || _d === void 0 ? void 0 : _d.stringifyOptions, }), ], sampling: this.config.sampling, blockClass: this.config.blockClass, ignoreClass: this.config.ignoreClass, maskTextClass: this.config.maskTextClass, blockSelector: this.config.blockSelector, maskTextSelector: this.config.maskTextSelector, inlineStylesheet: true, recordCrossOriginIframes: true, }); this._isRecording = true; this._isPaused = false; this._resetIdleTimeout(); } stopSession() { if (!this._isRecording) { logger.warn("No active recording session"); return; } if (this.stopFn) { this.stopFn(); } this.events = []; this._isRecording = false; this._isPaused = false; if (this._idleTimeout) { clearTimeout(this._idleTimeout); } } pause() { if (!this._isRecording || this._isPaused) { return; } this._isPaused = true; logger.debug("Recording paused"); } resume() { if (!this._isRecording || !this._isPaused) { return; } this._isPaused = false; logger.debug("Recording resumed"); } _canAddEvent(event) { // If the event is an interactive event, resume the recording // otherwise the idle timeout will pause the recording if (this._isInteractiveEvent(event) && !this._isUrlBlocked) { this._resetIdleTimeout(); this.resume(); } // If the recording is paused, don't add the event if (this._isRecording && this._isPaused) { return false; } // Handle page view events if (event.type === EventType.Meta) { this._checkMetaEvent(event); } else { this._pageViewFallBack(); } this.events.push(event); if (this.events.length > this.config.maxEvents) { this.events.shift(); } return true; } _checkMetaEvent(event) { const href = event.data.href; if (!href) { return false; } this._lastHref = href; this._shouldBlockUrl(href); } _pageViewFallBack() { if (typeof window === "undefined" || !window.location.href) { return; } const currentUrl = window.location.href; if (this._lastHref !== currentUrl) { this._lastHref = currentUrl; this.addCustomEvent("$url_changed", { href: currentUrl }); this._shouldBlockUrl(currentUrl); } } _shouldBlockUrl(url) { if (typeof window === "undefined" || !window.location.href) { return; } const isNowBlocked = sessionRecordingUrlTriggerMatches(url, this.config.urlBlocklist); this._isUrlBlocked = isNowBlocked; if (isNowBlocked && !this._isPaused) { this.pause(); } else if (!isNowBlocked && this._isPaused) { this.resume(); } } _resetIdleTimeout() { if (this._idleTimeout) { clearTimeout(this._idleTimeout); } this._idleTimeout = setTimeout(() => { if (this._isRecording && !this._isPaused) { this.pause(); this._idleTimeout = undefined; } }, this.config.idleTimeout); } _isInteractiveEvent(event) { var _a; return (event.type === INCREMENTAL_SNAPSHOT_EVENT_TYPE && ACTIVE_SOURCES.indexOf((_a = event.data) === null || _a === void 0 ? void 0 : _a.source) !== -1); } /** * Get the recording events * @returns The recording events */ getRecordingEvents() { if (!this._isRecording) { throw new Error("No active recording session"); } return this.events; } onEvent(callback) { const originalAddEvent = this._canAddEvent; this._canAddEvent = (event) => { const canBeAdded = originalAddEvent.call(this, event); if (canBeAdded) { // if the event can be added, call the callback to add it to the buffer callback(event); } return canBeAdded; }; // Return a function to unsubscribe return () => { this._canAddEvent = originalAddEvent; }; } /** * Add a custom event to the recording * @param name - Event name * @param payload - Event data */ addCustomEvent(name, payload) { if (!this._isRecording) { logger.warn("Cannot add custom event: No active recording session"); return; } try { record.addCustomEvent(name, payload); } catch (error) { logger.error(`Failed to add custom event: ${name}`, error); } } }