Skip to content

Commit 7a5643b

Browse files
Move telemetry metadata out of settings
Store telemetry queue, log, and device metadata in telemetry.json, and migrate legacy telemetry keys out of settings.json on startup.
1 parent 5bf6c6e commit 7a5643b

1 file changed

Lines changed: 79 additions & 17 deletions

File tree

src/features/telemetry/services/telemetry.ts

Lines changed: 79 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { getVersion } from "@tauri-apps/api/app";
22
import { fetch as tauriFetch } from "@tauri-apps/plugin-http";
33
import { arch, platform } from "@tauri-apps/plugin-os";
4+
import { load, type Store } from "@tauri-apps/plugin-store";
45
import { getSettingsStore } from "@/features/settings/lib/settings-persistence";
56
import { getApiBase } from "@/utils/api-base";
67

@@ -19,6 +20,16 @@ const STORE_KEY_LOG = "telemetry_log_v1";
1920
const STORE_KEY_INSTALL_DATE = "telemetry_install_date";
2021
const STORE_KEY_LAUNCH_COUNT = "telemetry_launch_count";
2122
const STORE_KEY_LAST_APP_VERSION = "telemetry_last_app_version";
23+
const TELEMETRY_STORE_NAME = "telemetry.json";
24+
const TELEMETRY_STORE_KEYS = [
25+
STORE_KEY_DEVICE_ID,
26+
STORE_KEY_LAST_HEARTBEAT,
27+
STORE_KEY_QUEUE,
28+
STORE_KEY_LOG,
29+
STORE_KEY_INSTALL_DATE,
30+
STORE_KEY_LAUNCH_COUNT,
31+
STORE_KEY_LAST_APP_VERSION,
32+
] as const;
2233

2334
type TelemetryEventType =
2435
| "heartbeat"
@@ -67,12 +78,60 @@ let flushInFlight: Promise<boolean> | null = null;
6778
let flushTimer: ReturnType<typeof setInterval> | null = null;
6879
let heartbeatTimer: ReturnType<typeof setInterval> | null = null;
6980
let listenersRegistered = false;
81+
let telemetryStoreInstance: Store | null = null;
82+
let telemetryStoreInitPromise: Promise<Store> | null = null;
7083
const logSubscribers = new Set<TelemetryLogSubscriber>();
7184

7285
function isRecord(value: unknown): value is Record<string, unknown> {
7386
return typeof value === "object" && value !== null && !Array.isArray(value);
7487
}
7588

