fix(telemetry): emit install_first_run from notice path and derive flagsPresent from commander

This commit is contained in:
Andrey Avtomonov 2026-05-22 17:51:23 +02:00
parent 3414d19916
commit 2532d4db56
6 changed files with 75 additions and 79 deletions

View file

@ -1,29 +1,13 @@
import { describe, expect, it } from 'vitest';
import {
beginCommandSpan,
completeCommandSpan,
extractFlagsPresent,
resetCommandSpan,
} from './command-hook.js';
import { beginCommandSpan, completeCommandSpan, resetCommandSpan } from './command-hook.js';
describe('telemetry command hook', () => {
it('extracts only flag names, never flag values', () => {
expect(
extractFlagsPresent(['--project-dir', '/Users/alice/private', '--json', '--limit=5', '-v', 'status']),
).toEqual({
'project-dir': true,
json: true,
limit: true,
v: true,
});
});
it('builds a completed command event from a span', () => {
resetCommandSpan();
beginCommandSpan({
commandPath: ['ktx', 'status'],
argv: ['--project-dir', '/tmp/private', 'status', '--json'],
flagsPresent: { projectDir: true, json: true },
projectDir: '/tmp/private',
hasProject: true,
attachProjectGroup: true,
@ -39,10 +23,7 @@ describe('telemetry command hook', () => {
commandPath: ['ktx', 'status'],
durationMs: 25,
outcome: 'ok',
flagsPresent: {
'project-dir': true,
json: true,
},
flagsPresent: { projectDir: true, json: true },
hasProject: true,
projectDir: '/tmp/private',
projectGroupAttached: true,

View file

@ -4,7 +4,7 @@ export type CommandOutcome = 'ok' | 'error' | 'aborted';
interface CommandSpan {
commandPath: string[];
argv: string[];
flagsPresent: Record<string, boolean>;
projectDir?: string;
hasProject: boolean;
attachProjectGroup: boolean;
@ -24,29 +24,6 @@ export interface CompletedCommandSpan {
let activeCommandSpan: CommandSpan | undefined;
/** @internal */
export function extractFlagsPresent(argv: string[]): Record<string, boolean> {
const flags: Record<string, boolean> = {};
for (const arg of argv) {
if (arg.startsWith('--') && arg.length > 2) {
const [name] = arg.slice(2).split('=', 1);
if (name) {
flags[name] = true;
}
continue;
}
if (arg.startsWith('-') && arg.length > 1) {
for (const shortFlag of arg.slice(1)) {
flags[shortFlag] = true;
}
}
}
return flags;
}
export function beginCommandSpan(input: CommandSpan): void {
activeCommandSpan = input;
}
@ -69,7 +46,7 @@ export function completeCommandSpan(input: {
durationMs: Math.max(0, input.completedAt - span.startedAt),
outcome: input.outcome,
...(errorClass ? { errorClass } : {}),
flagsPresent: extractFlagsPresent(span.argv),
flagsPresent: span.flagsPresent,
hasProject: span.hasProject,
projectDir: span.projectDir,
projectGroupAttached: span.attachProjectGroup,

View file

@ -20,12 +20,30 @@ import { buildProjectStackSnapshotFields } from './project-snapshot.js';
export { beginCommandSpan, completeCommandSpan, shutdownTelemetryEmitter };
export type { CommandOutcome, CompletedCommandSpan };
export async function showTelemetryNoticeIfNeeded(io: KtxCliIo): Promise<void> {
await loadTelemetryIdentity({
export async function showTelemetryNoticeIfNeeded(io: KtxCliIo, packageInfo: KtxCliPackageInfo): Promise<void> {
const identity = await loadTelemetryIdentity({
stdoutIsTTY: io.stdout.isTTY === true,
stderr: io.stderr,
env: process.env,
});
if (!identity.enabled || !identity.createdFile || !identity.installId) {
return;
}
await trackTelemetryEvent({
event: buildTelemetryEvent(
'install_first_run',
buildCommonEnvelope({
cliVersion: packageInfo.version,
isCi: Boolean(process.env.CI),
}),
{},
),
distinctId: identity.installId,
env: process.env,
stderr: io.stderr,
});
}
type TelemetryEventFields<Name extends TelemetryEventName> = Omit<
@ -46,30 +64,6 @@ export function mcpTelemetrySampleRate(): 0.1 {
return MCP_SAMPLE_RATE;
}
async function emitInstallFirstRunIfNeeded(input: {
identity: Awaited<ReturnType<typeof loadTelemetryIdentity>>;
packageInfo: KtxCliPackageInfo;
io: KtxCliIo;
}): Promise<void> {
if (!input.identity.enabled || !input.identity.createdFile || !input.identity.installId) {
return;
}
await trackTelemetryEvent({
event: buildTelemetryEvent(
'install_first_run',
buildCommonEnvelope({
cliVersion: input.packageInfo.version,
isCi: Boolean(process.env.CI),
}),
{},
),
distinctId: input.identity.installId,
env: process.env,
stderr: input.io.stderr,
});
}
export async function emitTelemetryEvent<Name extends TelemetryEventName>(input: {
name: Name;
fields: TelemetryEventFields<Name>;
@ -91,7 +85,6 @@ export async function emitTelemetryEvent<Name extends TelemetryEventName>(input:
name: '@kaelio/ktx',
version: process.env.npm_package_version ?? '0.0.0',
};
await emitInstallFirstRunIfNeeded({ identity, packageInfo, io: input.io });
const projectId = input.projectDir ? computeTelemetryProjectId(identity.installId, input.projectDir) : undefined;
await trackTelemetryEvent({