feat(telemetry): telemetryStats.aggregateStats (#7 v0.2.3)

This commit is contained in:
altair823
2026-05-01 17:03:31 +09:00
parent 729a3f9c47
commit 9a066ed807
2 changed files with 130 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
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;
}
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;
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 };
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;
}
}
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 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 |');
lines.push('|------|---------|--------------|-----------|');
for (const row of days) {
lines.push(`| ${row.date} | ${row.capture} | ${row.ai_succeeded} | ${row.ai_failed} |`);
}
lines.push('');
lines.push('## 핵심 ratio');
lines.push('');
lines.push(`- AI 성공률: ${successRate}`);
lines.push(`- 평균 ai_succeeded durationMs: ${avgDuration}`);
lines.push('');
return { md: lines.join('\n'), eventCount };
}

View File

@@ -0,0 +1,57 @@
import { describe, it, expect } from 'vitest';
import { aggregateStats } from '@main/services/telemetryStats.js';
import type { TelemetryEvent } from '@main/services/telemetryEvents.js';
const e = (ts: string, kind: TelemetryEvent['kind'], payload: TelemetryEvent['payload']): TelemetryEvent =>
({ ts, kind, payload } as TelemetryEvent);
describe('aggregateStats', () => {
it('produces empty stats for empty input', () => {
const r = aggregateStats([], new Date('2026-05-08T00:00:00Z'));
expect(r.eventCount).toBe(0);
expect(r.md).toContain('총 이벤트: 0');
});
it('counts events per KST day per kind', () => {
const events: TelemetryEvent[] = [
e('2026-05-01T12:00:00Z', 'capture', { noteId: 'n1', rawTextLength: 5, hasMedia: false }),
e('2026-05-01T12:01:00Z', 'capture', { noteId: 'n2', rawTextLength: 3, hasMedia: true }),
e('2026-05-01T12:02:00Z', 'ai_succeeded', { noteId: 'n1', durationMs: 1000, attempts: 0 }),
e('2026-05-02T00:00:00Z', 'ai_failed', { noteId: 'n2', reason: 'unreachable', attempts: 3 })
];
const r = aggregateStats(events, new Date('2026-05-08T00:00:00Z'));
expect(r.eventCount).toBe(4);
expect(r.md).toContain('| 2026-05-01 | 2 | 1 | 0 |');
expect(r.md).toContain('| 2026-05-02 | 0 | 0 | 1 |');
});
it('computes AI 성공률', () => {
const events: TelemetryEvent[] = [
e('2026-05-01T00:00:00Z', 'ai_succeeded', { noteId: 'n1', durationMs: 1, attempts: 0 }),
e('2026-05-01T00:00:01Z', 'ai_succeeded', { noteId: 'n2', durationMs: 1, attempts: 0 }),
e('2026-05-01T00:00:02Z', 'ai_succeeded', { noteId: 'n3', durationMs: 1, attempts: 0 }),
e('2026-05-01T00:00:03Z', 'ai_failed', { noteId: 'n4', reason: 'other', attempts: 1 })
];
const r = aggregateStats(events, new Date('2026-05-02T00:00:00Z'));
expect(r.md).toContain('AI 성공률: 75.0%');
expect(r.md).toContain('3/4');
});
it('AI 성공률 N/A when no AI 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('AI 성공률: N/A');
});
it('computes 평균 ai_succeeded durationMs', () => {
const events: TelemetryEvent[] = [
e('2026-05-01T00:00:00Z', 'ai_succeeded', { noteId: 'n1', durationMs: 1000, attempts: 0 }),
e('2026-05-01T00:00:01Z', 'ai_succeeded', { noteId: 'n2', durationMs: 2000, attempts: 0 }),
e('2026-05-01T00:00:02Z', 'ai_succeeded', { noteId: 'n3', durationMs: 3000, attempts: 0 })
];
const r = aggregateStats(events, new Date('2026-05-02T00:00:00Z'));
expect(r.md).toContain('평균 ai_succeeded durationMs: 2000');
});
});