feat(telemetry): collect PostHog $exception error reports in CLI and daemon (#262)

* feat(telemetry): add node exception reporter

* feat(telemetry): report node cli exceptions

* feat(telemetry): add daemon exception reporter

* feat(telemetry): report daemon exceptions

* docs(telemetry): document error reports

* fix(telemetry): pass redaction snapshots from node call sites

* test(telemetry): verify prepared node exception payload

* fix(telemetry): close daemon exception lifecycle gaps

* test(telemetry): verify prepared daemon exception payload

* test(telemetry): close error collection acceptance gaps

* test(telemetry): close posthog exception acceptance gaps
This commit is contained in:
Andrey Avtomonov 2026-06-05 19:36:21 +02:00 committed by GitHub
parent c3d8cedb0b
commit fb7b94b60e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 2870 additions and 140 deletions

View file

@ -7,6 +7,12 @@ import { runCommanderKtxCli } from '../src/cli-program.js';
import type { KtxCliDeps, KtxCliIo, KtxCliPackageInfo } from '../src/cli-runtime.js';
import { TELEMETRY_NOTICE } from '../src/telemetry/identity.js';
const reportExceptionMock = vi.hoisted(() => vi.fn(async () => {}));
vi.mock('../src/telemetry/exception.js', () => ({
reportException: reportExceptionMock,
}));
function makeIo(stdoutIsTTY = true): { io: KtxCliIo; stdout: () => string; stderr: () => string } {
let stdout = '';
let stderr = '';
@ -43,6 +49,7 @@ describe('runCommanderKtxCli telemetry', () => {
vi.stubEnv('CI', '');
vi.stubEnv('KTX_TELEMETRY_DISABLED', '');
vi.stubEnv('DO_NOT_TRACK', '');
reportExceptionMock.mockClear();
});
afterEach(async () => {
@ -131,4 +138,30 @@ describe('runCommanderKtxCli telemetry', () => {
await expect(runCommanderKtxCli(['unknown'], unknownIo.io, {}, info, { runInit: async () => 0 })).resolves.toBe(1);
expect(unknownIo.stderr()).not.toContain('[telemetry]');
});
it('reports genuine top-level command catches as handled exceptions', async () => {
const io = makeIo(true);
const deps: KtxCliDeps = {
doctor: async () => {
throw new Error('status failed');
},
};
await expect(
runCommanderKtxCli(
['--project-dir', tempDir, 'status', '--json'],
io.io,
deps,
info,
{ runInit: async () => 0 },
),
).resolves.toBe(1);
expect(reportExceptionMock).toHaveBeenCalledWith(
expect.objectContaining({
context: expect.objectContaining({ source: 'ktx status', handled: true, fatal: false }),
projectDir: tempDir,
}),
);
});
});