ktx/packages/cli/src/telemetry/project-snapshot.ts
Andrey Avtomonov b0dd13ce7c
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

67 lines
2.2 KiB
TypeScript

import { readdir } from 'node:fs/promises';
import { join } from 'node:path';
import type { KtxProjectConfig } from '../context/project/config.js';
import { resolveProjectRuntimeRequirements } from '../runtime-requirements.js';
import { isDemoConnection } from './demo-detect.js';
async function hasFileWithExtension(dir: string, extensions: Set<string>): Promise<boolean> {
let entries;
try {
entries = await readdir(dir, { withFileTypes: true });
} catch {
return false;
}
for (const entry of entries) {
const path = join(dir, entry.name);
if (entry.isDirectory() && (await hasFileWithExtension(path, extensions))) {
return true;
}
if (entry.isFile() && extensions.has(entry.name.slice(entry.name.lastIndexOf('.')))) {
return true;
}
}
return false;
}
async function hasFileNamed(dir: string, filenames: Set<string>): Promise<boolean> {
let entries;
try {
entries = await readdir(dir, { withFileTypes: true });
} catch {
return false;
}
return entries.some((entry) => entry.isFile() && filenames.has(entry.name));
}
async function hasMcpConfig(projectDir: string): Promise<boolean> {
return (
(await hasFileWithExtension(join(projectDir, '.ktx'), new Set(['.json']))) ||
(await hasFileWithExtension(join(projectDir, '.cursor'), new Set(['.json']))) ||
(await hasFileNamed(projectDir, new Set(['.mcp.json'])))
);
}
export async function buildProjectStackSnapshotFields(input: {
projectDir: string;
config: KtxProjectConfig;
}) {
const connectors = Object.entries(input.config.connections).map(([connectionId, connection]) => ({
driver: String(connection.driver ?? 'unknown').trim().toLowerCase() || 'unknown',
isDemo: isDemoConnection(connectionId, connection),
}));
const runtimeRequirements = resolveProjectRuntimeRequirements(input.config, {
env: process.env,
});
return {
connectors,
connectionCount: connectors.length,
hasSl: await hasFileWithExtension(join(input.projectDir, 'semantic-layer'), new Set(['.yaml', '.yml'])),
hasWiki: await hasFileWithExtension(join(input.projectDir, 'wiki'), new Set(['.md', '.mdx'])),
hasMcp: await hasMcpConfig(input.projectDir),
hasManagedRuntime: runtimeRequirements.features.length > 0,
};
}