feat(telemetry): collect PostHog $exception error reports in CLI and daemon (#262)

* feat(telemetry): add node exception reporter

* feat(telemetry): report node cli exceptions

* feat(telemetry): add daemon exception reporter

* feat(telemetry): report daemon exceptions

* docs(telemetry): document error reports

* fix(telemetry): pass redaction snapshots from node call sites

* test(telemetry): verify prepared node exception payload

* fix(telemetry): close daemon exception lifecycle gaps

* test(telemetry): verify prepared daemon exception payload

* test(telemetry): close error collection acceptance gaps

* test(telemetry): close posthog exception acceptance gaps
This commit is contained in:
Andrey Avtomonov 2026-06-05 19:36:21 +02:00 committed by GitHub
parent c3d8cedb0b
commit fb7b94b60e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 2870 additions and 140 deletions

View file

@ -3,7 +3,13 @@ import type { ToolAnnotations } from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod';
import type { KtxCliIo } from '../../cli-runtime.js';
import type { MemoryAgentInput } from '../../context/memory/types.js';
import { emitTelemetryEvent, mcpTelemetrySampleRate, shouldEmitMcpTelemetry } from '../../telemetry/index.js';
import {
emitTelemetryEvent,
mcpTelemetrySampleRate,
reportException,
shouldEmitMcpTelemetry,
} from '../../telemetry/index.js';
import { collectTelemetryRedactionSecrets } from '../../telemetry/redaction-secrets.js';
import { scrubErrorClass } from '../../telemetry/scrubber.js';
import type {
KtxMcpClientInfo,
@ -518,11 +524,26 @@ function registerParsedTool<TSchema extends z.ZodType>(
},
schema: TSchema,
handler: (input: z.infer<TSchema>, context?: KtxMcpToolHandlerContext) => Promise<KtxMcpToolResult>,
telemetry?: { projectDir?: string; io?: KtxCliIo },
): void {
server.registerTool(name, config, async (input, context) => {
try {
return await handler(schema.parse(input), context);
} catch (error) {
if (telemetry?.io) {
await reportException({
error,
context: { source: `mcp:${name}`, handled: true, fatal: false },
projectDir: telemetry.projectDir,
io: telemetry.io,
redactionSecrets: await collectTelemetryRedactionSecrets({
projectDir: telemetry.projectDir,
includeLlm: true,
includeEmbeddings: true,
env: process.env,
}),
});
}
return jsonErrorToolResult(formatToolError(error));
}
});
@ -571,6 +592,20 @@ function instrumentMcpServer(
}
return result;
} catch (error) {
if (telemetry.io) {
await reportException({
error,
context: { source: `mcp:${name}`, handled: true, fatal: false },
projectDir: telemetry.projectDir,
io: telemetry.io,
redactionSecrets: await collectTelemetryRedactionSecrets({
projectDir: telemetry.projectDir,
includeLlm: true,
includeEmbeddings: true,
env: process.env,
}),
});
}
if (telemetry.io && telemetry.projectDir && shouldEmitMcpTelemetry()) {
const errorClass = scrubErrorClass(error);
await emitTelemetryEvent({
@ -596,6 +631,7 @@ function instrumentMcpServer(
export function registerKtxContextTools(deps: RegisterKtxContextToolsDeps): void {
const { ports, userContext } = deps;
const toolTelemetry = { projectDir: deps.projectDir, io: deps.io };
const server = instrumentMcpServer(deps.server, {
projectDir: deps.projectDir,
io: deps.io,
@ -616,6 +652,7 @@ export function registerKtxContextTools(deps: RegisterKtxContextToolsDeps): void
},
connectionListSchema,
async () => jsonToolResult({ connections: await connections.list() }),
toolTelemetry,
);
}
@ -640,6 +677,7 @@ export function registerKtxContextTools(deps: RegisterKtxContextToolsDeps): void
limit: input.limit,
}),
),
toolTelemetry,
);
registerParsedTool(
@ -657,6 +695,7 @@ export function registerKtxContextTools(deps: RegisterKtxContextToolsDeps): void
const page = await knowledge.read({ userId: userContext.userId, key: input.key });
return page ? jsonToolResult(page) : jsonErrorToolResult(`Wiki page "${input.key}" was not found.`);
},
toolTelemetry,
);
}
@ -679,6 +718,7 @@ export function registerKtxContextTools(deps: RegisterKtxContextToolsDeps): void
? jsonToolResult(source)
: jsonErrorToolResult(`Semantic-layer source "${input.sourceName}" was not found.`);
},
toolTelemetry,
);
registerParsedTool(
@ -711,6 +751,7 @@ export function registerKtxContextTools(deps: RegisterKtxContextToolsDeps): void
);
return jsonToolResult(projectSlQueryResult(result, input.include));
},
toolTelemetry,
);
}
@ -728,6 +769,7 @@ export function registerKtxContextTools(deps: RegisterKtxContextToolsDeps): void
},
entityDetailsSchema,
async (input) => jsonToolResult(await entityDetails.read(input)),
toolTelemetry,
);
}
@ -745,6 +787,7 @@ export function registerKtxContextTools(deps: RegisterKtxContextToolsDeps): void
},
dictionarySearchSchema,
async (input) => jsonToolResult(await dictionarySearch.search(input)),
toolTelemetry,
);
}
@ -762,6 +805,7 @@ export function registerKtxContextTools(deps: RegisterKtxContextToolsDeps): void
},
discoverDataSchema,
async (input) => jsonToolResult({ refs: await discover.search(input) }),
toolTelemetry,
);
}
@ -791,6 +835,7 @@ export function registerKtxContextTools(deps: RegisterKtxContextToolsDeps): void
),
);
},
toolTelemetry,
);
}
@ -818,6 +863,7 @@ export function registerKtxContextTools(deps: RegisterKtxContextToolsDeps): void
};
return jsonToolResult(await memoryIngest.ingest(ingestInput));
},
toolTelemetry,
);
registerParsedTool(
@ -835,6 +881,7 @@ export function registerKtxContextTools(deps: RegisterKtxContextToolsDeps): void
const status = await memoryIngest.status(input.runId);
return status ? jsonToolResult(status) : jsonErrorToolResult(`Memory ingest run "${input.runId}" was not found.`);
},
toolTelemetry,
);
}
}