From fac0b405d30ea2b9b6bd76920a03dfbd96d4951c Mon Sep 17 00:00:00 2001 From: JunghwanNA <70629228+shaun0927@users.noreply.github.com> Date: Thu, 16 Apr 2026 11:58:16 +0900 Subject: [PATCH] fix: handle recording discard and write-failure in cursor telemetry buffer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address two issues raised during review: P1 – When a recording is cancelled or restarted, setRecordingState(false) enqueues its cursor batch but store-recorded-session is never called, leaving a stale batch that contaminates the next recording's telemetry. Add discardLatestPending() to the buffer and a discard-cursor-telemetry IPC handler; the renderer now calls it on the discard path. P2 – takeNextBatch() dequeued the batch before fs.writeFile, so a write failure would permanently lose the telemetry. Wrap the write in try/catch and re-insert the batch via prependBatch() on failure. Co-Authored-By: Claude Opus 4.6 (1M context) --- electron/electron-env.d.ts | 1 + electron/ipc/handlers.ts | 19 +++++++++--- electron/preload.ts | 3 ++ src/hooks/useScreenRecorder.ts | 1 + src/lib/cursorTelemetryBuffer.test.ts | 44 +++++++++++++++++++++++++++ src/lib/cursorTelemetryBuffer.ts | 10 ++++++ 6 files changed, 73 insertions(+), 5 deletions(-) diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index b2a3720..ea364a1 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -64,6 +64,7 @@ interface Window { error?: string; }>; setRecordingState: (recording: boolean) => Promise; + discardCursorTelemetry: () => Promise; getCursorTelemetry: (videoPath?: string) => Promise<{ success: boolean; samples: CursorTelemetryPoint[]; diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index 284a671..fc55006 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -281,11 +281,16 @@ async function storeRecordedSessionFiles(payload: StoreRecordedSessionInput) { const telemetryPath = `${screenVideoPath}.cursor.json`; const pendingSamples: CursorTelemetryPoint[] = cursorTelemetryBuffer.takeNextBatch(); if (pendingSamples.length > 0) { - await fs.writeFile( - telemetryPath, - JSON.stringify({ version: CURSOR_TELEMETRY_VERSION, samples: pendingSamples }, null, 2), - "utf-8", - ); + try { + await fs.writeFile( + telemetryPath, + JSON.stringify({ version: CURSOR_TELEMETRY_VERSION, samples: pendingSamples }, null, 2), + "utf-8", + ); + } catch (err) { + cursorTelemetryBuffer.prependBatch(pendingSamples); + throw err; + } } const sessionManifestPath = path.join( @@ -544,6 +549,10 @@ export function registerIpcHandlers( } }); + ipcMain.handle("discard-cursor-telemetry", () => { + cursorTelemetryBuffer.discardLatestPending(); + }); + ipcMain.handle("get-cursor-telemetry", async (_, videoPath?: string) => { const targetVideoPath = normalizeVideoSourcePath( videoPath ?? currentRecordingSession?.screenVideoPath, diff --git a/electron/preload.ts b/electron/preload.ts index eeca25c..9367122 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -53,6 +53,9 @@ contextBridge.exposeInMainWorld("electronAPI", { getCursorTelemetry: (videoPath?: string) => { return ipcRenderer.invoke("get-cursor-telemetry", videoPath); }, + discardCursorTelemetry: () => { + return ipcRenderer.invoke("discard-cursor-telemetry"); + }, onStopRecordingFromTray: (callback: () => void) => { const listener = () => callback(); ipcRenderer.on("stop-recording-from-tray", listener); diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts index 5cbc54a..fd8a307 100644 --- a/src/hooks/useScreenRecorder.ts +++ b/src/hooks/useScreenRecorder.ts @@ -225,6 +225,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn { try { const screenBlob = await activeScreenRecorder.recordedBlobPromise; if (discardRecordingId.current === activeRecordingId) { + window.electronAPI?.discardCursorTelemetry(); return; } if (screenBlob.size === 0) { diff --git a/src/lib/cursorTelemetryBuffer.test.ts b/src/lib/cursorTelemetryBuffer.test.ts index a626394..5ffbc7a 100644 --- a/src/lib/cursorTelemetryBuffer.test.ts +++ b/src/lib/cursorTelemetryBuffer.test.ts @@ -96,6 +96,50 @@ describe("createCursorTelemetryBuffer", () => { expect(batch.map((s) => s.timeMs)).toEqual([1]); }); + it("discardLatestPending() drops the most recently enqueued batch", () => { + const buf = createCursorTelemetryBuffer({ maxActiveSamples: 10 }); + + buf.startSession(); + buf.push(sample(1)); + buf.endSession(); + + buf.startSession(); + buf.push(sample(2)); + buf.endSession(); + + expect(buf.pendingCount).toBe(2); + buf.discardLatestPending(); + expect(buf.pendingCount).toBe(1); + expect(buf.takeNextBatch().map((s) => s.timeMs)).toEqual([1]); + }); + + it("discardLatestPending() is safe to call on an empty queue", () => { + const buf = createCursorTelemetryBuffer({ maxActiveSamples: 10 }); + buf.discardLatestPending(); + expect(buf.pendingCount).toBe(0); + }); + + it("prependBatch() re-inserts a batch at the front of the queue", () => { + const buf = createCursorTelemetryBuffer({ maxActiveSamples: 10 }); + + buf.startSession(); + buf.push(sample(1)); + buf.endSession(); + + const batch = buf.takeNextBatch(); + expect(buf.pendingCount).toBe(0); + + buf.prependBatch(batch); + expect(buf.pendingCount).toBe(1); + expect(buf.takeNextBatch().map((s) => s.timeMs)).toEqual([1]); + }); + + it("prependBatch() ignores empty batches", () => { + const buf = createCursorTelemetryBuffer({ maxActiveSamples: 10 }); + buf.prependBatch([]); + expect(buf.pendingCount).toBe(0); + }); + it("reset() clears both active and pending state", () => { const buf = createCursorTelemetryBuffer({ maxActiveSamples: 10 }); buf.startSession(); diff --git a/src/lib/cursorTelemetryBuffer.ts b/src/lib/cursorTelemetryBuffer.ts index 2b4ef0c..d812610 100644 --- a/src/lib/cursorTelemetryBuffer.ts +++ b/src/lib/cursorTelemetryBuffer.ts @@ -9,6 +9,8 @@ export interface CursorTelemetryBuffer { push(point: CursorTelemetryPoint): void; endSession(): void; takeNextBatch(): CursorTelemetryPoint[]; + prependBatch(batch: CursorTelemetryPoint[]): void; + discardLatestPending(): void; reset(): void; readonly activeCount: number; readonly pendingCount: number; @@ -52,6 +54,14 @@ export function createCursorTelemetryBuffer( takeNextBatch() { return pending.shift() ?? []; }, + prependBatch(batch) { + if (batch.length > 0) { + pending.unshift(batch); + } + }, + discardLatestPending() { + pending.pop(); + }, reset() { active = []; pending = [];