fix(mcp): skip bytes histogram when sizer throws

Previously a sizer exception (e.g. upstream result shape change) recorded
bytes=0, silently poisoning the 0-bucket with synthetic data. Skip the
histogram observe entirely so distribution stats stay honest.
This commit is contained in:
Nathan Hangen 2026-05-10 21:33:55 -04:00
parent 8e2c3dce93
commit 6b37e6e228
2 changed files with 25 additions and 4 deletions

View file

@ -158,13 +158,12 @@ export async function observe<T>(
requestsCounter?.add(1, labels);
durationHistogram?.record(durationSeconds, labels);
if (!errored) {
let bytes = 0;
try {
bytes = sizer(result);
const bytes = sizer(result);
resultBytesHistogram?.record(bytes, { tool });
} catch {
bytes = 0;
// Skip the histogram rather than poison the 0-bucket on a shape change.
}
resultBytesHistogram?.record(bytes, { tool });
}
return result;
} catch (err) {

View file

@ -226,6 +226,28 @@ describe('privacy: target_info / scope_info suppression', () => {
});
});
describe('observe() — sizer fault tolerance', () => {
it('skips the bytes histogram when sizer throws (does not record bytes=0)', async () => {
process.env['GITNEXUS_OTEL_METRICS'] = 'on';
await withRandomPort(async () => {
const init = await initMetrics();
await observe(
'list_repos',
async () => ({ content: [{ type: 'text', text: 'ok' }] }),
() => {
throw new Error('shape changed');
},
() => false,
);
const text = await fetch(`http://127.0.0.1:${init.port}/metrics`).then((r) => r.text());
expect(text).toContain(
'gitnexus_mcp_tool_requests_total{tool="list_repos",error="false"} 1',
);
expect(text).not.toMatch(/gitnexus_mcp_tool_result_bytes_count\{tool="list_repos"\}/);
});
});
});
describe('bucket boundary contract', () => {
it('publishes the documented duration buckets verbatim', () => {
expect(DURATION_BUCKETS_SECONDS).toEqual([0.001, 0.005, 0.025, 0.1, 0.5, 2.5, 10, 30]);