89+
async function migrateLegacyTelemetrySettingsStore(telemetryStore: Store) {
90+
const settingsStore = await getSettingsStore();
91+
let settingsChanged = false;
92+
let telemetryChanged = false;
93+
94+
await Promise.all(
95+
TELEMETRY_STORE_KEYS.map(async (key) => {
96+
const legacyValue = await settingsStore.get(key);
97+
if (legacyValue === null || legacyValue === undefined) return;
98+
99+
const existingValue = await telemetryStore.get(key);
100+
if (existingValue === null || existingValue === undefined) {
101+
await telemetryStore.set(key, legacyValue);
102+
telemetryChanged = true;
103+
}
104+
105+
await settingsStore.delete(key);
106+
settingsChanged = true;
107+
}),
108+
);
109+
110+
await Promise.all([
111+
settingsChanged ? settingsStore.save() : Promise.resolve(),
112+
telemetryChanged ? telemetryStore.save() : Promise.resolve(),
113+
]);
114+
}
115+
116+
async function getTelemetryStore() {
117+
if (telemetryStoreInstance) {
118+
return telemetryStoreInstance;
119+
}
120+
121+
if (!telemetryStoreInitPromise) {
122+
telemetryStoreInitPromise = (async () => {
123+
const store = await load(TELEMETRY_STORE_NAME, {
124+
autoSave: true,
125+
} as Parameters<typeof load>[1]);
126+
await migrateLegacyTelemetrySettingsStore(store);
127+
telemetryStoreInstance = store;
128+
return store;
129+
})();
130+
}
131+
132+
return telemetryStoreInitPromise;
133+
}
134+
76135
function serializeError(error: unknown): { message: string; stack?: string } {
77136
if (error instanceof Error) {
78137
return {
@@ -137,7 +196,7 @@ function sanitizePayload(value: unknown): unknown {
137196
}
138197

139198
async function getOrCreateDeviceId() {
140-
const store = await getSettingsStore();
199+
const store = await getTelemetryStore();
141200
const existing = await store.get<string>(STORE_KEY_DEVICE_ID);
142201
if (existing) return existing;
143202

@@ -148,9 +207,9 @@ async function getOrCreateDeviceId() {
148207
}
149208

150209
async function loadQueue(
151-
storeArg?: Awaited<ReturnType<typeof getSettingsStore>>,
210+
storeArg?: Awaited<ReturnType<typeof getTelemetryStore>>,
152211
): Promise<QueuedTelemetryEvent[]> {
153-
const store = storeArg ?? (await getSettingsStore());
212+
const store = storeArg ?? (await getTelemetryStore());
154213
const queue = await store.get<unknown>(STORE_KEY_QUEUE);
155214
if (!Array.isArray(queue)) return [];
156215

@@ -167,17 +226,17 @@ async function loadQueue(
167226

168227
async function saveQueue(
169228
queue: QueuedTelemetryEvent[],
170-
storeArg?: Awaited<ReturnType<typeof getSettingsStore>>,
229+
storeArg?: Awaited<ReturnType<typeof getTelemetryStore>>,
171230
) {
172-
const store = storeArg ?? (await getSettingsStore());
231+
const store = storeArg ?? (await getTelemetryStore());
173232
await store.set(STORE_KEY_QUEUE, queue);
174233
await store.save();
175234
}
176235

177236
async function loadLogEntries(
178-
storeArg?: Awaited<ReturnType<typeof getSettingsStore>>,
237+
storeArg?: Awaited<ReturnType<typeof getTelemetryStore>>,
179238
): Promise<TelemetryLogEntry[]> {
180-
const store = storeArg ?? (await getSettingsStore());
239+
const store = storeArg ?? (await getTelemetryStore());
181240
const log = await store.get<unknown>(STORE_KEY_LOG);
182241
if (!Array.isArray(log)) return [];
183242

@@ -195,9 +254,9 @@ async function loadLogEntries(
195254

196255
async function persistLogEntries(
197256
entries: TelemetryLogEntry[],
198-
storeArg?: Awaited<ReturnType<typeof getSettingsStore>>,
257+
storeArg?: Awaited<ReturnType<typeof getTelemetryStore>>,
199258
) {
200-
const store = storeArg ?? (await getSettingsStore());
259+
const store = storeArg ?? (await getTelemetryStore());
201260
const limitedEntries = entries.slice(-MAX_LOG_ENTRIES);
202261
await store.set(STORE_KEY_LOG, limitedEntries);
203262
await store.save();
@@ -212,9 +271,9 @@ function notifyLogSubscribers(entries: TelemetryLogEntry[]) {
212271

213272
async function appendLogEntry(
214273
entry: Omit<TelemetryLogEntry, "id" | "timestamp">,
215-
storeArg?: Awaited<ReturnType<typeof getSettingsStore>>,
274+
storeArg?: Awaited<ReturnType<typeof getTelemetryStore>>,
216275
) {
217-
const store = storeArg ?? (await getSettingsStore());
276+
const store = storeArg ?? (await getTelemetryStore());
218277
const entries = await loadLogEntries(store);
219278
entries.push({
220279
id: crypto.randomUUID(),
@@ -228,7 +287,7 @@ async function ensureClientContext(): Promise<TelemetryClientContext> {
228287
if (clientContextPromise) return clientContextPromise;
229288

230289
clientContextPromise = (async () => {
231-
const store = await getSettingsStore();
290+
const store = await getTelemetryStore();
232291
const [deviceId, appVersion] = await Promise.all([getOrCreateDeviceId(), getVersion()]);
233292

234293
const nowIso = new Date().toISOString();
@@ -267,14 +326,14 @@ async function isUsageTelemetryEnabled(): Promise<boolean> {
267326
}
268327

269328
async function shouldQueueHeartbeat(): Promise<boolean> {
270-
const store = await getSettingsStore();
329+
const store = await getTelemetryStore();
271330
const lastHeartbeat = await store.get<number>(STORE_KEY_LAST_HEARTBEAT);
272331
if (!lastHeartbeat) return true;
273332
return Date.now() - lastHeartbeat >= HEARTBEAT_INTERVAL_MS;
274333
}
275334

276335
async function markHeartbeatQueued() {
277-
const store = await getSettingsStore();
336+
const store = await getTelemetryStore();
278337
await store.set(STORE_KEY_LAST_HEARTBEAT, Date.now());
279338
await store.save();
280339
}
@@ -296,7 +355,7 @@ async function enqueueTelemetryEvent(
296355
const mode = options?.mode ?? "optional";
297356
if (mode === "optional" && !(await isUsageTelemetryEnabled())) return false;
298357

299-
const store = await getSettingsStore();
358+
const store = await getTelemetryStore();
300359
const context = await ensureClientContext();
301360
const queue = await loadQueue(store);
302361

@@ -358,7 +417,7 @@ export async function flushTelemetryQueue(): Promise<boolean> {
358417
}
359418

360419
flushInFlight = (async () => {
361-
const store = await getSettingsStore();
420+
const store = await getTelemetryStore();
362421
const queue = await loadQueue(store);
363422
if (queue.length === 0) return true;
364423

@@ -548,7 +607,7 @@ export async function getTelemetryLogEntries(): Promise<TelemetryLogEntry[]> {
548607
}
549608

550609
export async function clearTelemetryLogEntries() {
551-
const store = await getSettingsStore();
610+
const store = await getTelemetryStore();
552611
await persistLogEntries([], store);
553612
await appendLogEntry(
554613
{
@@ -573,6 +632,9 @@ export function initializeTelemetry(): Promise<void> {
573632
if (initializationPromise) return initializationPromise;
574633

575634
registerCrashListeners();
635+
void getTelemetryStore().catch((error) => {
636+
console.error("Telemetry store initialization failed:", error);
637+
});
576638
setTimeout(() => {
577639
void ensureClientContext()
578640
.then(() => queueHeartbeat())

0 commit comments

Comments
 (0)