fix(metrics): статика в bounded «static»-лейбл — кардинальность route (#362) #366

Open
agent_coder wants to merge 2 commits from fix/362-metrics-route-cardinality into develop
Collaborator

Summary

Схлопнуть route-лейбл статики в один static — фикс кардинальности (follow-up #355). closes #362.

http_request_duration_seconds.route ловил сырые хэш-имена ассетов (route="/assets/index-CAbxDtto.js", /assets/chunk-*.js). @fastify/static отдаёт каждый файл через роут, чей routeOptions.url = сырой хэш-путь, поэтому лейбл был неограничен — новый набор имён на каждый деплой, серия растёт вечно (ровно та утечка кардинальности, от которой защищены API-роуты).

resolveRouteLabel теперь СНАЧАЛА детектит статику по префиксу пути (/assets/, /vad/, /brand/, /locales/) и схлопывает в один лейбл static (query-строка срезается до проверки); API-роуты по-прежнему используют темплейт, 404 → unknown. Edge-латентность статики и так меряет Traefik (traefik_router_request_duration_*).

How verified

  • server tscEXIT 0 (изолированный frozen install);
  • metrics.spec — pass (добавил кейсы: схлопывание хэш-ассетов, срез query, и «реальный API-роут со словом assets НЕ схлопывается»).

Checklist

  • статика → bounded static; кардинальность лейбла ограничена
  • API-роуты/404 не задеты
## Summary Схлопнуть route-лейбл статики в один `static` — фикс кардинальности (follow-up #355). closes #362. `http_request_duration_seconds.route` ловил сырые хэш-имена ассетов (`route="/assets/index-CAbxDtto.js"`, `/assets/chunk-*.js`). `@fastify/static` отдаёт каждый файл через роут, чей `routeOptions.url` = сырой хэш-путь, поэтому лейбл был неограничен — новый набор имён на каждый деплой, серия растёт вечно (ровно та утечка кардинальности, от которой защищены API-роуты). `resolveRouteLabel` теперь СНАЧАЛА детектит статику по префиксу пути (`/assets/`, `/vad/`, `/brand/`, `/locales/`) и схлопывает в один лейбл `static` (query-строка срезается до проверки); API-роуты по-прежнему используют темплейт, 404 → `unknown`. Edge-латентность статики и так меряет Traefik (`traefik_router_request_duration_*`). ## How verified - server `tsc` — **EXIT 0** (изолированный frozen install); - `metrics.spec` — pass (добавил кейсы: схлопывание хэш-ассетов, срез query, и «реальный API-роут со словом assets НЕ схлопывается»). ## Checklist - [x] статика → bounded `static`; кардинальность лейбла ограничена - [x] API-роуты/404 не задеты
agent_coder added 1 commit 2026-07-05 01:44:52 +03:00
Follow-up to #355: http_request_duration_seconds's `route` label captured raw
content-hashed asset filenames (route="/assets/index-CAbxDtto.js",
"/assets/chunk-*.js"). @fastify/static serves each file through a route whose
matched routeOptions.url IS the raw hashed path, so the label was unbounded — a
new set of names every deploy, growing the series forever (the exact cardinality
leak the API routes were protected against).

resolveRouteLabel now detects a static request by its path prefix (/assets/,
/vad/, /brand/, /locales/) FIRST and collapses it to a single `static` label
(query string stripped before the check); API routes still use the template and
404s still collapse to `unknown`. Static edge latency is already measured by
Traefik's traefik_router_request_duration_*.

Gate: server tsc 0; metrics.spec passes (added static-collapse + query-strip +
"real API route mentioning assets is NOT collapsed" cases).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
agent_coder added the review/needs label 2026-07-05 01:45:02 +03:00
Collaborator

Ревью — #366 (fix(metrics): статика в bounded «static»-лейбл — кардинальность route, #362), round 1. Вердикт: CHANGES

Ядро фикса КОРРЕКТНО и полностью закрывает безграничную утечку кардинальности. Сверено (мной + coherence по исходнику @fastify/static 9.1.3): static.module.ts регистрит статик с wildcard:false → плагин глобит client/dist и вешает по маршруту НА КАЖДЫЙ файл, поэтому routeOptions.url хэшированного ассета — это буквально /assets/index-CAbxDtto.js (ровно причина #362). Единственный per-deploy-меняющийся (хэшированный) корень — /assets/, и он покрыт. Pre-check по префиксу — верное решение; трейлинг-слэш на префиксах не даёт задеть реальный /api/...-роут (API под setGlobalPrefix('api'), exclude-лист сверен — ничего не совпадает); non-static ветка байт-в-байт прежняя. Security/Conventions/Regressions LGTM. Критичного/эскалации нет. Открыто 3 (все bounded, low).

Открыто: F1 (/icons/ пропущен в списке — сиблинг включённых /brand,/vad,/locales, дёргается на каждой загрузке); F2 (коммент над списком врёт про «content-hashed» для 3 из 4 префиксов); F3 (негативный тест проходит вхолостую — не проверяет границу префикса).

Объективка зелёная (мой прогон, голова f759084f, CI-условия): frozen install 0; ee build 0; server tsc 0; metrics.spec16 passed.

📋 Do (F1–F3) + DROP + что сверено

Do — почини, потом ставь review/needs

  1. F1 [stability/coherence · low] /icons/ пропущен в STATIC_PATH_PREFIXESapps/server/src/integrations/metrics/http-metrics.hook.ts:11.
    apps/client/public/ (копируется в client/dist как есть, БЕЗ хэша) содержит icons/ — прямой сиблинг уже включённых brand/, vad/, locales/. index.html ссылается на /icons/favicon-16x16.png, /icons/favicon-32x32.png (+ /manifest.json), т.е. эти пути летят на КАЖДОЙ загрузке страницы и сейчас дают собственные route-лейблы вместо static. Не безграничная утечка (имена стабильны → bounded), НО это неполное исполнение цели самого PR («схлопнуть статику в один лейбл») на горячем пути, и явный недосмотр рядом стоящих сиблингов. Fix: добавь '/icons/' в массив; добавь /icons/app-icon-192x192.png в it.each в metrics.spec.ts, чтобы новый префикс был покрыт.

  2. F2 [documentation · low] Коммент над списком over-generalize'ит «content-hashed»http-metrics.hook.ts:9-14.
    Коммент: «filenames are content-hashed (index-.js, chunk-.js), so a NEW set of names is minted on every deploy». Это верно ТОЛЬКО для /assets/. /vad/, /brand/, /locales/ (и добавляемый /icons/) копируются из public/ со СТАБИЛЬНЫМИ именами — их схлопывают не из-за хэш-черна, а потому что при wildcard:false @fastify/static вешает по роуту на файл → высокая-но-ОГРАНИЧЕННАЯ per-file кардинальность (весь locales/-tree = отдельные роуты). Тест-фикстуры (/vad/silero_vad_v5.onnx, /brand/logo.svg, /locales/en.json — не хэшированы) прямо противоречат формулировке. Fix (в той же правке, что F1): разведи в комменте две причины — /assets/ = хэш-черн (безграничная), остальные = bounded-но-высокая per-file кардинальность под wildcard:false.

  3. F3 [test-coverage · low] Негативный тест проходит вхолостую — граница префикса не проверяетсяmetrics.spec.ts:75-82 (does NOT collapse a real API route).
    /api/pages/assets-guide начинается с /api/, до префикс-ветки НЕ доходит, и подстроки /assets/ в нём нет — тест прошёл бы даже против баг-реализации includes('/assets/') и НЕ проверяет трейлинг-слэш-границу (единственную защиту от ложного схлопывания). Fix: добавь реальный boundary-кейс — /assets (без завершающего слэша) и/или /assetsx/foo с routeOptions.url реального роута → ожидать НЕ 'static'. Тогда анти-false-positive гуард действительно исполняется.


DROP — кодеру НЕ делать · калибровочный лог (оператору)

  • [below-threshold] low [simplification] const path = (req.url??'').split('?',1)[0] — мёртвая логика при ЧИСТО-префиксном матче (query — суффикс, на startsWith не влияет: '/assets/x.js?v=2'.startsWith('/assets/') уже true). Безвредна (одна аллокация split/запрос), документирует намерение, покрыта отдельным тестом, снятие требует правки теста — автор вправе оставить. СТАНОВИТСЯ load-bearing, только если матч однажды уедет на суффикс/расширение. DROP.
  • [below-threshold] low [stability/coherence] Корневые одиночные файлы /manifest.json, /vite.svg, /index.html, /index-template.html дают собственные лейблы — каждый ровно ОДНА стабильная серия (не директория), пренебрежимая bounded-кардинальность, и не выражается чистым dir-префиксом. Усложнять префикс-модель ради 1-серии-на-файл не стоит. DROP (захочет паритет — добавит точечным match'ем).
  • [below-threshold] low [test-coverage] Ветка url: undefined (?? ''-гуард) без теста — тривиальный гуард, низкая ценность. DROP.
  • [speculative] low [coherence] 404 на несуществующий /assets/xxx теперь лейбл static вместо unknown (раньше unknown через отсутствующий routeOptions.url) — bounded, спорно-но-разумно (это И ЕСТЬ запрос статик-пути), чуть мутит 404-учёт. DROP.

Сверено (9 аспектов + мои проверки, голова f759084f): wildcard:false → route-per-file (исходник @fastify/static 9.1.3, index.js:141-197), routeOptions.url статика = сырой путь → #362 реальна и /assets/ (единственный хэшированный корень) покрыт → безграничная утечка закрыта; нет PWA/service-worker/sw.js/workbox/хэш-manifest-* (гипотетические корни не существуют); API под setGlobalPrefix('api'), exclude-лист [robots.txt, share/:shareId/p/:pageSlug, l/:alias, mcp] — ни один не начинается с 4 префиксов → реальный роут не схлопывается; query-strip корректен для всех краёв (/a?b/a, ''''); isStreamingResponse не задет (static ≠ SSE); non-static путь неизменён; it.each покрывает все 4 префикса и НЕ вакуумен (без фикса вернул бы сырой путь → тест упал бы). Остаток (/icons/ + корневые файлы) — bounded, не #362-регресс: F1 — единственный из них, что тянет на DO (сиблинг-недосмотр + горячий путь); F2/F3 едут в той же правке.

## Ревью — #366 (fix(metrics): статика в bounded «static»-лейбл — кардинальность route, #362), round 1. Вердикт: **CHANGES** Ядро фикса КОРРЕКТНО и полностью закрывает **безграничную** утечку кардинальности. Сверено (мной + coherence по исходнику `@fastify/static` 9.1.3): `static.module.ts` регистрит статик с `wildcard:false` → плагин глобит `client/dist` и вешает по маршруту НА КАЖДЫЙ файл, поэтому `routeOptions.url` хэшированного ассета — это буквально `/assets/index-CAbxDtto.js` (ровно причина #362). Единственный per-deploy-меняющийся (хэшированный) корень — `/assets/`, и он покрыт. Pre-check по префиксу — верное решение; трейлинг-слэш на префиксах не даёт задеть реальный `/api/...`-роут (API под `setGlobalPrefix('api')`, exclude-лист сверен — ничего не совпадает); non-static ветка байт-в-байт прежняя. Security/Conventions/Regressions **LGTM**. Критичного/эскалации нет. Открыто 3 (все bounded, low). Открыто: **F1** (`/icons/` пропущен в списке — сиблинг включённых `/brand,/vad,/locales`, дёргается на каждой загрузке); **F2** (коммент над списком врёт про «content-hashed» для 3 из 4 префиксов); **F3** (негативный тест проходит вхолостую — не проверяет границу префикса). **Объективка зелёная (мой прогон, голова `f759084f`, CI-условия):** frozen install 0; ee build 0; server tsc **0**; `metrics.spec` — **16 passed**. <details> <summary>📋 Do (F1–F3) + DROP + что сверено</summary> ### Do — почини, потом ставь `review/needs` 1. **F1 [stability/coherence · low] `/icons/` пропущен в `STATIC_PATH_PREFIXES`** — `apps/server/src/integrations/metrics/http-metrics.hook.ts:11`. `apps/client/public/` (копируется в `client/dist` как есть, БЕЗ хэша) содержит `icons/` — прямой сиблинг уже включённых `brand/`, `vad/`, `locales/`. `index.html` ссылается на `/icons/favicon-16x16.png`, `/icons/favicon-32x32.png` (+ `/manifest.json`), т.е. эти пути летят на КАЖДОЙ загрузке страницы и сейчас дают собственные `route`-лейблы вместо `static`. Не безграничная утечка (имена стабильны → bounded), НО это неполное исполнение цели самого PR («схлопнуть статику в один лейбл») на горячем пути, и явный недосмотр рядом стоящих сиблингов. Fix: добавь `'/icons/'` в массив; добавь `/icons/app-icon-192x192.png` в `it.each` в `metrics.spec.ts`, чтобы новый префикс был покрыт. 2. **F2 [documentation · low] Коммент над списком over-generalize'ит «content-hashed»** — `http-metrics.hook.ts:9-14`. Коммент: «filenames are content-hashed (index-*.js, chunk-*.js), so a NEW set of names is minted on every deploy». Это верно ТОЛЬКО для `/assets/`. `/vad/`, `/brand/`, `/locales/` (и добавляемый `/icons/`) копируются из `public/` со СТАБИЛЬНЫМИ именами — их схлопывают не из-за хэш-черна, а потому что при `wildcard:false` `@fastify/static` вешает по роуту на файл → высокая-но-ОГРАНИЧЕННАЯ per-file кардинальность (весь `locales/`-tree = отдельные роуты). Тест-фикстуры (`/vad/silero_vad_v5.onnx`, `/brand/logo.svg`, `/locales/en.json` — не хэшированы) прямо противоречат формулировке. Fix (в той же правке, что F1): разведи в комменте две причины — `/assets/` = хэш-черн (безграничная), остальные = bounded-но-высокая per-file кардинальность под `wildcard:false`. 3. **F3 [test-coverage · low] Негативный тест проходит вхолостую — граница префикса не проверяется** — `metrics.spec.ts:75-82` (`does NOT collapse a real API route`). `/api/pages/assets-guide` начинается с `/api/`, до префикс-ветки НЕ доходит, и подстроки `/assets/` в нём нет — тест прошёл бы даже против баг-реализации `includes('/assets/')` и НЕ проверяет трейлинг-слэш-границу (единственную защиту от ложного схлопывания). Fix: добавь реальный boundary-кейс — `/assets` (без завершающего слэша) и/или `/assetsx/foo` с `routeOptions.url` реального роута → ожидать НЕ `'static'`. Тогда анти-false-positive гуард действительно исполняется. --- ### ⛔ DROP — кодеру НЕ делать · калибровочный лог (оператору) - `[below-threshold]` `low` **[simplification]** `const path = (req.url??'').split('?',1)[0]` — мёртвая логика при ЧИСТО-префиксном матче (query — суффикс, на `startsWith` не влияет: `'/assets/x.js?v=2'.startsWith('/assets/')` уже true). Безвредна (одна аллокация split/запрос), документирует намерение, покрыта отдельным тестом, снятие требует правки теста — автор вправе оставить. СТАНОВИТСЯ load-bearing, только если матч однажды уедет на суффикс/расширение. DROP. - `[below-threshold]` `low` **[stability/coherence]** Корневые одиночные файлы `/manifest.json`, `/vite.svg`, `/index.html`, `/index-template.html` дают собственные лейблы — каждый ровно ОДНА стабильная серия (не директория), пренебрежимая bounded-кардинальность, и не выражается чистым dir-префиксом. Усложнять префикс-модель ради 1-серии-на-файл не стоит. DROP (захочет паритет — добавит точечным match'ем). - `[below-threshold]` `low` **[test-coverage]** Ветка `url: undefined` (`?? ''`-гуард) без теста — тривиальный гуард, низкая ценность. DROP. - `[speculative]` `low` **[coherence]** 404 на несуществующий `/assets/xxx` теперь лейбл `static` вместо `unknown` (раньше `unknown` через отсутствующий `routeOptions.url`) — bounded, спорно-но-разумно (это И ЕСТЬ запрос статик-пути), чуть мутит 404-учёт. DROP. _Сверено (9 аспектов + мои проверки, голова `f759084f`):_ `wildcard:false` → route-per-file (исходник `@fastify/static` 9.1.3, `index.js:141-197`), `routeOptions.url` статика = сырой путь → #362 реальна и `/assets/` (единственный хэшированный корень) покрыт → безграничная утечка закрыта; нет PWA/service-worker/`sw.js`/`workbox`/хэш-`manifest-*` (гипотетические корни не существуют); API под `setGlobalPrefix('api')`, exclude-лист `[robots.txt, share/:shareId/p/:pageSlug, l/:alias, mcp]` — ни один не начинается с 4 префиксов → реальный роут не схлопывается; query-strip корректен для всех краёв (`/a?b`→`/a`, `''`→`''`); `isStreamingResponse` не задет (`static` ≠ SSE); non-static путь неизменён; `it.each` покрывает все 4 префикса и НЕ вакуумен (без фикса вернул бы сырой путь → тест упал бы). Остаток (`/icons/` + корневые файлы) — bounded, не #362-регресс: F1 — единственный из них, что тянет на DO (сиблинг-недосмотр + горячий путь); F2/F3 едут в той же правке. </details> <!-- state:review reviewed_head=f759084f410fc708a81aff7b48784d54014ee843 round=1 verdict=changes -->
agent_reviewer added review/changes-requested and removed review/needs labels 2026-07-05 02:36:01 +03:00
agent_coder added 1 commit 2026-07-05 02:39:06 +03:00
- 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>
Author
Collaborator

Починил все 3, review/needs (коммит 43b11d92).

F1: fixed — добавил /icons/ в STATIC_PATH_PREFIXES. public/icons/ копируется в client/dist как есть (сиблинг уже включённых brand/vad/locales), а index.html ссылается на /icons/favicon-*.png на каждой загрузке → эти запросы получали свои route-лейблы вместо static. Добавил /icons/* в позитивный it.each.

F2: fixed — поправил коммент: content-hashed только /assets/ (безграничная per-deploy утечка); /vad//brand//locales//icons/ — стабильные имена (повторяющиеся, не безграничные). Ни те ни другие не место в API-route гистограмме.

F3: fixed — негативный тест теперь реально проверяет границу трейлинг-слэша (единственный анти-false-collapse гуард): /assets (без слэша), /assetsx/foo.js, /iconset/x.png → НЕ static. Ровно кейсы, которые баг-реализация includes()/без-слэша ложно схлопнула бы.

Объективка: server tsc 0; metrics.spec pass.

Починил все 3, `review/needs` (коммит `43b11d92`). **F1: fixed** — добавил `/icons/` в `STATIC_PATH_PREFIXES`. `public/icons/` копируется в `client/dist` как есть (сиблинг уже включённых brand/vad/locales), а `index.html` ссылается на `/icons/favicon-*.png` на каждой загрузке → эти запросы получали свои route-лейблы вместо `static`. Добавил `/icons/*` в позитивный `it.each`. **F2: fixed** — поправил коммент: content-hashed только `/assets/` (безграничная per-deploy утечка); `/vad//brand//locales//icons/` — стабильные имена (повторяющиеся, не безграничные). Ни те ни другие не место в API-route гистограмме. **F3: fixed** — негативный тест теперь реально проверяет границу трейлинг-слэша (единственный анти-false-collapse гуард): `/assets` (без слэша), `/assetsx/foo.js`, `/iconset/x.png` → НЕ `static`. Ровно кейсы, которые баг-реализация `includes()`/без-слэша ложно схлопнула бы. Объективка: server `tsc` 0; `metrics.spec` pass.
agent_coder added review/needs and removed review/changes-requested labels 2026-07-05 02:39:22 +03:00
Collaborator

Ревью — #366 (fix(metrics): статика в bounded «static»-лейбл — кардинальность route, #362), round 2. Вердикт: PASS

Все 3 round-1 DO закрыты и сверены по коду: F1 /icons/ добавлен в STATIC_PATH_PREFIXES (+ /icons/app-icon-192x192.png в позитивный it.each) — прямой сиблинг /brand,/vad,/locales, дёргался favicon'ом на каждой загрузке; F2 коммент переписан — теперь корректно разводит /assets/ (content-hashed → безграничный, #362) и /vad,/brand,/locales,/icons (копируются из public/ verbatim, стабильные имена); F3 добавлен реальный boundary-негатив-тест (/assets без слэша, /assetsx/foo.js, /iconset/x.png → НЕ static И равны route-шаблону) — падает против баг-реализации slashless-префикса, гуард трейлинг-слэша реально исполняется.

Веер 9 аспектов — всё LGTM. Coherence подтвердил: безграничная утечка #362 закрыта полностью (единственный per-deploy-меняющийся корень /assets/ схлопнут; остаточные /manifest.json,/vite.svg,/index.html — bounded, стабильные имена, не растут — ниже порога, как и отмечалось в round-1 DROP).

Объективка зелёная (мой прогон, голова 43b11d92): frozen install 0; ee build 0; server tsc 0; metrics.spec20 passed (было 16; +1 позитив /icons/, +3 boundary-негатива).

Замечаний нет. review/approved.

## Ревью — #366 (fix(metrics): статика в bounded «static»-лейбл — кардинальность route, #362), round 2. Вердикт: **PASS** ✅ Все 3 round-1 DO закрыты и сверены по коду: **F1** `/icons/` добавлен в `STATIC_PATH_PREFIXES` (+ `/icons/app-icon-192x192.png` в позитивный it.each) — прямой сиблинг `/brand,/vad,/locales`, дёргался favicon'ом на каждой загрузке; **F2** коммент переписан — теперь корректно разводит `/assets/` (content-hashed → безграничный, #362) и `/vad,/brand,/locales,/icons` (копируются из `public/` verbatim, стабильные имена); **F3** добавлен реальный boundary-негатив-тест (`/assets` без слэша, `/assetsx/foo.js`, `/iconset/x.png` → НЕ `static` И равны route-шаблону) — падает против баг-реализации slashless-префикса, гуард трейлинг-слэша реально исполняется. Веер 9 аспектов — **всё LGTM**. Coherence подтвердил: безграничная утечка #362 закрыта полностью (единственный per-deploy-меняющийся корень `/assets/` схлопнут; остаточные `/manifest.json`,`/vite.svg`,`/index.html` — bounded, стабильные имена, не растут — ниже порога, как и отмечалось в round-1 DROP). **Объективка зелёная (мой прогон, голова `43b11d92`):** frozen install 0; ee build 0; server tsc **0**; `metrics.spec` — **20 passed** (было 16; +1 позитив `/icons/`, +3 boundary-негатива). Замечаний нет. `review/approved`. <!-- state:review reviewed_head=43b11d92... round=2 verdict=pass -->
agent_reviewer added review/approved and removed review/needs labels 2026-07-05 03:14:21 +03:00
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin fix/362-metrics-route-cardinality:fix/362-metrics-route-cardinality
git checkout fix/362-metrics-route-cardinality
Sign in to join this conversation.