- DailyRow +4 cols (recall_shown/opened/dismissed/snoozed)
- accumulators + 4 branches + recallAgeDaysSum
- table 컬럼 +4
- summary lines: "- 회상 추천: shown N / opened O / dismissed D / snoozed S (열림율 X%)"
"- 회상 평균 ageDays: avg"
- TelemetryService.EmitInput union 15 → 19
- 단위 +2 cases
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
192 lines
8.2 KiB
TypeScript
192 lines
8.2 KiB
TypeScript
import type { TelemetryEvent } from './telemetryEvents.js';
|
|
|
|
const KST_OFFSET_MS = 9 * 60 * 60 * 1000;
|
|
|
|
function kstDate(ts: string): string {
|
|
const d = new Date(ts);
|
|
const k = new Date(d.getTime() + KST_OFFSET_MS);
|
|
return new Date(Date.UTC(k.getUTCFullYear(), k.getUTCMonth(), k.getUTCDate()))
|
|
.toISOString().slice(0, 10);
|
|
}
|
|
|
|
interface DailyRow {
|
|
date: string;
|
|
capture: number;
|
|
ai_succeeded: number;
|
|
ai_failed: number;
|
|
trash: number;
|
|
restore: number;
|
|
permanent_delete: number;
|
|
empty_trash: number;
|
|
expired_banner_shown: number;
|
|
expired_batch_trash: number;
|
|
ollama_unreachable: number;
|
|
ollama_recovered: number;
|
|
ollama_recheck_manual: number;
|
|
ai_retry_manual: number;
|
|
tag_vocab_hit: number;
|
|
tag_vocab_miss: number;
|
|
recall_shown: number;
|
|
recall_opened: number;
|
|
recall_dismissed: number;
|
|
recall_snoozed: number;
|
|
}
|
|
|
|
export interface StatsResult {
|
|
md: string;
|
|
eventCount: number;
|
|
}
|
|
|
|
export function aggregateStats(events: TelemetryEvent[], generatedAt: Date): StatsResult {
|
|
const eventCount = events.length;
|
|
const byDay = new Map<string, DailyRow>();
|
|
let aiSucceeded = 0;
|
|
let aiFailed = 0;
|
|
let durationSum = 0;
|
|
let durationN = 0;
|
|
let trashCount = 0;
|
|
let restoreCount = 0;
|
|
let expiredBannerShownCandidatesSum = 0;
|
|
let expiredBatchTrashCountSum = 0;
|
|
let ollamaDowntimeSum = 0;
|
|
let ollamaRecoveredCount = 0;
|
|
let ollamaRecheckManualCount = 0;
|
|
let aiRetryManualCount = 0;
|
|
let aiRetryManualFailedSum = 0;
|
|
let tagVocabHitCount = 0;
|
|
let tagVocabMissCount = 0;
|
|
let recallShownCount = 0;
|
|
let recallOpenedCount = 0;
|
|
let recallDismissedCount = 0;
|
|
let recallSnoozedCount = 0;
|
|
let recallAgeDaysSum = 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,
|
|
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,
|
|
tag_vocab_hit: 0, tag_vocab_miss: 0,
|
|
recall_shown: 0, recall_opened: 0, recall_dismissed: 0, recall_snoozed: 0
|
|
};
|
|
byDay.set(day, row);
|
|
}
|
|
if (ev.kind === 'capture') row.capture += 1;
|
|
else if (ev.kind === 'ai_succeeded') {
|
|
row.ai_succeeded += 1;
|
|
aiSucceeded += 1;
|
|
durationSum += ev.payload.durationMs;
|
|
durationN += 1;
|
|
} 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;
|
|
} else if (ev.kind === 'expired_banner_shown') {
|
|
row.expired_banner_shown += 1;
|
|
expiredBannerShownCandidatesSum += ev.payload.candidateCount;
|
|
} else if (ev.kind === 'expired_batch_trash') {
|
|
row.expired_batch_trash += 1;
|
|
expiredBatchTrashCountSum += ev.payload.count;
|
|
} else if (ev.kind === 'ollama_unreachable') {
|
|
row.ollama_unreachable += 1;
|
|
} else if (ev.kind === 'ollama_recovered') {
|
|
row.ollama_recovered += 1;
|
|
ollamaDowntimeSum += ev.payload.downtimeMs;
|
|
ollamaRecoveredCount += 1;
|
|
} else if (ev.kind === 'ollama_recheck_manual') {
|
|
row.ollama_recheck_manual += 1;
|
|
ollamaRecheckManualCount += 1;
|
|
} else if (ev.kind === 'ai_retry_manual') {
|
|
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;
|
|
} else if (ev.kind === 'recall_shown') {
|
|
row.recall_shown += 1;
|
|
recallShownCount += 1;
|
|
recallAgeDaysSum += ev.payload.ageDays;
|
|
} else if (ev.kind === 'recall_opened') {
|
|
row.recall_opened += 1;
|
|
recallOpenedCount += 1;
|
|
} else if (ev.kind === 'recall_dismissed') {
|
|
row.recall_dismissed += 1;
|
|
recallDismissedCount += 1;
|
|
} else if (ev.kind === 'recall_snoozed') {
|
|
row.recall_snoozed += 1;
|
|
recallSnoozedCount += 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 expiredTrashRatio = expiredBannerShownCandidatesSum === 0
|
|
? 'N/A'
|
|
: `${(expiredBatchTrashCountSum / expiredBannerShownCandidatesSum * 100).toFixed(1)}% (${expiredBatchTrashCountSum}/${expiredBannerShownCandidatesSum})`;
|
|
const avgDowntime = ollamaRecoveredCount === 0
|
|
? '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 recallSummary = recallShownCount === 0
|
|
? '(데이터 없음)'
|
|
: `shown ${recallShownCount} / opened ${recallOpenedCount} / dismissed ${recallDismissedCount} / snoozed ${recallSnoozedCount} (열림율 ${(recallOpenedCount / recallShownCount * 100).toFixed(1)}%)`;
|
|
const recallAvgAge = recallShownCount === 0
|
|
? '(데이터 없음)'
|
|
: `${Math.round(recallAgeDaysSum / recallShownCount)}`;
|
|
const lines: string[] = [];
|
|
lines.push('# Inkling Telemetry Stats');
|
|
lines.push('');
|
|
lines.push(`생성: ${generatedAt.toISOString()}`);
|
|
lines.push(`총 이벤트: ${eventCount}`);
|
|
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 | tag_vocab_hit | tag_vocab_miss | recall_shown | recall_opened | recall_dismissed | recall_snoozed |');
|
|
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} | ${row.tag_vocab_hit} | ${row.tag_vocab_miss} | ${row.recall_shown} | ${row.recall_opened} | ${row.recall_dismissed} | ${row.recall_snoozed} |`);
|
|
}
|
|
lines.push('');
|
|
lines.push('## 핵심 ratio');
|
|
lines.push('');
|
|
lines.push(`- AI 성공률: ${successRate}`);
|
|
lines.push(`- 평균 ai_succeeded durationMs: ${avgDuration}`);
|
|
lines.push(`- 휴지통 회수율: ${trashRecoveryRate}`);
|
|
lines.push(`- 만료 trash ratio: ${expiredTrashRatio}`);
|
|
lines.push(`- Ollama unreachable 빈도: ${totalUnreachable}건`);
|
|
lines.push(`- 평균 downtimeMs (recovered): ${avgDowntime}`);
|
|
lines.push(`- 수동 recheck 사용량: ${ollamaRecheckManualCount}건`);
|
|
lines.push(`- AI 수동 재시도: ${aiRetryManualCount}회 / 누적 ${aiRetryManualFailedSum}건`);
|
|
lines.push(`- 태그 vocab: ${tagVocabSummary}`);
|
|
lines.push(`- 회상 추천: ${recallSummary}`);
|
|
lines.push(`- 회상 평균 ageDays: ${recallAvgAge}`);
|
|
lines.push('');
|
|
return { md: lines.join('\n'), eventCount };
|
|
}
|