feat(trash): telemetryStats 4 new counters + 휴지통 회수율 ratio (#4 v0.2.3)
This commit is contained in:
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user