diff --git a/src/main/services/telemetryStats.ts b/src/main/services/telemetryStats.ts index 15d07b0..bdbb918 100644 --- a/src/main/services/telemetryStats.ts +++ b/src/main/services/telemetryStats.ts @@ -24,6 +24,8 @@ interface DailyRow { ollama_recovered: number; ollama_recheck_manual: number; ai_retry_manual: number; + tag_vocab_hit: number; + tag_vocab_miss: number; } export interface StatsResult { @@ -47,6 +49,8 @@ export function aggregateStats(events: TelemetryEvent[], generatedAt: Date): Sta let ollamaRecheckManualCount = 0; let aiRetryManualCount = 0; let aiRetryManualFailedSum = 0; + let tagVocabHitCount = 0; + let tagVocabMissCount = 0; for (const ev of events) { const day = kstDate(ev.ts); let row = byDay.get(day); @@ -57,7 +61,8 @@ export function aggregateStats(events: TelemetryEvent[], generatedAt: Date): Sta trash: 0, restore: 0, permanent_delete: 0, empty_trash: 0, expired_banner_shown: 0, expired_batch_trash: 0, ollama_unreachable: 0, ollama_recovered: 0, ollama_recheck_manual: 0, - ai_retry_manual: 0 + ai_retry_manual: 0, + tag_vocab_hit: 0, tag_vocab_miss: 0 }; byDay.set(day, row); } @@ -99,6 +104,12 @@ export function aggregateStats(events: TelemetryEvent[], generatedAt: Date): Sta row.ai_retry_manual += 1; aiRetryManualCount += 1; aiRetryManualFailedSum += ev.payload.failedCount; + } else if (ev.kind === 'tag_vocab_hit') { + row.tag_vocab_hit += 1; + tagVocabHitCount += 1; + } else if (ev.kind === 'tag_vocab_miss') { + row.tag_vocab_miss += 1; + tagVocabMissCount += 1; } } const days = Array.from(byDay.values()).sort((a, b) => a.date.localeCompare(b.date)); @@ -115,6 +126,10 @@ export function aggregateStats(events: TelemetryEvent[], generatedAt: Date): Sta ? 'N/A' : `${Math.round(ollamaDowntimeSum / ollamaRecoveredCount)}`; const totalUnreachable = days.reduce((s, r) => s + r.ollama_unreachable, 0); + const tagVocabTotal = tagVocabHitCount + tagVocabMissCount; + const tagVocabSummary = tagVocabTotal === 0 + ? '(데이터 없음)' + : `hit/miss = ${tagVocabHitCount}/${tagVocabMissCount} (적중률 ${(tagVocabHitCount / tagVocabTotal * 100).toFixed(1)}%)`; const lines: string[] = []; lines.push('# Inkling Telemetry Stats'); lines.push(''); @@ -123,10 +138,10 @@ export function aggregateStats(events: TelemetryEvent[], generatedAt: Date): Sta lines.push(''); lines.push('## 일자별 카운트'); lines.push(''); - lines.push('| 일자 | capture | ai_succeeded | ai_failed | trash | restore | permanent_delete | empty_trash | expired_banner_shown | expired_batch_trash | ollama_unreachable | ollama_recovered | ollama_recheck_manual | ai_retry_manual |'); - lines.push('|------|---------|--------------|-----------|-------|---------|------------------|-------------|----------------------|---------------------|--------------------|------------------|----------------------|-----------------|'); + lines.push('| 일자 | capture | ai_succeeded | ai_failed | trash | restore | permanent_delete | empty_trash | expired_banner_shown | expired_batch_trash | ollama_unreachable | ollama_recovered | ollama_recheck_manual | ai_retry_manual | tag_vocab_hit | tag_vocab_miss |'); + lines.push('|------|---------|--------------|-----------|-------|---------|------------------|-------------|----------------------|---------------------|--------------------|------------------|----------------------|-----------------|---------------|----------------|'); for (const row of days) { - lines.push(`| ${row.date} | ${row.capture} | ${row.ai_succeeded} | ${row.ai_failed} | ${row.trash} | ${row.restore} | ${row.permanent_delete} | ${row.empty_trash} | ${row.expired_banner_shown} | ${row.expired_batch_trash} | ${row.ollama_unreachable} | ${row.ollama_recovered} | ${row.ollama_recheck_manual} | ${row.ai_retry_manual} |`); + lines.push(`| ${row.date} | ${row.capture} | ${row.ai_succeeded} | ${row.ai_failed} | ${row.trash} | ${row.restore} | ${row.permanent_delete} | ${row.empty_trash} | ${row.expired_banner_shown} | ${row.expired_batch_trash} | ${row.ollama_unreachable} | ${row.ollama_recovered} | ${row.ollama_recheck_manual} | ${row.ai_retry_manual} | ${row.tag_vocab_hit} | ${row.tag_vocab_miss} |`); } lines.push(''); lines.push('## 핵심 ratio'); @@ -139,6 +154,7 @@ export function aggregateStats(events: TelemetryEvent[], generatedAt: Date): Sta lines.push(`- 평균 downtimeMs (recovered): ${avgDowntime}`); lines.push(`- 수동 recheck 사용량: ${ollamaRecheckManualCount}건`); lines.push(`- AI 수동 재시도: ${aiRetryManualCount}회 / 누적 ${aiRetryManualFailedSum}건`); + lines.push(`- 태그 vocab: ${tagVocabSummary}`); lines.push(''); return { md: lines.join('\n'), eventCount }; } diff --git a/tests/unit/telemetryStats.test.ts b/tests/unit/telemetryStats.test.ts index 0631efc..4fa74b5 100644 --- a/tests/unit/telemetryStats.test.ts +++ b/tests/unit/telemetryStats.test.ts @@ -164,3 +164,27 @@ describe('aggregateStats — ai_retry_manual', () => { expect(r.md).toMatch(/AI 수동 재시도.*2회.*10건/); }); }); + +describe('aggregateStats — tag_vocab hit/miss', () => { + it('aggregates tag_vocab hit/miss with success rate', () => { + const events: TelemetryEvent[] = [ + e('2026-05-02T00:00:00Z', 'tag_vocab_hit', { tagId: 1, vocabSize: 10 }), + e('2026-05-02T00:00:01Z', 'tag_vocab_hit', { tagId: 2, vocabSize: 10 }), + e('2026-05-02T00:00:02Z', 'tag_vocab_hit', { tagId: 3, vocabSize: 10 }), + e('2026-05-02T00:00:03Z', 'tag_vocab_hit', { tagId: 4, vocabSize: 10 }), + e('2026-05-02T00:00:04Z', 'tag_vocab_hit', { tagId: 5, vocabSize: 10 }), + e('2026-05-02T00:00:05Z', 'tag_vocab_miss', { vocabSize: 10 }), + e('2026-05-02T00:00:06Z', 'tag_vocab_miss', { vocabSize: 10 }), + e('2026-05-02T00:00:07Z', 'tag_vocab_miss', { vocabSize: 10 }) + ]; + const r = aggregateStats(events, new Date('2026-05-03T00:00:00Z')); + expect(r.md).toContain('태그 vocab: hit/miss = 5/3'); + expect(r.md).toContain('적중률 62.5%'); + }); + + it('태그 vocab summary shows 데이터 없음 when no events', () => { + const r = aggregateStats([], new Date('2026-05-03T00:00:00Z')); + expect(r.md).toContain('태그 vocab'); + expect(r.md).toContain('데이터 없음'); + }); +});