fix(telemetry): deliver stability metrics when the app config cannot be read

An unreadable app config made the identity gate throw into 'pending', so
metrics were held and eventually dropped instead of being delivered through
captureSafety's fallback identity. Treat a config read failure as 'none'.
This commit is contained in:
heggria
2026-10-22 16:16:37 +08:00
parent 4395562c98
commit 0d10e67d4f
2 changed files with 29 additions and 1 deletions
+4 -1
View File
@@ -158,7 +158,10 @@ export function registerTelemetryRoutes(app: Express, deps: RegisterTelemetryRou
const launchEventQueue = createIdentityGatedQueue<Parameters<typeof analyticsService.captureSafety>[0]>({
limit: LAUNCH_EVENT_RETENTION_LIMIT,
resolveIdentity: async () => {
const appCfg = await deps.readAppConfig(dataDir);
// An unreadable config can never yield an id, so deliver immediately
// through captureSafety's fallback identity instead of holding.
let appCfg: Awaited<ReturnType<typeof deps.readAppConfig>>;
try { appCfg = await deps.readAppConfig(dataDir); } catch { return 'none'; }
if (typeof appCfg.installationId === 'string' && appCfg.installationId) return 'ready';
if (appCfg.telemetry?.metrics === false) return 'none';
// Only a readable consent that is still on can produce an id later. An
@@ -179,3 +179,28 @@ it('does not create host fault incidents for clean exits or recovery attempts an
expect(fault).not.toHaveBeenCalled();
}, fault);
});
it('delivers stability metrics immediately when the app config cannot be read', async () => {
version.mockResolvedValue({ version: '2.0.0', channel: 'stable' });
const app = express();
const telemetry = registerTelemetryRoutes(app, {
dataDir: '/unused', readAppConfig: vi.fn(async () => { throw new Error('EACCES'); }) as never,
writeAppConfig: vi.fn() as never,
});
const server = app.listen(0, '127.0.0.1');
try {
if (!server.listening) await new Promise<void>(resolve => server.once('listening', resolve));
const url = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
for (let i = 0; i < 3; i += 1) {
const response = await fetch(`${url}/api/observability/event`, {
method: 'POST', headers: { 'content-type': 'application/json' },
body: JSON.stringify({ event: 'client_process_memory_sample', properties: { ...metric, event_id: `unreadable-${i}` } }),
});
expect(response.status).toBe(200);
}
expect(captureSafety).toHaveBeenCalledTimes(3);
} finally {
telemetry.disposeFatalHandlers();
await new Promise<void>((resolve, reject) => server.close(error => error ? reject(error) : resolve()));
}
});