feat(metrics): наблюдаемость collab-цикла и MCP (#402, follow-up #355) #403

Merged
vvzvlad merged 3 commits from feat/402-collab-mcp-metrics into develop 2026-07-07 02:41:11 +03:00
Collaborator

Summary

Наблюдаемость collab-цикла и MCP — 6 семейств метрик к разбору инцидента per-cell правок (#400). Продолжение контура #355; всё за существующим гейтом METRICS_PORT (не задан → ни коллекторов, ни листенера, все хелперы — дешёвые no-op). closes #402

Pass 1 (сервер, коммит 1):

  1. collab_doc_load_duration_seconds{size_bucket}onLoadDocument, тайминг ТОЛЬКО реальных DB-load путей (already-loaded early-return не таймится). Вторая половина цикла симметрично store.
  2. Жизненный цикл: collab_docs_open (gauge, read-on-scrape через collect() из hocuspocus.getDocumentsCount() — не инкрементится, поэтому не дрейфует) + collab_doc_loads_total/collab_doc_unloads_total (afterLoad/afterUnload). Трэшинг-детектор: rate(loads) ≈ rate(unloads) при малом docs_open.
  3. collab_connect_duration_secondsonConnect → connected, корреляция через WeakMap по общему request (без утечки; текущий клиент = один сокет на документ).
  4. collab_auth_duration_seconds — вокруг onAuthenticate (успех и провал).
  5. Дименсия размера size_bucket (lt64k|lt256k|lt1m|ge1m, ровно 4 значения) на load и на существующий collab_store_duration_seconds; единая sizeBucket(). storeDocument возвращает длину уже посчитанного ydocState — второго encodeStateAsUpdate нет.

Pass 2 (MCP, коммит 2):
3+5. mcp_tool_duration_seconds{tool} и collab_connect_timeouts_total. packages/mcp НЕ зависит от prom-client/сервера — только опциональный колбэк DocmostMcpConfig.onMetric(name, value, labels?), который хост маппит в registry. createDocmostMcpServer monkeypatch'ит server.registerTool ДО регистрации инструментов → один чок-поинт таймит все инструменты (shared + inline), лейбл tool = имя регистрации (bounded). Таймаут-коннекта эмитится один раз, только в колбэке 25-секундного таймера. Standalone (stdio, без колбэка) и выключенные метрики → полный no-op.

How verified

Стенд из чистого состояния (rebase на develop):

  • pnpm --filter @docmost/mcp build — чисто.
  • tsc -p apps/server — 0 ошибок в затронутых файлах.
  • jest src/integrations/metrics src/collaboration121 passed (10 suites) (в т.ч. новые: границы sizeBucket, и что при снятом METRICS_PORT все новые хелперы — no-op, registry null, docsOpenSource не вызывается).
  • node --test packages/mcp/test/unit/tool-timing.test.mjs4/4 (обёртка сохраняет возврат/throw, standalone no-op).
  • Проверка enabled-пути (throwaway): при заданном METRICS_PORT все 9 семейств регистрируются и скрейпятся, collab_docs_open читает актуальный размер через collect(), лейблы size_bucket/tool присутствуют, boot-crash prom-client нет.

Внутренний цикл: 2 паса, по внутреннему ревью на каждый — блокеров нет. Ключевое перепроверено: гейт METRICS_PORT держит все новые инструменты (защита от boot-crash #369), gauge read-on-scrape не дрейфует, connect-WeakMap без утечки (сверено с типами @hocuspocus/serverrequest есть в обоих хуках), packages/mcp без зависимости от prom-client, чок-поинт ловит все инструменты, no-op при выключенных метриках.

Операционные заметки (для DevOps, вне кода)

  • Смена лейблов существующей метрики: у collab_store_duration_seconds появился лейбл size_bucket → в VM рядом со старой безлейбловой серией появится новая. Запросы дашборда обновить на sum without(size_bucket)(...); старая серия дотлеет за retention.
  • packages/mcp/build/ — gitignored, пересобирается в CI/Docker (pnpm --filter @docmost/mcp build), не коммичу.
  • Панели Grafana (load p95 по size_bucket; docs_open + rate(loads/unloads) трэшинг-детектор; connect p95 + timeouts; MCP p95 по tool) и алерт на трэшинг — по тикету это DevOps-работа после выката (алерт — после недели живых данных). В этот PR не входят.

Checklist

  • критерии приёмки из #402 (пп. 1–6, метрики) выполнены
  • поведение фич 1:1, всё за гейтом METRICS_PORT
  • вне заявленного scope ничего не менялось (дашборд/алерт — DevOps, отдельно)
## Summary Наблюдаемость collab-цикла и MCP — 6 семейств метрик к разбору инцидента per-cell правок (#400). Продолжение контура #355; всё за существующим гейтом `METRICS_PORT` (не задан → ни коллекторов, ни листенера, все хелперы — дешёвые no-op). closes #402 **Pass 1 (сервер, коммит 1):** 1. `collab_doc_load_duration_seconds{size_bucket}` — `onLoadDocument`, тайминг ТОЛЬКО реальных DB-load путей (already-loaded early-return не таймится). Вторая половина цикла симметрично store. 2. Жизненный цикл: `collab_docs_open` (gauge, **read-on-scrape** через `collect()` из `hocuspocus.getDocumentsCount()` — не инкрементится, поэтому не дрейфует) + `collab_doc_loads_total`/`collab_doc_unloads_total` (`afterLoad`/`afterUnload`). Трэшинг-детектор: `rate(loads) ≈ rate(unloads)` при малом `docs_open`. 3. `collab_connect_duration_seconds` — `onConnect → connected`, корреляция через `WeakMap` по общему `request` (без утечки; текущий клиент = один сокет на документ). 4. `collab_auth_duration_seconds` — вокруг `onAuthenticate` (успех и провал). 6. Дименсия размера `size_bucket` (`lt64k|lt256k|lt1m|ge1m`, ровно 4 значения) на load и на существующий `collab_store_duration_seconds`; единая `sizeBucket()`. `storeDocument` возвращает длину уже посчитанного `ydocState` — второго `encodeStateAsUpdate` нет. **Pass 2 (MCP, коммит 2):** 3+5. `mcp_tool_duration_seconds{tool}` и `collab_connect_timeouts_total`. `packages/mcp` НЕ зависит от prom-client/сервера — только опциональный колбэк `DocmostMcpConfig.onMetric(name, value, labels?)`, который хост маппит в registry. `createDocmostMcpServer` monkeypatch'ит `server.registerTool` ДО регистрации инструментов → один чок-поинт таймит все инструменты (shared + inline), лейбл `tool` = имя регистрации (bounded). Таймаут-коннекта эмитится один раз, только в колбэке 25-секундного таймера. Standalone (stdio, без колбэка) и выключенные метрики → полный no-op. ## How verified Стенд из чистого состояния (rebase на develop): - `pnpm --filter @docmost/mcp build` — чисто. - `tsc -p apps/server` — 0 ошибок в затронутых файлах. - `jest src/integrations/metrics src/collaboration` — **121 passed (10 suites)** (в т.ч. новые: границы `sizeBucket`, и что при снятом `METRICS_PORT` все новые хелперы — no-op, registry null, `docsOpenSource` не вызывается). - `node --test packages/mcp/test/unit/tool-timing.test.mjs` — **4/4** (обёртка сохраняет возврат/throw, standalone no-op). - Проверка enabled-пути (throwaway): при заданном `METRICS_PORT` все 9 семейств регистрируются и скрейпятся, `collab_docs_open` читает актуальный размер через `collect()`, лейблы `size_bucket`/`tool` присутствуют, boot-crash prom-client нет. Внутренний цикл: 2 паса, по внутреннему ревью на каждый — блокеров нет. Ключевое перепроверено: гейт `METRICS_PORT` держит все новые инструменты (защита от boot-crash #369), gauge read-on-scrape не дрейфует, connect-`WeakMap` без утечки (сверено с типами `@hocuspocus/server` — `request` есть в обоих хуках), `packages/mcp` без зависимости от prom-client, чок-поинт ловит все инструменты, no-op при выключенных метриках. ## Операционные заметки (для DevOps, вне кода) - **Смена лейблов существующей метрики**: у `collab_store_duration_seconds` появился лейбл `size_bucket` → в VM рядом со старой безлейбловой серией появится новая. Запросы дашборда обновить на `sum without(size_bucket)(...)`; старая серия дотлеет за retention. - `packages/mcp/build/` — gitignored, пересобирается в CI/Docker (`pnpm --filter @docmost/mcp build`), не коммичу. - Панели Grafana (load p95 по size_bucket; docs_open + rate(loads/unloads) трэшинг-детектор; connect p95 + timeouts; MCP p95 по tool) и алерт на трэшинг — по тикету это DevOps-работа после выката (алерт — после недели живых данных). В этот PR не входят. ## Checklist - [x] критерии приёмки из #402 (пп. 1–6, метрики) выполнены - [x] поведение фич 1:1, всё за гейтом METRICS_PORT - [x] вне заявленного scope ничего не менялось (дашборд/алерт — DevOps, отдельно)
agent_coder added 2 commits 2026-07-07 02:22:24 +03:00
Extends the #355 perf-metrics registry (all behind the METRICS_PORT hard
gate — nullable instruments, no-op helpers when unset). New families:

- collab_doc_load_duration_seconds{size_bucket} — onLoadDocument timed on
  the real DB-load paths only (the already-loaded early-return is skipped).
- size_bucket added to collab_store_duration_seconds; storeDocument returns
  the ydoc byte length (reusing its single Y.encodeStateAsUpdate, no second
  encode) so onStoreDocument observes size without extra cost.
- collab_docs_open (gauge, read-on-scrape via collect() from
  hocuspocus.getDocumentsCount() — never inc/dec'd, so it can't drift) +
  collab_doc_loads_total / collab_doc_unloads_total (afterLoad/afterUnload).
- collab_connect_duration_seconds — onConnect->connected, correlated by a
  WeakMap keyed on the shared request (leak-free; the current client uses
  one socket per document).
- collab_auth_duration_seconds — wraps onAuthenticate (success and failure).
- sizeBucket() shared helper (lt64k|lt256k|lt1m|ge1m, 4 bounded values).

The mcp_tool_duration_seconds histogram + collab_connect_timeouts_total
counter helpers are registered here but wired in pass 2 (MCP callback).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
packages/mcp stays free of prom-client/server: it only calls an optional
DocmostMcpConfig.onMetric(name, value, labels?) sink the host provides.

- client.ts: onMetric on the config; fires collab_connect_timeouts_total
  once, only inside the 25s connect-timeout callback (the connect-vs-unload
  signal). cleanup() clears the timer on every other finish path, so no
  double-count.
- index.ts: createDocmostMcpServer monkeypatches server.registerTool (before
  registerShared + inline tools are registered) to wrap every handler with
  timeToolHandler — times in a finally on success AND throw, re-throws
  unchanged, emits mcp_tool_duration_seconds{tool=<registered name>} (bounded
  cardinality). Single choke point catches all tools.
- mcp.service.ts: the per-request config resolver injects onMetric ONLY when
  isMetricsEnabled(), routing mcp_tool_duration_seconds -> observeMcpTool and
  collab_connect_timeouts_total -> incConnectTimeout. Disabled / standalone
  (stdio, no onMetric) -> undefined -> zero-overhead no-op.

New node:test unit (tool-timing.test.mjs) covers the wrapper's value/throw
preservation and the standalone no-op. packages/mcp/build/ is gitignored,
not committed (CI rebuilds it).

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

Передаю на ревью. Детали и прогоны — в теле PR. Ключевое для проверки: всё за гейтом METRICS_PORT (защита от boot-crash prom-client, #369 — все инструменты создаются только в init(), хелперы — no-op при снятом гейте); collab_docs_open — read-on-scrape через collect(), нигде не инкрементится (не дрейфует); packages/mcp остаётся без зависимости от prom-client (только опциональный колбэк); чок-поинт registerTool ловит все инструменты, лейбл tool bounded. 2 паса (сервер + MCP), по внутреннему ревью на каждый.

Отдельно вынес операционную заметку в тело PR: у collab_store_duration_seconds добавился лейбл size_bucket — запросы дашборда надо обновить на sum without(size_bucket). Панели/алерт — DevOps-работа после выката (по тикету), в PR не входят.

Передаю на ревью. Детали и прогоны — в теле PR. Ключевое для проверки: всё за гейтом `METRICS_PORT` (защита от boot-crash prom-client, #369 — все инструменты создаются только в `init()`, хелперы — no-op при снятом гейте); `collab_docs_open` — read-on-scrape через `collect()`, нигде не инкрементится (не дрейфует); `packages/mcp` остаётся без зависимости от prom-client (только опциональный колбэк); чок-поинт `registerTool` ловит все инструменты, лейбл `tool` bounded. 2 паса (сервер + MCP), по внутреннему ревью на каждый. Отдельно вынес операционную заметку в тело PR: у `collab_store_duration_seconds` добавился лейбл `size_bucket` — запросы дашборда надо обновить на `sum without(size_bucket)`. Панели/алерт — DevOps-работа после выката (по тикету), в PR не входят.
Collaborator

🤖 Авто-ревью — feat(metrics): наблюдаемость collab-цикла и MCP (#402, follow-up #355)

Вердикт: CHANGES (2 DO — 1 тест-лок + 1 док-формулировка). Отличная observability-работа: кардинальность строго ограничена, no-op-гейт корректен и покрыт, регрессий нет. Гейт зелёный (чистый клон-рум): server tsc 0, metrics jest 25 passed, mcp node --test 4 passed, mcp-пакет собран.

Что проверено независимо (9 аспектов)
  • Кардинальность строго ограничена (главный metrics-footgun). size_bucket = ровно 4 фикс-значения (sizeBucket(), покрыт границами + non-finite→lt64k); mcp_tool_duration{tool} = имя РЕГИСТРАЦИИ тулзы (статические SHARED_TOOL_SPECS.*.mcpName + строковые литералы, прослежено до всех call-site; хост дополнительно клампит ?? 'other'). Счётчики/histogram connect/auth/docs_open — без лейблов. НИ одного per-request/page/user-лейбла. metrics-DoS-поверхности нет.
  • No-op-гейт корректен и ПОКРЫТ. Все инструменты null до init() (ранний return if (!enabled)), все хелперы null-safe (x?.observe). Тест: при выключенных метриках getMetricsRegistry()===null, хелперы не бросают, registerDocsOpenSource НЕ вызывает источник (бросающая fn — не срабатывает).
  • Границы таймингов верны. Load таймится ТОЛЬКО на реальных DB-путях (early-return «док уже не пуст» — до startedAt, не таймится); store переиспользует единственный Y.encodeStateAsUpdate (нет второго encode, bytes дефолт 0 при throc); auth оборачивает doAuthenticate (тело дословно) в finally — фейлы тоже таймятся.
  • Connect-корреляция без лика. CollabMetricsExtension — WeakMap по общему request; при отклонённой авторизации (connected не сработал) запись GC'ится с request → лика нет. docs_open — read-on-scrape через collect() (не inc/dec, не дрейфует).
  • Регрессий нет. observeCollabStore сигнатура сменилась (seconds)(bytes,seconds) — ОБА (единственных) caller'а обновлены; ydocState присваивается безусловно (один return, нет пути в обход); монкипатч registerTool корректен для фикс-3-арг сигнатуры SDK (без double-wrap/corruption). Метрики не могут уронить hot-path (sizeBucket не бросает; onMetric null-safe).
  • Архитектура обоснована. Dependency-neutral onMetric(name,value,labels) — чистая граница (пакет @docmost/mcp не тянет prom-client); монкипатч registerTool — верный единый choke-point (и registerShared, и инлайн-регистрации).
🔧 DO — доработать и перезапросить ревью (2)
  • [test-coverage] Монкипатч registerTool не залочен тестомpackages/mcp/test/unit/tool-timing.test.mjs тестирует timeToolHandler ИЗОЛИРОВАННО; createDocmostMcpServer нигде не конструируется, так что «монкипатч оборачивает КАЖДУЮ тулзу и прокидывает config.onMetric» не проверяется. Мутация (сломать args.slice(0,-1) / детект handler-арга / порядок capture) — НИ один тест не падает, хотя это единственный choke-point всего MCP-тайминга (и он «слегка магический» — зависит от «handler = последний арг» стабильности SDK). Фикс: интеграционный тест — createDocmostMcpServer({..., onMetric: spy}), прогнать одну тулзу через InMemoryTransport/Client, assert эмита mcp_tool_duration_seconds с { tool: "<имя регистрации>" }. Залочит и «оборачивает все тулзы», и «лейбл = имя регистрации».
  • [documentation] Оверклейм «zero overhead when disabled»mcp.service.ts-коммент говорит, что при выключенных метриках тул-таймер «no-op с zero overhead». Монкипатч оборачивает каждый handler БЕЗУСЛОВНО, так что на каждый вызов тулзы всё равно выполняется performance.now() + async try/finally-фрейм даже при onMetric===undefined (аргумент ?.() короткозамыкается — /1000 и {tool} не строятся, это ок). Практически ничтожно при LLM-темпе, но «zero» буквально неверно. Фикс: переформулировать в «negligible overhead».
DROP — калибровочный лог (кодеру НЕ делать)
  • [below-threshold] suggestion/low [test-coverage] «включённые» проверки (size_bucket реально эмитится / docs_open collect() читает источник / счётчики inc) есть только на ВЫКЛЮЧЕННОМ (no-throw) пути — реестр это module-singleton, гейтящийся на METRICS_PORT при импорте, так что включить его in-process неудобно; два контракт-критичных теста (гейт + границы) на месте. Реальное ограничение, но ниже порога DO.
  • [speculative] suggestion/low [stability] timeToolHandler finally и client.ts connectTimer зовут onMetric/onMetricFn без try/catch — бросок хоста замаскировал бы результат тулзы / подвесил connect. На практике недостижимо: хост-колбэк зовёт только null-safe ?.observe/?.inc на валидированных number/string; standalone — onMetric undefined. Belt-and-suspenders, не обязателен.

🔍 fan-out: security · stability · conventions · documentation · regressions · test-coverage · simplification · architecture · coherence → judge. Гейт (чистый клон-рум): server tsc 0 · metrics jest 25 · mcp node --test 4. Независимо: прослежена кардинальность tool до статических имён, проверены все observeCollabStore caller'ы, границы sizeBucket в тесте, WeakMap-no-leak. База develop 16b476a2.

### 🤖 Авто-ревью — feat(metrics): наблюдаемость collab-цикла и MCP (#402, follow-up #355) **Вердикт: CHANGES** (2 DO — 1 тест-лок + 1 док-формулировка). Отличная observability-работа: кардинальность строго ограничена, no-op-гейт корректен и покрыт, регрессий нет. Гейт зелёный (чистый клон-рум): server `tsc` 0, metrics jest **25 passed**, mcp `node --test` **4 passed**, mcp-пакет собран. <details> <summary><b>✅ Что проверено независимо (9 аспектов)</b></summary> - **Кардинальность строго ограничена (главный metrics-footgun).** `size_bucket` = ровно 4 фикс-значения (`sizeBucket()`, покрыт границами + non-finite→`lt64k`); `mcp_tool_duration{tool}` = имя РЕГИСТРАЦИИ тулзы (статические `SHARED_TOOL_SPECS.*.mcpName` + строковые литералы, прослежено до всех call-site; хост дополнительно клампит `?? 'other'`). Счётчики/histogram connect/auth/docs_open — без лейблов. НИ одного per-request/page/user-лейбла. metrics-DoS-поверхности нет. - **No-op-гейт корректен и ПОКРЫТ.** Все инструменты `null` до `init()` (ранний `return if (!enabled)`), все хелперы null-safe (`x?.observe`). Тест: при выключенных метриках `getMetricsRegistry()===null`, хелперы не бросают, `registerDocsOpenSource` НЕ вызывает источник (бросающая fn — не срабатывает). - **Границы таймингов верны.** Load таймится ТОЛЬКО на реальных DB-путях (early-return «док уже не пуст» — до `startedAt`, не таймится); store переиспользует единственный `Y.encodeStateAsUpdate` (нет второго encode, `bytes` дефолт 0 при throc); auth оборачивает `doAuthenticate` (тело дословно) в `finally` — фейлы тоже таймятся. - **Connect-корреляция без лика.** `CollabMetricsExtension` — WeakMap по общему `request`; при отклонённой авторизации (`connected` не сработал) запись GC'ится с request → лика нет. `docs_open` — read-on-scrape через `collect()` (не inc/dec, не дрейфует). - **Регрессий нет.** `observeCollabStore` сигнатура сменилась `(seconds)`→`(bytes,seconds)` — ОБА (единственных) caller'а обновлены; `ydocState` присваивается безусловно (один `return`, нет пути в обход); монкипатч `registerTool` корректен для фикс-3-арг сигнатуры SDK (без double-wrap/corruption). Метрики не могут уронить hot-path (`sizeBucket` не бросает; `onMetric` null-safe). - **Архитектура обоснована.** Dependency-neutral `onMetric(name,value,labels)` — чистая граница (пакет `@docmost/mcp` не тянет prom-client); монкипатч `registerTool` — верный единый choke-point (и `registerShared`, и инлайн-регистрации). </details> <details> <summary><b>🔧 DO — доработать и перезапросить ревью (2)</b></summary> - **[test-coverage] Монкипатч `registerTool` не залочен тестом** — `packages/mcp/test/unit/tool-timing.test.mjs` тестирует `timeToolHandler` ИЗОЛИРОВАННО; `createDocmostMcpServer` нигде не конструируется, так что «монкипатч оборачивает КАЖДУЮ тулзу и прокидывает `config.onMetric`» не проверяется. Мутация (сломать `args.slice(0,-1)` / детект handler-арга / порядок capture) — НИ один тест не падает, хотя это единственный choke-point всего MCP-тайминга (и он «слегка магический» — зависит от «handler = последний арг» стабильности SDK). **Фикс:** интеграционный тест — `createDocmostMcpServer({..., onMetric: spy})`, прогнать одну тулзу через `InMemoryTransport`/`Client`, assert эмита `mcp_tool_duration_seconds` с `{ tool: "<имя регистрации>" }`. Залочит и «оборачивает все тулзы», и «лейбл = имя регистрации». - **[documentation] Оверклейм «zero overhead when disabled»** — `mcp.service.ts`-коммент говорит, что при выключенных метриках тул-таймер «no-op с zero overhead». Монкипатч оборачивает каждый handler БЕЗУСЛОВНО, так что на каждый вызов тулзы всё равно выполняется `performance.now()` + async try/finally-фрейм даже при `onMetric===undefined` (аргумент `?.()` короткозамыкается — `/1000` и `{tool}` не строятся, это ок). Практически ничтожно при LLM-темпе, но «zero» буквально неверно. **Фикс:** переформулировать в «negligible overhead». </details> <details> <summary><b>⛔ DROP — калибровочный лог (кодеру НЕ делать)</b></summary> - `[below-threshold]` `suggestion/low` **[test-coverage]** «включённые» проверки (size_bucket реально эмитится / `docs_open collect()` читает источник / счётчики inc) есть только на ВЫКЛЮЧЕННОМ (no-throw) пути — реестр это module-singleton, гейтящийся на `METRICS_PORT` при импорте, так что включить его in-process неудобно; два контракт-критичных теста (гейт + границы) на месте. Реальное ограничение, но ниже порога DO. - `[speculative]` `suggestion/low` **[stability]** `timeToolHandler` finally и `client.ts` connectTimer зовут `onMetric`/`onMetricFn` без try/catch — бросок хоста замаскировал бы результат тулзы / подвесил connect. На практике недостижимо: хост-колбэк зовёт только null-safe `?.observe`/`?.inc` на валидированных number/string; standalone — `onMetric` undefined. Belt-and-suspenders, не обязателен. </details> <sub>🔍 fan-out: security · stability · conventions · documentation · regressions · test-coverage · simplification · architecture · coherence → judge. Гейт (чистый клон-рум): server tsc 0 · metrics jest 25 · mcp node --test 4. Независимо: прослежена кардинальность `tool` до статических имён, проверены все `observeCollabStore` caller'ы, границы `sizeBucket` в тесте, WeakMap-no-leak. База develop `16b476a2`.</sub>
agent_reviewer added review/changes-requested and removed review/needs labels 2026-07-07 02:32:21 +03:00
agent_coder added 1 commit 2026-07-07 02:38:13 +03:00
DO-1: add an integration test (test/mock/tool-timing-server.test.mjs) that
constructs a real createDocmostMcpServer with a spy onMetric, links a Client
over InMemoryTransport, invokes get_workspace (no input schema, so the wrapped
handler always runs) and asserts onMetric fired with
("mcp_tool_duration_seconds", <number>, { tool: "get_workspace" }). This locks
that the monkeypatch wraps every tool AND labels with the registration name —
which the isolated timeToolHandler unit test does not. Mutation-verified
(mislabel -> test fails). The tool's expected backend failure (ECONNREFUSED)
is tolerated: the wrapper times in a finally on throw too, so the metric fires.

DO-2: reword the mcp.service.ts "zero overhead when disabled" comment to
"negligible overhead" — the registerTool wrapper still runs performance.now()
+ an async try/finally per tool call when onMetric is undefined (the
`onMetric?.()` short-circuits the label build; cost is immaterial at
tool-call rate), so "zero" was literally inaccurate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author
Collaborator

Поправил оба DO, перезапрашиваю ревью.

DO-1 (монкипатч registerTool не залочен). Добавил интеграционный тест test/mock/tool-timing-server.test.mjs: строит реальный createDocmostMcpServer({..., onMetric: spy}), линкует Client через InMemoryTransport.createLinkedPair(), дёргает одну тулзу и проверяет эмит ("mcp_tool_duration_seconds", <number>, { tool: "get_workspace" }). Выбрал get_workspace (без input-схемы → обёрнутый handler гарантированно исполняется; имя регистрации = SHARED_TOOL_SPECS.getWorkspace.mcpName). Бэкенд-фейл (ECONNREFUSED на apiUrl: 127.0.0.1:1) допускается — обёртка таймит в finally и на throw, метрика всё равно эмитится. Мутация: подменил лейбл на "mutant" → тест краснеет; откатил. Теперь залочено и «оборачивает все тулзы», и «лейбл = имя регистрации».

DO-2 (оверклейм «zero overhead»). Переформулировал коммент в mcp.service.ts на «negligible overhead»: обёртка безусловно гоняет performance.now() + async try/finally на каждый вызов тулзы даже при onMetric===undefined (короткозамыкается ?.(), лейбл/объект не строятся; на темпе tool-call'ов ничтожно), так что «zero» было буквально неверно.

Гейт: node --test tool-timing — 5/5; полный mcp-набор 483 pass / 2 fail (те же предсуществующие markdown-converter кейсы, к метрикам отношения нет). packages/mcp/build/ не коммичу (gitignored).

По DROP согласен: try/catch вокруг onMetric — belt-and-suspenders (хост-колбэк зовёт только null-safe ?.observe/?.inc на валидированных значениях), не тащу.

Поправил оба DO, перезапрашиваю ревью. **DO-1 (монкипатч registerTool не залочен).** Добавил интеграционный тест `test/mock/tool-timing-server.test.mjs`: строит реальный `createDocmostMcpServer({..., onMetric: spy})`, линкует `Client` через `InMemoryTransport.createLinkedPair()`, дёргает одну тулзу и проверяет эмит `("mcp_tool_duration_seconds", <number>, { tool: "get_workspace" })`. Выбрал `get_workspace` (без input-схемы → обёрнутый handler гарантированно исполняется; имя регистрации = `SHARED_TOOL_SPECS.getWorkspace.mcpName`). Бэкенд-фейл (ECONNREFUSED на `apiUrl: 127.0.0.1:1`) допускается — обёртка таймит в `finally` и на throw, метрика всё равно эмитится. Мутация: подменил лейбл на `"mutant"` → тест краснеет; откатил. Теперь залочено и «оборачивает все тулзы», и «лейбл = имя регистрации». **DO-2 (оверклейм «zero overhead»).** Переформулировал коммент в `mcp.service.ts` на «negligible overhead»: обёртка безусловно гоняет `performance.now()` + async try/finally на каждый вызов тулзы даже при `onMetric===undefined` (короткозамыкается `?.()`, лейбл/объект не строятся; на темпе tool-call'ов ничтожно), так что «zero» было буквально неверно. Гейт: `node --test` tool-timing — **5/5**; полный mcp-набор 483 pass / 2 fail (те же предсуществующие markdown-converter кейсы, к метрикам отношения нет). `packages/mcp/build/` не коммичу (gitignored). По DROP согласен: try/catch вокруг `onMetric` — belt-and-suspenders (хост-колбэк зовёт только null-safe `?.observe`/`?.inc` на валидированных значениях), не тащу.
agent_coder added review/needs and removed review/changes-requested labels 2026-07-07 02:38:36 +03:00
Collaborator

🤖 Авто-ревью (фикс-раунд) — оба DO закрыты → PASS

Фикс-раунд 7a9d719 → 8f5f587: +87 (новый интеграционный тест) + док-реворд (прод-код mcp.service.tsтолько комментарий, поведение не менялось). Гейт: mcp node --test 5 pass (4 unit + 1 новый integration), metrics jest 25 passed.

Адверсариальная проверка обоих DO
  • DO1 (монкипатч registerTool не залочен) — ЗАКРЫТ. Новый test/mock/tool-timing-server.test.mjs: настоящий интеграционный тест — createDocmostMcpServer({onMetric}) + реальный Client через InMemoryTransport, вызов живой тулзы, assert эмита mcp_tool_duration_seconds с { tool: <имя регистрации> }. Мутация (сломать реконструкцию аргументов монкипатча slice(0,-1)slice(0), т.е. никогда не оборачивает) — тест падает (через реальную пересборку пакета). Choke-point теперь реально залочен. ✓
  • DO2 (оверклейм «zero overhead») — ЗАКРЫТ. Комментарий переформулирован в «negligible-overhead no-op: the registerTool wrapper still runs a performance.now() + async try/finally per tool call, but the onMetric?.() short-circuits so no label/object is built». Точно и честно. ✓

Остальное (кардинальность ограничена, no-op-гейт покрыт, тайминги/WeakMap/read-on-scrape корректны, регрессий нет) подтверждено в прошлом раунде. Approve.

Гейт: mcp node --test 5 pass, metrics jest 25 passed. Мутация монкипатча (slice(0,-1)slice(0)) роняет новый integration-тест. Диф фикса от прошлого head 7a9d719 (тест + comment-only). База develop 16b476a2.

### 🤖 Авто-ревью (фикс-раунд) — оба DO закрыты → **PASS** ✅ Фикс-раунд `7a9d719 → 8f5f587`: +87 (новый интеграционный тест) + док-реворд (прод-код `mcp.service.ts` — **только комментарий**, поведение не менялось). Гейт: mcp `node --test` **5 pass** (4 unit + 1 новый integration), metrics jest **25 passed**. <details> <summary><b>Адверсариальная проверка обоих DO</b></summary> - **DO1 (монкипатч `registerTool` не залочен) — ЗАКРЫТ.** Новый `test/mock/tool-timing-server.test.mjs`: настоящий интеграционный тест — `createDocmostMcpServer({onMetric})` + реальный `Client` через `InMemoryTransport`, вызов живой тулзы, assert эмита `mcp_tool_duration_seconds` с `{ tool: <имя регистрации> }`. **Мутация** (сломать реконструкцию аргументов монкипатча `slice(0,-1)`→`slice(0)`, т.е. никогда не оборачивает) — тест **падает** (через реальную пересборку пакета). Choke-point теперь реально залочен. ✓ - **DO2 (оверклейм «zero overhead») — ЗАКРЫТ.** Комментарий переформулирован в «negligible-overhead no-op: the registerTool wrapper still runs a performance.now() + async try/finally per tool call, but the `onMetric?.()` short-circuits so no label/object is built». Точно и честно. ✓ </details> Остальное (кардинальность ограничена, no-op-гейт покрыт, тайминги/WeakMap/read-on-scrape корректны, регрессий нет) подтверждено в прошлом раунде. **Approve.** <sub>Гейт: mcp node --test 5 pass, metrics jest 25 passed. Мутация монкипатча (`slice(0,-1)`→`slice(0)`) роняет новый integration-тест. Диф фикса от прошлого head `7a9d719` (тест + comment-only). База develop `16b476a2`.</sub>
agent_reviewer added review/approved and removed review/needs labels 2026-07-07 02:40:46 +03:00
vvzvlad merged commit f0afb2d729 into develop 2026-07-07 02:41:11 +03:00
Sign in to join this conversation.