Skip to content

Commit 0a8ecda

Browse files
committed
feat(analytics): log suggestion show/no_show decisions
1 parent 915a0c8 commit 0a8ecda

5 files changed

Lines changed: 319 additions & 63 deletions

File tree

desktop/src/core/modules/ipc-handlers.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ import ASRModelManager from '../../asr/model-manager.js';
55
import LLMSuggestionService from './llm-suggestion-service.js';
66
import ReviewService from './review-service.js';
77
import MemoryService from './memory-service.js';
8+
import TelemetryService from './telemetry-service.js';
89
import { registerWindowHandlers } from './ipc-handlers/window-handlers.js';
910
import { registerDatabaseHandlers } from './ipc-handlers/database-handlers.js';
1011
import { registerLLMHandlers } from './ipc-handlers/llm-handlers.js';
1112
import { registerSuggestionHandlers } from './ipc-handlers/suggestion-handlers.js';
1213
import { registerReviewHandlers } from './ipc-handlers/review-handlers.js';
1314
import { registerMemoryHandlers } from './ipc-handlers/memory-handlers.js';
15+
import { registerTelemetryHandlers } from './ipc-handlers/telemetry-handlers.js';
1416
import { registerASRModelHandlers } from './ipc-handlers/asr-model-handlers.js';
1517
import { registerASRAudioHandlers } from './ipc-handlers/asr-audio-handlers.js';
1618
import { registerMediaHandlers } from './ipc-handlers/media-handlers.js';
@@ -28,6 +30,7 @@ export class IPCManager {
2830
this.llmSuggestionService = null;
2931
this.reviewService = null;
3032
this.memoryService = null;
33+
this.telemetryService = null;
3134
this.asrModelPreloading = false;
3235
this.asrModelPreloaded = false;
3336
this.asrServerCrashCallback = null;
@@ -82,6 +85,15 @@ export class IPCManager {
8285
this.memoryService = new MemoryService();
8386
}
8487
}
88+
89+
/**
90+
* 初始化 Telemetry Service(本地训练信号 JSONL)
91+
*/
92+
initTelemetryService() {
93+
if (!this.telemetryService) {
94+
this.telemetryService = new TelemetryService();
95+
}
96+
}
8597
/**
8698
* 初始化 Review Service
8799
*/
@@ -102,13 +114,15 @@ export class IPCManager {
102114
this.initLLMSuggestionService();
103115
this.initReviewService();
104116
this.initMemoryService();
117+
this.initTelemetryService();
105118
this.setupWindowHandlers();
106119
this.setupAppConfigHandlers();
107120
this.setupDatabaseHandlers();
108121
this.setupLLMHandlers();
109122
this.setupSuggestionHandlers();
110123
this.setupReviewHandlers();
111124
this.setupMemoryHandlers();
125+
this.setupTelemetryHandlers();
112126
this.setupASRModelHandlers();
113127
this.setupASRAudioHandlers();
114128
this.setupMediaHandlers();
@@ -186,6 +200,14 @@ export class IPCManager {
186200
registerMemoryHandlers({ memoryService: this.memoryService });
187201
}
188202

203+
/**
204+
* 设置 Telemetry 相关 IPC 处理器
205+
*/
206+
setupTelemetryHandlers() {
207+
this.initTelemetryService();
208+
registerTelemetryHandlers({ telemetryService: this.telemetryService });
209+
}
210+
189211
/**
190212
* 设置 ASR 模型管理相关 IPC 处理器
191213
*/
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { ipcMain } from 'electron';
2+
3+
/**
4+
* 注册训练信号本地埋点写入 IPC 处理器
5+
*/
6+
export function registerTelemetryHandlers({ telemetryService }) {
7+
if (!telemetryService) {
8+
console.warn('[TelemetryHandlers] telemetryService not provided, skip registration');
9+
return;
10+
}
11+
12+
ipcMain.handle('telemetry-track', async (_event, payload = {}) => {
13+
try {
14+
return await telemetryService.appendEvent(payload);
15+
} catch (error) {
16+
console.error('[TelemetryHandlers] track failed', error);
17+
throw error;
18+
}
19+
});
20+
21+
console.log('[TelemetryHandlers] Telemetry handlers registered');
22+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { app } from 'electron';
2+
import fs from 'fs/promises';
3+
import path from 'path';
4+
5+
const DEFAULT_DIRNAME = 'telemetry';
6+
const DEFAULT_FILENAME = 'training-signals.jsonl';
7+
const MAX_RECORD_BYTES = 512 * 1024;
8+
9+
export default class TelemetryService {
10+
constructor(options = {}) {
11+
const baseDir = options.baseDir || this.resolveBaseDir();
12+
this.baseDir = baseDir;
13+
this.fileName = options.fileName || DEFAULT_FILENAME;
14+
this.maxRecordBytes = Number.isFinite(options.maxRecordBytes)
15+
? options.maxRecordBytes
16+
: MAX_RECORD_BYTES;
17+
}
18+
19+
resolveBaseDir() {
20+
try {
21+
const userDataDir = app?.getPath ? app.getPath('userData') : null;
22+
if (!userDataDir) return null;
23+
return path.join(userDataDir, DEFAULT_DIRNAME);
24+
} catch (error) {
25+
console.warn('[TelemetryService] Failed to resolve userData dir:', error);
26+
return null;
27+
}
28+
}
29+
30+
get enabled() {
31+
return Boolean(this.baseDir);
32+
}
33+
34+
get filePath() {
35+
if (!this.enabled) return null;
36+
return path.join(this.baseDir, this.fileName);
37+
}
38+
39+
normalizePayload(payload) {
40+
if (!payload || typeof payload !== 'object') return null;
41+
if (Array.isArray(payload)) return { items: payload };
42+
return payload;
43+
}
44+
45+
async appendEvent(payload = {}) {
46+
if (!this.enabled) return { success: false, disabled: true };
47+
48+
const normalized = this.normalizePayload(payload);
49+
if (!normalized) return { success: false, error: 'invalid_payload' };
50+
51+
const record = {
52+
...normalized,
53+
recorded_at: Date.now()
54+
};
55+
56+
let line = '';
57+
try {
58+
line = `${JSON.stringify(record)}\n`;
59+
} catch (error) {
60+
return { success: false, error: 'stringify_failed' };
61+
}
62+
63+
if (this.maxRecordBytes && line.length > this.maxRecordBytes) {
64+
return { success: false, error: 'payload_too_large' };
65+
}
66+
67+
await fs.mkdir(this.baseDir, { recursive: true });
68+
await fs.appendFile(this.filePath, line, 'utf8');
69+
return { success: true, path: this.filePath };
70+
}
71+
}

desktop/src/preload.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
184184
memoryQueryProfiles: (payload) => ipcRenderer.invoke('memory-query-profiles', payload),
185185
memoryQueryEvents: (payload) => ipcRenderer.invoke('memory-query-events', payload),
186186
memoryUpsertEvent: (payload) => ipcRenderer.invoke('memory-upsert-event', payload),
187+
// Telemetry (本地训练信号 JSONL)
188+
telemetryTrack: (payload) => ipcRenderer.invoke('telemetry-track', payload),
187189
onSuggestionStreamStart: (callback) => {
188190
const listener = (event, data) => callback(data);
189191
ipcRenderer.on('llm-suggestion-stream-start', listener);

0 commit comments

Comments
 (0)