Vue Network Dashboard: трансформация ответов, брейкпоинты и OpenAPI-импорт

20.04.2026
Vue Network Dashboard: трансформация ответов, брейкпоинты и OpenAPI-импорт

Vue Network Dashboard: десять новых фич

Продолжаю рассказывать про vue-network-dashboard — встраиваемый отладчик сети для Vue-приложений. В этом посте — всё, что накопилось за последние два коммита: от детектора N+1 до брейкпоинтов в стиле Charles Proxy.


Mock прямо из трафика

Раньше, чтобы замокать запрос, нужно было вручную открыть вкладку Mocks, угадать URL-паттерн и вписать тело ответа. Теперь в деталях любого запроса появилась кнопка Mock — она берёт URL, метод и тело ответа из захваченного лога и создаёт правило автоматически.

typescript Copy
function createMockFromLog(log: UnifiedLogEntry) {
  const group = mockGroups.value.find(g => g.name === 'default') ?? mockGroups.value[0]
  group.rules.push({
    pattern:  new URL(log.url).pathname,
    method:   log.method,
    status:   log.response.status,
    response: log.response.body,
    enabled:  true,
  })
  activeTab.value = 'mocks'
}

Кликнул — переключился на вкладку Mocks и увидел готовое правило. Можно сразу поправить статус или тело и включить.


Редактирование запроса перед Replay

Старый Replay просто повторял запрос один в один. Теперь перед отправкой открывается модальное окно:

