mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-10 08:05:14 +02:00
feat: add telemetry schema sync artifact
This commit is contained in:
parent
9dcead31ed
commit
1c6c6c853f
6 changed files with 194 additions and 3 deletions
|
|
@ -11,6 +11,7 @@
|
|||
"packages/cli": {
|
||||
"entry": [
|
||||
"src/print-command-tree.ts!",
|
||||
"src/telemetry/schema-writer.ts!",
|
||||
"src/telemetry/index.ts!",
|
||||
"scripts/**/*.mjs",
|
||||
"src/**/*.test-utils.ts",
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
},
|
||||
"scripts": {
|
||||
"assets:demo": "node scripts/build-demo-assets.mjs",
|
||||
"build": "tsc -p tsconfig.json && node scripts/copy-runtime-assets.mjs && node ../../scripts/prepare-cli-bin.mjs",
|
||||
"build": "tsc -p tsconfig.json && node dist/telemetry/schema-writer.js src/telemetry/events.schema.json ../../python/ktx-daemon/src/ktx_daemon/telemetry/events.schema.json && node scripts/copy-runtime-assets.mjs && node ../../scripts/prepare-cli-bin.mjs",
|
||||
"clean": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\"",
|
||||
"docs:commands": "pnpm run build && node dist/print-command-tree.js",
|
||||
"smoke": "vitest run src/standalone-smoke.test.ts src/example-smoke.test.ts --testTimeout 30000",
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ const envelope: TelemetryCommonEnvelope = {
|
|||
};
|
||||
|
||||
describe('telemetry event schemas', () => {
|
||||
it('catalogs all Node v1 telemetry events', () => {
|
||||
it('catalogs all v1 telemetry events', () => {
|
||||
expect(telemetryEventCatalog.map((event) => event.name)).toEqual([
|
||||
'install_first_run',
|
||||
'command',
|
||||
|
|
@ -33,9 +33,50 @@ describe('telemetry event schemas', () => {
|
|||
'sql_completed',
|
||||
'wiki_query_completed',
|
||||
'mcp_request_completed',
|
||||
'daemon_started',
|
||||
'daemon_stopped',
|
||||
'sl_plan_completed',
|
||||
'sql_gen_completed',
|
||||
]);
|
||||
});
|
||||
|
||||
it('builds strict daemon telemetry events', () => {
|
||||
const daemonEnvelope = {
|
||||
...envelope,
|
||||
runtime: 'daemon-py' as const,
|
||||
nodeVersion: '3.13.0',
|
||||
};
|
||||
|
||||
expect(
|
||||
buildTelemetryEvent('sl_plan_completed', daemonEnvelope, {
|
||||
outcome: 'ok',
|
||||
stage: 'transpile',
|
||||
durationMs: 25,
|
||||
sourceCount: 2,
|
||||
joinCount: 1,
|
||||
}),
|
||||
).toMatchObject({
|
||||
name: 'sl_plan_completed',
|
||||
properties: {
|
||||
runtime: 'daemon-py',
|
||||
outcome: 'ok',
|
||||
stage: 'transpile',
|
||||
sourceCount: 2,
|
||||
joinCount: 1,
|
||||
},
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
telemetryEventSchemas.sql_gen_completed.parse({
|
||||
...daemonEnvelope,
|
||||
outcome: 'ok',
|
||||
dialect: 'postgres',
|
||||
durationMs: 4,
|
||||
sql: 'select * from private_table',
|
||||
}),
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it('builds a strict install_first_run event', () => {
|
||||
expect(buildTelemetryEvent('install_first_run', envelope, {})).toEqual({
|
||||
name: 'install_first_run',
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ const telemetryCommonEnvelopeSchema = z
|
|||
osPlatform: z.string(),
|
||||
osRelease: z.string(),
|
||||
arch: z.string(),
|
||||
runtime: z.literal('node'),
|
||||
runtime: z.enum(['node', 'daemon-py']),
|
||||
isCi: z.boolean(),
|
||||
})
|
||||
.strict();
|
||||
|
|
@ -161,6 +161,42 @@ const mcpRequestCompletedSchema = telemetryCommonEnvelopeSchema
|
|||
})
|
||||
.strict();
|
||||
|
||||
const daemonStartedSchema = telemetryCommonEnvelopeSchema
|
||||
.extend({
|
||||
daemonVersion: z.string(),
|
||||
pythonVersion: z.string(),
|
||||
runtimeVersion: z.string(),
|
||||
startupDurationMs: z.number().nonnegative(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const daemonStoppedSchema = telemetryCommonEnvelopeSchema
|
||||
.extend({
|
||||
reason: z.enum(['signal', 'request', 'crash']),
|
||||
uptimeMs: z.number().nonnegative(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const slPlanCompletedSchema = telemetryCommonEnvelopeSchema
|
||||
.extend({
|
||||
outcome: z.enum(['ok', 'error']),
|
||||
stage: z.enum(['parse', 'resolve', 'compile', 'transpile']),
|
||||
errorClass: z.string().optional(),
|
||||
durationMs: z.number().nonnegative(),
|
||||
sourceCount: z.number().int().nonnegative(),
|
||||
joinCount: z.number().int().nonnegative(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const sqlGenCompletedSchema = telemetryCommonEnvelopeSchema
|
||||
.extend({
|
||||
outcome: z.enum(['ok', 'error']),
|
||||
dialect: z.string(),
|
||||
errorClass: z.string().optional(),
|
||||
durationMs: z.number().nonnegative(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
/** @internal */
|
||||
export const telemetryEventSchemas = {
|
||||
install_first_run: installFirstRunSchema,
|
||||
|
|
@ -176,6 +212,10 @@ export const telemetryEventSchemas = {
|
|||
sql_completed: sqlCompletedSchema,
|
||||
wiki_query_completed: wikiQueryCompletedSchema,
|
||||
mcp_request_completed: mcpRequestCompletedSchema,
|
||||
daemon_started: daemonStartedSchema,
|
||||
daemon_stopped: daemonStoppedSchema,
|
||||
sl_plan_completed: slPlanCompletedSchema,
|
||||
sql_gen_completed: sqlGenCompletedSchema,
|
||||
} as const;
|
||||
|
||||
/** @internal */
|
||||
|
|
@ -288,6 +328,26 @@ export const telemetryEventCatalog = [
|
|||
description: 'Emitted for sampled MCP tool requests.',
|
||||
fields: ['toolName', 'outcome', 'durationMs', 'errorClass', 'sampleRate'],
|
||||
},
|
||||
{
|
||||
name: 'daemon_started',
|
||||
description: 'Emitted when the long-lived ktx-daemon HTTP server starts.',
|
||||
fields: ['daemonVersion', 'pythonVersion', 'runtimeVersion', 'startupDurationMs'],
|
||||
},
|
||||
{
|
||||
name: 'daemon_stopped',
|
||||
description: 'Emitted when the long-lived ktx-daemon HTTP server shuts down.',
|
||||
fields: ['reason', 'uptimeMs'],
|
||||
},
|
||||
{
|
||||
name: 'sl_plan_completed',
|
||||
description: 'Emitted after a daemon semantic-layer planning pass completes.',
|
||||
fields: ['outcome', 'stage', 'errorClass', 'durationMs', 'sourceCount', 'joinCount'],
|
||||
},
|
||||
{
|
||||
name: 'sql_gen_completed',
|
||||
description: 'Emitted after daemon SQL generation completes.',
|
||||
fields: ['outcome', 'dialect', 'errorClass', 'durationMs'],
|
||||
},
|
||||
] as const;
|
||||
|
||||
export type TelemetryEventName = keyof typeof telemetryEventSchemas;
|
||||
|
|
|
|||
26
packages/cli/src/telemetry/schema-writer.test.ts
Normal file
26
packages/cli/src/telemetry/schema-writer.test.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { buildTelemetrySchemaArtifact } from './schema-writer.js';
|
||||
|
||||
describe('telemetry schema writer', () => {
|
||||
it('exports a schema artifact with the full catalog and strict metadata', () => {
|
||||
const artifact = buildTelemetrySchemaArtifact();
|
||||
|
||||
expect(artifact.$schema).toBe('https://json-schema.org/draft/2020-12/schema');
|
||||
expect(artifact['x-ktx-common-fields']).toEqual([
|
||||
'cliVersion',
|
||||
'nodeVersion',
|
||||
'osPlatform',
|
||||
'osRelease',
|
||||
'arch',
|
||||
'runtime',
|
||||
'isCi',
|
||||
]);
|
||||
expect(artifact['x-ktx-catalog'].map((event) => event.name)).toContain('daemon_started');
|
||||
expect(artifact['x-ktx-catalog'].map((event) => event.name)).toContain('sql_gen_completed');
|
||||
expect(artifact.$defs.sql_gen_completed).toMatchObject({
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
63
packages/cli/src/telemetry/schema-writer.ts
Normal file
63
packages/cli/src/telemetry/schema-writer.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { telemetryEventCatalog, telemetryEventSchemas } from './events.js';
|
||||
|
||||
const commonFields = ['cliVersion', 'nodeVersion', 'osPlatform', 'osRelease', 'arch', 'runtime', 'isCi'] as const;
|
||||
|
||||
export interface TelemetrySchemaArtifact {
|
||||
$schema: 'https://json-schema.org/draft/2020-12/schema';
|
||||
title: 'ktx telemetry events';
|
||||
type: 'object';
|
||||
additionalProperties: false;
|
||||
'x-ktx-common-fields': string[];
|
||||
'x-ktx-catalog': Array<{ name: string; description: string; fields: readonly string[] }>;
|
||||
$defs: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function buildTelemetrySchemaArtifact(): TelemetrySchemaArtifact {
|
||||
return {
|
||||
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
||||
title: 'ktx telemetry events',
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
'x-ktx-common-fields': [...commonFields],
|
||||
'x-ktx-catalog': telemetryEventCatalog.map((event) => ({
|
||||
name: event.name,
|
||||
description: event.description,
|
||||
fields: event.fields,
|
||||
})),
|
||||
$defs: Object.fromEntries(
|
||||
Object.entries(telemetryEventSchemas).map(([name, schema]) => [
|
||||
name,
|
||||
z.toJSONSchema(schema, { target: 'draft-2020-12' }),
|
||||
]),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
async function writeTelemetrySchemaArtifact(path: string): Promise<void> {
|
||||
const target = resolve(path);
|
||||
await mkdir(dirname(target), { recursive: true });
|
||||
await writeFile(target, `${JSON.stringify(buildTelemetrySchemaArtifact(), null, 2)}\n`, 'utf-8');
|
||||
}
|
||||
|
||||
async function main(argv: string[]): Promise<void> {
|
||||
const targets = argv.slice(2);
|
||||
if (targets.length === 0) {
|
||||
throw new Error('Usage: node dist/telemetry/schema-writer.js <target> [target...]');
|
||||
}
|
||||
for (const target of targets) {
|
||||
await writeTelemetrySchemaArtifact(target);
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(fileURLToPath(import.meta.url)).href && process.argv[1]) {
|
||||
const invoked = pathToFileURL(resolve(process.argv[1])).href;
|
||||
if (import.meta.url === invoked) {
|
||||
await main(process.argv);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue