ktx/packages/cli/test/telemetry/scrubber.test.ts

62 lines
2.5 KiB
TypeScript
Raw Permalink Normal View History

feat(telemetry): anonymous posthog usage telemetry across node cli and python daemon (#205) * feat: add telemetry phase 1 * feat: add node telemetry event catalog * feat: add telemetry event helpers * feat: emit setup and connection telemetry * feat: emit connection and stack telemetry * feat: emit ingest and scan telemetry * feat: emit query telemetry * feat: emit sampled mcp telemetry * docs: expand telemetry event catalog * feat: add telemetry schema sync artifact * feat: pass telemetry project id to semantic daemon * feat: add daemon telemetry foundation * feat: emit semantic daemon telemetry * feat: emit daemon lifecycle telemetry * docs: document full telemetry event catalog * feat(telemetry): dim first-run notice * feat(telemetry): show first-run notice before command output * feat(telemetry): wire ktx PostHog project for live ingestion * docs(telemetry): drop posthog project name and host from storage section * docs(telemetry): trim to general overview and disclaimer * docs(agents): add short telemetry guidelines * feat(telemetry): enable posthog geoip enrichment * docs(telemetry): drop ip-geoip note from public overview * refactor(telemetry): drop no-op groupIdentify, rely on capture groups field * fix(telemetry): respect CI kill switch in python daemon identity * fix(sql): route table-count analysis to existing analyze-batch endpoint * fix(telemetry): emit install_first_run from notice path and derive flagsPresent from commander * fix(telemetry): read package info via getKtxCliPackageInfo to satisfy boundary check * fix(telemetry): make python identity env={} bypass os.environ and unset CI in tests * fix(telemetry): unset CI kill switch in cli-program-telemetry tests
2026-05-22 18:18:47 +02:00
import { describe, expect, it } from 'vitest';
import { formatErrorDetail, scrubErrorClass } from '../../src/telemetry/scrubber.js';
feat(telemetry): anonymous posthog usage telemetry across node cli and python daemon (#205) * feat: add telemetry phase 1 * feat: add node telemetry event catalog * feat: add telemetry event helpers * feat: emit setup and connection telemetry * feat: emit connection and stack telemetry * feat: emit ingest and scan telemetry * feat: emit query telemetry * feat: emit sampled mcp telemetry * docs: expand telemetry event catalog * feat: add telemetry schema sync artifact * feat: pass telemetry project id to semantic daemon * feat: add daemon telemetry foundation * feat: emit semantic daemon telemetry * feat: emit daemon lifecycle telemetry * docs: document full telemetry event catalog * feat(telemetry): dim first-run notice * feat(telemetry): show first-run notice before command output * feat(telemetry): wire ktx PostHog project for live ingestion * docs(telemetry): drop posthog project name and host from storage section * docs(telemetry): trim to general overview and disclaimer * docs(agents): add short telemetry guidelines * feat(telemetry): enable posthog geoip enrichment * docs(telemetry): drop ip-geoip note from public overview * refactor(telemetry): drop no-op groupIdentify, rely on capture groups field * fix(telemetry): respect CI kill switch in python daemon identity * fix(sql): route table-count analysis to existing analyze-batch endpoint * fix(telemetry): emit install_first_run from notice path and derive flagsPresent from commander * fix(telemetry): read package info via getKtxCliPackageInfo to satisfy boundary check * fix(telemetry): make python identity env={} bypass os.environ and unset CI in tests * fix(telemetry): unset CI kill switch in cli-program-telemetry tests
2026-05-22 18:18:47 +02:00
class KtxProjectMissingAbortError extends Error {}
describe('scrubErrorClass', () => {
it('keeps normal JavaScript class names', () => {
expect(scrubErrorClass(new KtxProjectMissingAbortError('missing'))).toBe('KtxProjectMissingAbortError');
});
it('drops path-like, URL-like, email-like, and long values', () => {
expect(scrubErrorClass({ constructor: { name: '/Users/alice/project' } })).toBeUndefined();
expect(scrubErrorClass({ constructor: { name: 'https://example.test/error' } })).toBeUndefined();
expect(scrubErrorClass({ constructor: { name: 'alice@example.test' } })).toBeUndefined();
expect(scrubErrorClass({ constructor: { name: 'A'.repeat(81) } })).toBeUndefined();
});
it('drops lowercase, spaced, and non-error-like values', () => {
expect(scrubErrorClass({ constructor: { name: 'lowercaseError' } })).toBeUndefined();
expect(scrubErrorClass({ constructor: { name: 'Bad Error' } })).toBeUndefined();
expect(scrubErrorClass('plain string')).toBeUndefined();
expect(scrubErrorClass(null)).toBeUndefined();
});
});
describe('formatErrorDetail', () => {
it('prefixes a string or numeric .code onto the message', () => {
const refused = new Error('connect failed');
(refused as { code?: unknown }).code = 'ECONNREFUSED';
expect(formatErrorDetail(refused)).toBe('ECONNREFUSED: connect failed');
const forbidden = new Error('forbidden');
(forbidden as { code?: unknown }).code = 403;
expect(formatErrorDetail(forbidden)).toBe('403: forbidden');
});
it('uses the bare message when there is no .code', () => {
expect(formatErrorDetail(new Error('password authentication failed for user "x"'))).toBe(
'password authentication failed for user "x"',
);
});
it('accepts non-Error values', () => {
expect(formatErrorDetail('boom')).toBe('boom');
});
it('collapses whitespace to a single line', () => {
expect(formatErrorDetail(new Error('line one\n line two'))).toBe('line one line two');
});
it('caps the length at 1000 characters', () => {
expect(formatErrorDetail(new Error('x'.repeat(2000)))?.length).toBe(1000);
});
it('returns undefined for empty, null, or undefined input', () => {
expect(formatErrorDetail(new Error(' '))).toBeUndefined();
expect(formatErrorDetail(null)).toBeUndefined();
expect(formatErrorDetail(undefined)).toBeUndefined();
});
});