43b11d92ab
- F1: added '/icons/' to STATIC_PATH_PREFIXES — public/icons/ is copied verbatim to client/dist (a sibling of the already-included brand/vad/locales), and index.html references /icons/favicon-*.png on every page load, so those requests were getting their own route labels instead of collapsing to `static`. - F2: corrected the comment — only /assets/ is content-hashed (unbounded per deploy); /vad//brand//locales//icons/ have stable names (repetitive, not unbounded). Either way none belong in the API-route histogram. - F3: the negative test now exercises the trailing-slash boundary (the actual anti-false-collapse guard): '/assets' (no slash), '/assetsx/foo.js', '/iconset/x.png' must NOT collapse to `static` — cases that a buggy includes()/slashless-prefix impl would wrongly collapse. Plus '/icons/*' added to the positive it.each. Gate: server tsc 0; metrics.spec passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
126 lines
4.6 KiB
TypeScript
126 lines
4.6 KiB
TypeScript
import { FastifyRequest } from 'fastify';
|
|
import { resolveRouteLabel } from './http-metrics.hook';
|
|
import { firstSqlToken, isStreamingResponse } from './metrics.constants';
|
|
|
|
describe('resolveRouteLabel (histogram route label)', () => {
|
|
it('uses the ROUTE TEMPLATE, never the raw URL', () => {
|
|
// routeOptions.url is the matched template; url is the raw path with the id.
|
|
const req = {
|
|
url: '/api/pages/abc-123-def',
|
|
routeOptions: { url: '/api/pages/:id' },
|
|
} as unknown as FastifyRequest;
|
|
expect(resolveRouteLabel(req)).toBe('/api/pages/:id');
|
|
expect(resolveRouteLabel(req)).not.toContain('abc-123-def');
|
|
});
|
|
|
|
it('falls back to "unknown" on a 404 (no matched route template)', () => {
|
|
const req = {
|
|
url: '/totally/unmatched/path',
|
|
routeOptions: {},
|
|
} as unknown as FastifyRequest;
|
|
expect(resolveRouteLabel(req)).toBe('unknown');
|
|
});
|
|
|
|
it('falls back to "unknown" when routeOptions is missing', () => {
|
|
const req = { url: '/x' } as unknown as FastifyRequest;
|
|
expect(resolveRouteLabel(req)).toBe('unknown');
|
|
});
|
|
|
|
it.each([
|
|
'/assets/index-CAbxDtto.js',
|
|
'/assets/chunk-3OPIFGDE-CJOt9nr5.js',
|
|
'/assets/excalidraw-menu-DpsI0kFW.js',
|
|
'/vad/silero_vad_v5.onnx',
|
|
'/brand/logo.svg',
|
|
'/locales/en.json',
|
|
'/icons/app-icon-192x192.png',
|
|
])('collapses hashed/static asset %p to "static" (#362 cardinality)', (url) => {
|
|
// @fastify/static serves each file through a route whose matched url is the
|
|
// raw (hashed) file path, so routeOptions.url is itself unbounded here.
|
|
const req = {
|
|
url,
|
|
routeOptions: { url },
|
|
} as unknown as FastifyRequest;
|
|
const label = resolveRouteLabel(req);
|
|
expect(label).toBe('static');
|
|
expect(label).not.toContain('.js');
|
|
expect(label).not.toContain('index-');
|
|
});
|
|
|
|
it('strips the query string before the static-prefix check', () => {
|
|
const req = {
|
|
url: '/assets/index-CAbxDtto.js?v=2',
|
|
routeOptions: { url: '/assets/index-CAbxDtto.js' },
|
|
} as unknown as FastifyRequest;
|
|
expect(resolveRouteLabel(req)).toBe('static');
|
|
});
|
|
|
|
it('does NOT collapse a real API route that merely mentions assets', () => {
|
|
// A templated API route is kept as-is; only the static path PREFIXES collapse.
|
|
const req = {
|
|
url: '/api/pages/assets-guide',
|
|
routeOptions: { url: '/api/pages/:id' },
|
|
} as unknown as FastifyRequest;
|
|
expect(resolveRouteLabel(req)).toBe('/api/pages/:id');
|
|
});
|
|
|
|
it.each([
|
|
// The TRAILING SLASH on the prefix is the anti-false-collapse guard: a path
|
|
// that is the prefix WITHOUT its slash, or merely shares the prefix as a
|
|
// substring of a longer segment, must NOT collapse. These would collapse
|
|
// under a buggy `includes('/assets/')` / slashless-prefix impl.
|
|
'/assets',
|
|
'/assetsx/foo.js',
|
|
'/iconset/x.png',
|
|
])('does NOT collapse the prefix-boundary case %p', (url) => {
|
|
const req = {
|
|
url,
|
|
routeOptions: { url: '/some/:route' },
|
|
} as unknown as FastifyRequest;
|
|
expect(resolveRouteLabel(req)).not.toBe('static');
|
|
expect(resolveRouteLabel(req)).toBe('/some/:route');
|
|
});
|
|
});
|
|
|
|
describe('isStreamingResponse (SSE exclusion)', () => {
|
|
it('excludes text/event-stream responses by content-type', () => {
|
|
expect(isStreamingResponse('text/event-stream', '/api/ai-chat/stream')).toBe(
|
|
true,
|
|
);
|
|
expect(isStreamingResponse('text/event-stream; charset=utf-8', '/x')).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
it('excludes known /stream routes by suffix as a fallback', () => {
|
|
expect(isStreamingResponse('application/json', '/api/ai-chat/stream')).toBe(
|
|
true,
|
|
);
|
|
expect(isStreamingResponse(undefined, '/api/shares/ai/stream')).toBe(true);
|
|
});
|
|
|
|
it('does not exclude ordinary JSON responses', () => {
|
|
expect(isStreamingResponse('application/json', '/api/pages/:id')).toBe(
|
|
false,
|
|
);
|
|
expect(isStreamingResponse(undefined, '/api/pages/:id')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('firstSqlToken (bounded db label)', () => {
|
|
it('returns the lower-cased leading keyword', () => {
|
|
expect(firstSqlToken('SELECT * FROM pages')).toBe('select');
|
|
expect(firstSqlToken(' insert into x values (1)')).toBe('insert');
|
|
expect(firstSqlToken('UPDATE pages SET a=1')).toBe('update');
|
|
expect(firstSqlToken('delete from pages')).toBe('delete');
|
|
expect(firstSqlToken('(SELECT 1)')).toBe('select');
|
|
});
|
|
|
|
it('collapses unknown/empty queries to "other"', () => {
|
|
expect(firstSqlToken('')).toBe('other');
|
|
expect(firstSqlToken(undefined)).toBe('other');
|
|
expect(firstSqlToken('123 not sql')).toBe('other');
|
|
expect(firstSqlToken('vacuum analyze')).toBe('other');
|
|
});
|
|
});
|