From 36a5c67ed67a1b62adf3f9bb37a4a8da8b348097 Mon Sep 17 00:00:00 2001 From: altair823 Date: Fri, 1 May 2026 17:08:34 +0900 Subject: [PATCH] feat(telemetry): exportTo writes events.jsonl + stats.md (#7 v0.2.3) --- src/main/services/TelemetryService.ts | 13 +++++++++- tests/unit/TelemetryService.test.ts | 35 +++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/main/services/TelemetryService.ts b/src/main/services/TelemetryService.ts index b517530..2df5b45 100644 --- a/src/main/services/TelemetryService.ts +++ b/src/main/services/TelemetryService.ts @@ -1,6 +1,7 @@ -import { mkdir, appendFile, readFile, readdir, unlink } from 'node:fs/promises'; +import { mkdir, appendFile, readFile, readdir, unlink, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { validateEvent, TelemetryEvent } from './telemetryEvents.js'; +import { aggregateStats } from './telemetryStats.js'; const KST_OFFSET_MS = 9 * 60 * 60 * 1000; @@ -105,4 +106,14 @@ export class TelemetryService { } return events; } + + async exportTo(outDir: string): Promise<{ eventCount: number }> { + const events = await this.readAllRecent(); + await mkdir(outDir, { recursive: true }); + const eventsContent = events.map((e) => JSON.stringify(e)).join('\n') + (events.length > 0 ? '\n' : ''); + await writeFile(join(outDir, 'events.jsonl'), eventsContent, 'utf8'); + const stats = aggregateStats(events, this.now()); + await writeFile(join(outDir, 'stats.md'), stats.md, 'utf8'); + return { eventCount: stats.eventCount }; + } } diff --git a/tests/unit/TelemetryService.test.ts b/tests/unit/TelemetryService.test.ts index 01a1726..8da4ea6 100644 --- a/tests/unit/TelemetryService.test.ts +++ b/tests/unit/TelemetryService.test.ts @@ -172,3 +172,38 @@ describe('TelemetryService.readAllRecent', () => { expect(await svc.readAllRecent()).toEqual([]); }); }); + +describe('TelemetryService.exportTo', () => { + let dir: string; + let outDir: string; + beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), 'inkling-telem-')); + outDir = mkdtempSync(join(tmpdir(), 'inkling-export-')); + }); + afterEach(() => { + rmSync(dir, { recursive: true, force: true }); + rmSync(outDir, { recursive: true, force: true }); + }); + + it('writes events.jsonl (concat) + stats.md to folder', async () => { + writeFileSync(join(dir, 'events-2026-05-01.jsonl'), + JSON.stringify({ ts: '2026-05-01T00:00:00.000Z', kind: 'capture', payload: { noteId: 'a', rawTextLength: 1, hasMedia: false } }) + '\n'); + const svc = new TelemetryService(dir, () => new Date('2026-05-01T12:00:00Z'), 14); + const r = await svc.exportTo(outDir); + expect(r.eventCount).toBe(1); + expect(existsSync(join(outDir, 'events.jsonl'))).toBe(true); + expect(existsSync(join(outDir, 'stats.md'))).toBe(true); + const events = readFileSync(join(outDir, 'events.jsonl'), 'utf8').trim().split('\n'); + expect(events).toHaveLength(1); + const stats = readFileSync(join(outDir, 'stats.md'), 'utf8'); + expect(stats).toContain('총 이벤트: 1'); + }); + + it('handles empty input — writes 0-event stats', async () => { + const svc = new TelemetryService(dir, () => new Date('2026-05-01T12:00:00Z'), 14); + const r = await svc.exportTo(outDir); + expect(r.eventCount).toBe(0); + expect(readFileSync(join(outDir, 'events.jsonl'), 'utf8')).toBe(''); + expect(readFileSync(join(outDir, 'stats.md'), 'utf8')).toContain('총 이벤트: 0'); + }); +});