Copy
┌─────────────────────────────────────┐
│ Edit & Replay                    ✕  │
├─────────────────────────────────────┤
│ [POST ▾] [https://api.example/gql]  │
│                                     │
│ Headers                             │
│ Authorization  Bearer eyJ...    ✕   │
│ Content-Type   application/json ✕   │
│ + Add header                        │
│                                     │
│ Body                                │
│ { "query": "{ user { id } }" }      │
│                                     │
├─────────────────────────────────────┤
│ [Cancel]                   [↺ Send] │
└─────────────────────────────────────┘

Можно поменять URL, метод, добавить или удалить заголовки, отредактировать JSON-тело — и если тело невалидно, подсветится предупреждение. Технически это отдельный компонент ReplayModal.vue, который эмитит событие replay с готовым объектом запроса.


Throttling сети

Четыре пресета прямо в тулбаре вкладки Logs:

Пресет Задержка
No throttle 0 мс
Fast 3G 400 мс
Slow 3G 2000 мс
Offline-ish 5000 мс

Хитрость реализации: интерцепторы инициализируются один раз при монтировании, и пересоздавать их ради смены задержки было бы дорого. Поэтому NetworkDashboard хранит значение в приватном поле, а наружу отдаёт геттер:

typescript Copy
getThrottleDelay = () => this.throttleDelay

Интерцептор вызывает геттер прямо перед каждым запросом — всегда получает актуальное значение без какой-либо перерегистрации.


Детекция N+1 запросов

N+1 — классика: компонент рендерит список из 20 элементов, и каждый делает отдельный запрос за деталями. В тулбаре лога теперь появляется бейдж ×N оранжевого цвета, если в окне 5 секунд встретилось больше одного запроса с таким же методом и URL.

typescript Copy
const duplicateCounts = computed(() => {
  const counts: Record<string, number> = {}
  const cutoff = Date.now() - 5000
  for (const log of filteredLogs.value) {
    if (log.startTime < cutoff) continue
    const key = `${log.method}::${log.url}`
    counts[key] = (counts[key] ?? 0) + 1
  }
  return counts
})

Никаких таймеров — чистый computed. Как только лог выходит за пределы окна, реактивность сама пересчитывает счётчики.


Определение GraphQL

Если POST-запрос содержит body.query: string, дашборд автоматически считает его GraphQL и показывает фиолетовый бейдж с именем операции:

Copy
[POST] /graphql  [query GetUser]  200  142ms

В деталях запроса появляется отдельная секция с типом операции, именем и распарсенными Variables — рядом с исходным телом запроса.

typescript Copy
const gqlInfo = computed(() => {
  if (props.log.method !== 'POST') return null
  const body = props.log.request.body
  if (!body || typeof body.query !== 'string') return null
  const opMatch = (body.query as string).trim()
    .match(/^(query|mutation|subscription)\s+(\w+)/)
  return {
    operationType: opMatch?.[1] ?? 'query',
    operationName: opMatch?.[2] ?? body.operationName ?? null,
    variables:     body.variables ?? null,
  }
})

Работает без каких-либо сторонних GraphQL-парсеров — только регулярка.


Условия в моках

Раньше мок матчился только по URL-паттерну и методу. Теперь можно добавить условия:

typescript Copy
interface MockRule {
  // ...
  conditions?: {
    queryParams?: Record<string, string>
    headers?:     Record<string, string>
    bodyFields?:  Record<string, unknown>
  }
}

Все условия — AND. Например, один и тот же POST /api/search можно замокать по-разному в зависимости от поля в теле:

typescript Copy
// Вернёт 200 с результатами
{ pattern: '/api/search', method: 'POST',
  conditions: { bodyFields: { type: 'user' } },
  response: { items: [...] } }

// Вернёт пустой список
{ pattern: '/api/search', method: 'POST',
  conditions: { bodyFields: { type: 'product' } },
  response: { items: [] } }

Реорганизация шапки

Шапка панели была перегружена: восемь иконок в одну строку. Теперь в header-right остались только глобальные действия — тема, автоскрытие, полноэкранный режим, закрытие окна. Всё, что зависит от текущей вкладки, переехало в отдельный тулбар под вкладками:

Copy
┌────────────────────────────────────────────────┐
│ Network Dashboard        🌙 📌 ⛶ ✕           │
├──────────┬──────────┬───────────┐              │
│  Logs    │  Mocks   │  Compare  │              │
├──────────┴──────────┴───────────┘              │
│ [Groups] [Diff] [↑] [↓] [Clear]  ← Logs        │
│ (только когда активна соответствующая вкладка) │
└────────────────────────────────────────────────┘

Тулбар появляется только на нужной вкладке — никаких серых неактивных кнопок.


Response Transform — модификация реального ответа

Моки полезны, но иногда нужно не заменить ответ целиком, а лишь подправить пару полей — например, добавить флаг isAdmin: true или убрать из тела чувствительное поле. Для этого в MockRule появился режим transform.

typescript Copy
interface MockRule {
  mode?: 'mock' | 'transform'  // по умолчанию 'mock'
  transform?: {
    status?: number                        // переопределить HTTP-статус
    headers?: Record<string, string>       // добавить / перезаписать заголовки
    bodyMerge?: Record<string, unknown>    // влить поля в JSON-тело
    bodyDelete?: string[]                  // удалить поля из JSON-тела
  }
}

В режиме transform запрос уходит на реальный сервер как обычно. После получения ответа интерцептор применяет трансформации и возвращает в приложение уже изменённый Response. Реальный ответ при этом виден в логах с пометкой mocked.

Пример — добавить поле и сменить статус:

typescript Copy
dashboard.addMock({
  urlPattern: '/api/me',
  method: 'GET',
  mode: 'transform',
  enabled: true,
  response: { status: 200 },  // обязательное поле, не используется в transform
  transform: {
    bodyMerge:  { isAdmin: true, beta: true },
    bodyDelete: ['internalId'],
  }
})

В UI режим переключается прямо в форме мока — кнопки Mock / Transform, и набор полей меняется в зависимости от выбора.


Breakpoints — заморозка запросов

Наверное, самая интересная фича с точки зрения реализации. Брейкпоинты работают как в Charles Proxy: запрос перехватывается до отправки, показывается в панели со всеми деталями, разработчик редактирует что нужно — и нажимает Release или Cancel.

Технически это Promise-канал между интерцептором и UI. Когда запрос совпадает с правилом, интерцептор создаёт промис и ждёт его резолва:

typescript Copy
// В NetworkDashboard:
public checkBreakpoint = (url, method, body, headers) => {
  const rule = this.breakpointRules.find(r => matches(r, url, method))
  if (!rule) return null

  return new Promise<BreakpointEdits | null>(resolve => {
    this.pendingBreakpoints.set(id, { data, resolve })
    this.notifyActiveBreakpoints()  // → обновляет реактивный ref → рендерит карточку в UI
  })
}

// В fetchInterceptor — запрос ждёт здесь:
const edits = await pauseRequest(url, method, body, headers)
if (edits === null) throw new DOMException('Cancelled', 'AbortError')
// иначе — пересобираем аргументы с отредактированными данными и делаем реальный fetch

В панели Breakpoints каждый паузированный запрос показывается как карточка с редактируемыми полями: URL, метод, заголовки (построчно), тело. Release отправляет запрос с изменёнными данными, Cancel бросает AbortError.

Правила добавляются через форму — паттерн URL + метод + опциональное имя. Бейдж с числом паузированных запросов появляется прямо на табе.

typescript Copy
// Подключение через плагин:
const dashboard = useNetworkDashboard()

dashboard.addBreakpointRule({
  urlPattern: '/api/checkout',
  method: 'POST',
  enabled: true,
  name: 'Pause checkout'
})

// Управление из кода:
dashboard.releaseBreakpoint(id, { url, method, headers, body })
dashboard.cancelBreakpoint(id)

OpenAPI / Swagger import

Если у проекта есть спека — моки можно не создавать вручную. Кнопка OpenAPI в тулбаре вкладки Mocks принимает .json-файл (OpenAPI 3.x или Swagger 2.x) и генерирует правила для каждого path + method.

Парсер написан без зависимостей — около 120 строк TypeScript. Из схемы ответа он строит пример тела по типам полей:

typescript Copy
function schemaToExample(schema, doc, depth = 0) {
  if (schema.$ref) schema = resolveRef(schema.$ref, doc)
  if (schema.example !== undefined) return schema.example

  if (schema.type === 'object') {
    return Object.fromEntries(
      Object.entries(schema.properties ?? {})
        .map(([k, v]) => [k, schemaToExample(v, doc, depth + 1)])
    )
  }
  if (schema.type === 'array') return [schemaToExample(schema.items, doc, depth + 1)]
  if (schema.type === 'string') return schema.enum?.[0] ?? schema.format ?? 'string'
  if (schema.type === 'integer') return 0
  if (schema.type === 'boolean') return false
  // ...
}

Все сгенерированные моки попадают в новую группу с именем из info.title спеки. Оттуда их можно включать, редактировать и экспортировать как обычно.

Пример: загрузить спеку Petstore — получить 18 моков за один клик.


Группировка логов по маршруту Vue Router

Когда дашборд работает вместе с Vue Router, каждый лог-запись получает поле route — текущий $route.path в момент отправки запроса. Это позволяет фильтровать логи по странице и понимать, какой компонент что отправлял.

Подключается при инициализации плагина:

typescript Copy
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({ history: createWebHistory(), routes })

app.use(NetworkDashboardPlugin, {
  router,          // передаём инстанс роутера
  enrichWithRoute: true,
})

После этого в каждом логе появляется колонка маршрута, а в фильтрах — поле Route для поиска по пути. Никакой жёсткой зависимости от vue-router нет — плагин принимает любой объект с currentRoute.value и afterEach.


NPM: https://www.npmjs.com/package/vue-network-dashboard
GitHub: https://github.com/macrulezru/vue-network-dashboard

Читать далее