import { EventType, type eventWithTime } from '@rrweb/types'; import { SessionType } from '@multiplayer-app/session-recorder-common'; import { logger } from '../utils'; import { type RecorderConfig, type EventRecorder } from '../types'; import { SocketService } from '../services/socket.service'; import { CrashBufferService } from '../services/crashBuffer.service'; import { ScreenRecorder } from './screenRecorder'; import { GestureRecorder } from './gestureRecorder'; import { NavigationRecorder } from './navigationRecorder'; export class RecorderReactNativeSDK implements EventRecorder { private isRecording = false; private generation = 0; private config?: RecorderConfig; private screenRecorder: ScreenRecorder; private gestureRecorder: GestureRecorder; private navigationRecorder: NavigationRecorder; private recordedEvents: eventWithTime[] = []; private socketService!: SocketService; private crashBuffer?: CrashBufferService; private bufferingEnabled: boolean = false; private bufferWindowMs: number = 2 * 60 * 1000; private sessionId: string | null = null; private sessionType: SessionType = SessionType.MANUAL; constructor() { this.screenRecorder = new ScreenRecorder(); this.gestureRecorder = new GestureRecorder(); this.navigationRecorder = new NavigationRecorder(); } init( config: RecorderConfig, socketService: SocketService, crashBuffer?: CrashBufferService, buffering?: { enabled: boolean; windowMs: number } ): void { this.config = config; this.socketService = socketService; this.crashBuffer = crashBuffer; this.bufferingEnabled = Boolean(buffering?.enabled); this.bufferWindowMs = Math.max( 10_000, buffering?.windowMs || 0.5 * 60 * 1000 ); this.screenRecorder.init(config, this); this.navigationRecorder.init(config, this.screenRecorder); this.gestureRecorder.init(config, this, this.screenRecorder); } start(sessionId: string | null, sessionType: SessionType): void { if (!this.config) { throw new Error( 'Configuration not initialized. Call init() before start().' ); } this.sessionId = sessionId; this.sessionType = sessionType; this.isRecording = true; this.generation++; // Emit recording started meta event if (this.config.recordScreen) { this.screenRecorder.setBufferOnlyMode( !sessionId && this.bufferingEnabled ); this.screenRecorder.start(); } if (this.config.recordGestures) { this.gestureRecorder.start(); } if (this.config.recordNavigation) { this.navigationRecorder.start(); } } stop(): void { this.isRecording = false; this.generation++; this.gestureRecorder.stop(); this.navigationRecorder.stop(); this.screenRecorder.stop(); this.socketService?.close(); } /** * Current recorder generation. Capture at the start of an async * capture/emit and re-check before publishing the event; a mismatch * means the recorder was stopped/restarted mid-flight and the event * would bleed across sessions (different node-id mirrors). */ getGeneration(): number { return this.generation; } setNavigationRef(ref: any): void { this.navigationRecorder.setNavigationRef(ref); } /** * Set the viewshot ref for screen capture * @param ref - React Native View ref for screen capture */ setViewShotRef(ref: any): void { this.screenRecorder.setViewShotRef(ref); } /** * Record an rrweb event * @param event - The rrweb event to record */ recordEvent(event: eventWithTime): void { if (!this.isRecording) { return; } // Buffer-only mode (no active debug session): persist locally. if (!this.sessionId && this.crashBuffer && this.bufferingEnabled) { void this.crashBuffer.appendEvent( { ts: event.timestamp, isFullSnapshot: event.type === EventType.FullSnapshot, event: { event: event, eventType: event.type, timestamp: event.timestamp, }, }, this.bufferWindowMs ); return; } if (this.socketService) { logger.debug( 'RecorderReactNativeSDK', 'Sending to socket service', event ); // Skip packing to avoid blob creation issues in Hermes // const packedEvent = pack(event) this.socketService.send({ event: event, // Send raw event instead of packed eventType: event.type, timestamp: event.timestamp, debugSessionId: this.sessionId, debugSessionType: this.sessionType, }); } } /** * Record touch start event * @param x - X coordinate * @param y - Y coordinate * @param target - Target element identifier * @param pressure - Touch pressure */ recordTouchStart( x: number, y: number, target?: string, pressure?: number ): void { if (!this.isRecording) { return; } this.gestureRecorder.recordTouchStart(x, y, target, pressure); } /** * Record touch move event * @param x - X coordinate * @param y - Y coordinate * @param target - Target element identifier * @param pressure - Touch pressure */ recordTouchMove( x: number, y: number, target?: string, pressure?: number ): void { if (!this.isRecording) { return; } this.gestureRecorder.recordTouchMove(x, y, target, pressure); } /** * Record touch end event * @param x - X coordinate * @param y - Y coordinate * @param target - Target element identifier * @param pressure - Touch pressure */ recordTouchEnd( x: number, y: number, target?: string, pressure?: number ): void { if (!this.isRecording) { return; } this.gestureRecorder.recordTouchEnd(x, y, target, pressure); } /** * Get all recorded events * @returns Array of recorded rrweb events */ getRecordedEvents(): eventWithTime[] { return [...this.recordedEvents]; } /** * Clear all recorded events */ clearRecordedEvents(): void { this.recordedEvents = []; } /** * Get recording statistics * @returns Recording statistics */ getRecordingStats(): { totalEvents: number; isRecording: boolean } { return { totalEvents: this.recordedEvents.length, isRecording: this.isRecording, }; } }