mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-07 07:55:13 +02:00
* 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
155 lines
4.7 KiB
TypeScript
155 lines
4.7 KiB
TypeScript
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { createGlobalExceptionReporter, type KtxCliIo } from '../../src/cli-runtime.js';
|
|
import { beginCommandSpan, emitAbortedCommandAndShutdown, emitTelemetryEvent } from '../../src/telemetry/index.js';
|
|
import { resetCommandSpan } from '../../src/telemetry/command-hook.js';
|
|
|
|
function makeIo(): { io: KtxCliIo; stderr: () => string } {
|
|
let stderr = '';
|
|
return {
|
|
io: {
|
|
stdout: {
|
|
isTTY: true,
|
|
write: () => {},
|
|
},
|
|
stderr: {
|
|
write: (chunk) => {
|
|
stderr += chunk;
|
|
},
|
|
},
|
|
},
|
|
stderr: () => stderr,
|
|
};
|
|
}
|
|
|
|
describe('emitTelemetryEvent', () => {
|
|
let homeDir: string;
|
|
|
|
beforeEach(async () => {
|
|
homeDir = await mkdtemp(join(tmpdir(), 'ktx-telemetry-index-'));
|
|
vi.stubEnv('HOME', homeDir);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
vi.unstubAllEnvs();
|
|
await rm(homeDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('prints debug telemetry when live telemetry is disabled without creating an identity file', async () => {
|
|
vi.stubEnv('KTX_TELEMETRY_DEBUG', '1');
|
|
vi.stubEnv('KTX_TELEMETRY_DISABLED', '1');
|
|
vi.stubEnv('DO_NOT_TRACK', '1');
|
|
const testIo = makeIo();
|
|
const projectDir = join(homeDir, 'private-project');
|
|
|
|
await emitTelemetryEvent({
|
|
name: 'connection_added',
|
|
projectDir,
|
|
io: testIo.io,
|
|
packageInfo: { name: '@kaelio/ktx', version: '0.0.0-test' },
|
|
fields: {
|
|
driver: 'sqlite',
|
|
isDemoConnection: false,
|
|
},
|
|
});
|
|
|
|
expect(testIo.stderr()).toContain('[telemetry]');
|
|
expect(testIo.stderr()).toContain('"event":"connection_added"');
|
|
expect(testIo.stderr()).not.toContain(projectDir);
|
|
await expect(readFile(join(homeDir, '.ktx', 'telemetry.json'), 'utf-8')).rejects.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('emitAbortedCommandAndShutdown', () => {
|
|
let homeDir: string;
|
|
|
|
beforeEach(async () => {
|
|
homeDir = await mkdtemp(join(tmpdir(), 'ktx-telemetry-abort-'));
|
|
vi.stubEnv('HOME', homeDir);
|
|
vi.stubEnv('KTX_TELEMETRY_DEBUG', '1');
|
|
vi.stubEnv('CI', '');
|
|
vi.stubEnv('KTX_TELEMETRY_DISABLED', '');
|
|
vi.stubEnv('DO_NOT_TRACK', '');
|
|
resetCommandSpan();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
resetCommandSpan();
|
|
vi.unstubAllEnvs();
|
|
await rm(homeDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('flushes the active command span as aborted (the signal path)', async () => {
|
|
const testIo = makeIo();
|
|
beginCommandSpan({
|
|
commandPath: ['ktx', 'ingest'],
|
|
flagsPresent: {},
|
|
hasProject: true,
|
|
attachProjectGroup: false,
|
|
startedAt: performance.now(),
|
|
});
|
|
|
|
await emitAbortedCommandAndShutdown({
|
|
packageInfo: { name: '@kaelio/ktx', version: '0.0.0-test' },
|
|
io: testIo.io,
|
|
});
|
|
|
|
expect(testIo.stderr()).toContain('"event":"command"');
|
|
expect(testIo.stderr()).toContain('"outcome":"aborted"');
|
|
expect(testIo.stderr()).toContain('"commandPath":["ktx","ingest"]');
|
|
});
|
|
|
|
it('is idempotent: a second call (or no active span) emits nothing', async () => {
|
|
const testIo = makeIo();
|
|
beginCommandSpan({
|
|
commandPath: ['ktx', 'ingest'],
|
|
flagsPresent: {},
|
|
hasProject: true,
|
|
attachProjectGroup: false,
|
|
startedAt: performance.now(),
|
|
});
|
|
const pkg = { name: '@kaelio/ktx', version: '0.0.0-test' };
|
|
|
|
await emitAbortedCommandAndShutdown({ packageInfo: pkg, io: testIo.io });
|
|
const secondIo = makeIo();
|
|
await emitAbortedCommandAndShutdown({ packageInfo: pkg, io: secondIo.io });
|
|
|
|
expect(secondIo.stderr()).not.toContain('"event":"command"');
|
|
});
|
|
});
|
|
|
|
describe('global exception reporting contract', () => {
|
|
let homeDir: string;
|
|
|
|
beforeEach(async () => {
|
|
homeDir = await mkdtemp(join(tmpdir(), 'ktx-telemetry-global-exception-'));
|
|
vi.stubEnv('HOME', homeDir);
|
|
vi.stubEnv('KTX_TELEMETRY_DEBUG', '1');
|
|
vi.stubEnv('KTX_TELEMETRY_DISABLED', '1');
|
|
vi.stubEnv('DO_NOT_TRACK', '');
|
|
vi.stubEnv('CI', '');
|
|
});
|
|
|
|
afterEach(async () => {
|
|
vi.unstubAllEnvs();
|
|
await rm(homeDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('reports uncaughtException through the fatal debug payload', async () => {
|
|
const testIo = makeIo();
|
|
const report = createGlobalExceptionReporter(testIo.io, {
|
|
name: '@kaelio/ktx',
|
|
version: '0.0.0-test',
|
|
});
|
|
|
|
await report('uncaughtException', new Error('global boom'));
|
|
|
|
expect(testIo.stderr()).toContain('[telemetry-exception]');
|
|
expect(testIo.stderr()).toContain('"source":"uncaughtException"');
|
|
expect(testIo.stderr()).toContain('"handled":false');
|
|
expect(testIo.stderr()).toContain('"fatal":true');
|
|
});
|
|
});
|