feat: report MCP client telemetry (#242)

This commit is contained in:
Andrey Avtomonov 2026-05-30 18:00:25 +02:00 committed by GitHub
parent 25f639fba2
commit 2e5f7f25aa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 216 additions and 29 deletions

View file

@ -128,7 +128,9 @@ describe('telemetry privacy snapshot', () => {
outcome: 'error',
errorClass: 'KtxProjectMissingAbortError',
durationMs: 12,
sampleRate: 0.1,
sampleRate: 1,
mcpClientName: 'Claude Desktop',
mcpClientVersion: '0.7.1',
}),
];

View file

@ -146,6 +146,75 @@ describe('telemetry identity', () => {
});
});
it('enables a consented identity without a TTY (MCP servers run headless)', async () => {
await mkdir(join(homeDir, '.ktx'), { recursive: true });
await writeFile(
join(homeDir, '.ktx', 'telemetry.json'),
JSON.stringify(
{
installId: '00000000-0000-4000-8000-000000000000',
enabled: true,
noticeShownAt: '2026-05-22T14:33:02.000Z',
noticeShownVersion: 1,
createdAt: '2026-05-22T14:33:02.000Z',
},
null,
2,
) + '\n',
'utf-8',
);
const testIo = makeIo(false);
await expect(
loadTelemetryIdentity({
homeDir,
env,
stdoutIsTTY: false,
stderr: testIo.io.stderr,
now: () => new Date('2026-05-22T15:00:00.000Z'),
}),
).resolves.toMatchObject({
installId: '00000000-0000-4000-8000-000000000000',
enabled: true,
createdFile: false,
noticeShown: false,
});
// The one-time notice belongs to interactive surfaces only; a headless load
// must never write it (the MCP stdio protocol shares the process streams).
expect(testIo.stderr()).toBe('');
});
it('keeps opt-outs suppressing a consented identity without a TTY', async () => {
await mkdir(join(homeDir, '.ktx'), { recursive: true });
await writeFile(
join(homeDir, '.ktx', 'telemetry.json'),
JSON.stringify(
{
installId: '00000000-0000-4000-8000-000000000000',
enabled: true,
noticeShownAt: '2026-05-22T14:33:02.000Z',
noticeShownVersion: 1,
createdAt: '2026-05-22T14:33:02.000Z',
},
null,
2,
) + '\n',
'utf-8',
);
for (const optOut of [{ KTX_TELEMETRY_DISABLED: '1' }, { DO_NOT_TRACK: '1' }, { CI: '1' }]) {
await expect(
loadTelemetryIdentity({
homeDir,
env: optOut,
stdoutIsTTY: false,
stderr: makeIo(false).io.stderr,
now: () => new Date('2026-05-22T15:00:00.000Z'),
}),
).resolves.toMatchObject({ enabled: false });
}
});
it('recreates a corrupted file instead of surfacing an error to users', async () => {
await mkdir(join(homeDir, '.ktx'), { recursive: true });
await writeFile(join(homeDir, '.ktx', 'telemetry.json'), '{bad json', 'utf-8');