diff --git a/src/main/services/telemetryStats.ts b/src/main/services/telemetryStats.ts index 08bcadf..0fbf768 100644 --- a/src/main/services/telemetryStats.ts +++ b/src/main/services/telemetryStats.ts @@ -14,6 +14,10 @@ interface DailyRow { capture: number; ai_succeeded: number; ai_failed: number; + trash: number; + restore: number; + permanent_delete: number; + empty_trash: number; } export interface StatsResult { @@ -28,11 +32,13 @@ export function aggregateStats(events: TelemetryEvent[], generatedAt: Date): Sta let aiFailed = 0; let durationSum = 0; let durationN = 0; + let trashCount = 0; + let restoreCount = 0; for (const ev of events) { const day = kstDate(ev.ts); let row = byDay.get(day); if (!row) { - row = { date: day, capture: 0, ai_succeeded: 0, ai_failed: 0 }; + row = { date: day, capture: 0, ai_succeeded: 0, ai_failed: 0, trash: 0, restore: 0, permanent_delete: 0, empty_trash: 0 }; byDay.set(day, row); } if (ev.kind === 'capture') row.capture += 1; @@ -44,12 +50,25 @@ export function aggregateStats(events: TelemetryEvent[], generatedAt: Date): Sta } else if (ev.kind === 'ai_failed') { row.ai_failed += 1; aiFailed += 1; + } else if (ev.kind === 'trash') { + row.trash += 1; + trashCount += 1; + } else if (ev.kind === 'restore') { + row.restore += 1; + restoreCount += 1; + } else if (ev.kind === 'permanent_delete') { + row.permanent_delete += 1; + } else if (ev.kind === 'empty_trash') { + row.empty_trash += 1; } } const days = Array.from(byDay.values()).sort((a, b) => a.date.localeCompare(b.date)); const aiTotal = aiSucceeded + aiFailed; const successRate = aiTotal === 0 ? 'N/A' : `${(aiSucceeded / aiTotal * 100).toFixed(1)}% (${aiSucceeded}/${aiTotal})`; const avgDuration = durationN === 0 ? 'N/A' : `${Math.round(durationSum / durationN)}`; + const trashRecoveryRate = trashCount === 0 + ? 'N/A' + : `${(restoreCount / trashCount * 100).toFixed(1)}% (${restoreCount}/${trashCount})`; const lines: string[] = []; lines.push('# Inkling Telemetry Stats'); lines.push(''); @@ -58,16 +77,17 @@ export function aggregateStats(events: TelemetryEvent[], generatedAt: Date): Sta lines.push(''); lines.push('## 일자별 카운트'); lines.push(''); - lines.push('| 일자 | capture | ai_succeeded | ai_failed |'); - lines.push('|------|---------|--------------|-----------|'); + lines.push('| 일자 | capture | ai_succeeded | ai_failed | trash | restore | permanent_delete | empty_trash |'); + lines.push('|------|---------|--------------|-----------|-------|---------|------------------|-------------|'); for (const row of days) { - lines.push(`| ${row.date} | ${row.capture} | ${row.ai_succeeded} | ${row.ai_failed} |`); + lines.push(`| ${row.date} | ${row.capture} | ${row.ai_succeeded} | ${row.ai_failed} | ${row.trash} | ${row.restore} | ${row.permanent_delete} | ${row.empty_trash} |`); } lines.push(''); lines.push('## 핵심 ratio'); lines.push(''); lines.push(`- AI 성공률: ${successRate}`); lines.push(`- 평균 ai_succeeded durationMs: ${avgDuration}`); + lines.push(`- 휴지통 회수율: ${trashRecoveryRate}`); lines.push(''); return { md: lines.join('\n'), eventCount }; } diff --git a/tests/unit/telemetryStats.test.ts b/tests/unit/telemetryStats.test.ts index 359efa7..ab16994 100644 --- a/tests/unit/telemetryStats.test.ts +++ b/tests/unit/telemetryStats.test.ts @@ -66,3 +66,38 @@ describe('aggregateStats', () => { expect(r.md).not.toContain('| 2026-05-01 |'); }); }); + +describe('aggregateStats — trash family (v0.2.3 #4)', () => { + it('counts trash/restore/permanent_delete/empty_trash per day', () => { + const events: TelemetryEvent[] = [ + e('2026-05-01T00:00:00Z', 'trash', { noteId: 'n1' }), + e('2026-05-01T01:00:00Z', 'trash', { noteId: 'n2' }), + e('2026-05-01T02:00:00Z', 'restore', { noteId: 'n1' }), + e('2026-05-01T03:00:00Z', 'permanent_delete', { noteId: 'n3' }), + e('2026-05-01T04:00:00Z', 'empty_trash', { count: 5 }) + ]; + const r = aggregateStats(events, new Date('2026-05-08T00:00:00Z')); + expect(r.eventCount).toBe(5); + expect(r.md).toContain('| 2026-05-01 | 0 | 0 | 0 | 2 | 1 | 1 | 1 |'); + }); + + it('computes restore/trash ratio', () => { + const events: TelemetryEvent[] = [ + e('2026-05-01T00:00:00Z', 'trash', { noteId: 'a' }), + e('2026-05-01T00:00:01Z', 'trash', { noteId: 'b' }), + e('2026-05-01T00:00:02Z', 'trash', { noteId: 'c' }), + e('2026-05-01T00:00:03Z', 'trash', { noteId: 'd' }), + e('2026-05-01T00:00:04Z', 'restore', { noteId: 'a' }) + ]; + const r = aggregateStats(events, new Date('2026-05-02T00:00:00Z')); + expect(r.md).toContain('휴지통 회수율: 25.0% (1/4)'); + }); + + it('휴지통 회수율 N/A when no trash events', () => { + const events: TelemetryEvent[] = [ + e('2026-05-01T00:00:00Z', 'capture', { noteId: 'n1', rawTextLength: 1, hasMedia: false }) + ]; + const r = aggregateStats(events, new Date('2026-05-02T00:00:00Z')); + expect(r.md).toContain('휴지통 회수율: N/A'); + }); +});