fix: handle recording discard and write-failure in cursor telemetry buffer
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) <noreply@anthropic.com>
This commit is contained in:
Vendored
+1
@@ -64,6 +64,7 @@ interface Window {
|
||||
error?: string;
|
||||
}>;
|
||||
setRecordingState: (recording: boolean) => Promise<void>;
|
||||
discardCursorTelemetry: () => Promise<void>;
|
||||
getCursorTelemetry: (videoPath?: string) => Promise<{
|
||||
success: boolean;
|
||||
samples: CursorTelemetryPoint[];
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
Reference in New Issue
Block a user