# vue-network-dashboard — AI Reference Vue 3 in-app network debugger — a DevTools-Network-tab-style floating panel plus a programmatic API: intercepts `fetch`/`XMLHttpRequest`/ `WebSocket`/`EventSource` (SSE), normalizes every event into one `UnifiedLogEntry` shape, sanitizes secrets, and exposes reactive logs, stats, request mocking (incl. OpenAPI import), request breakpoints, throttling, HAR/CSV/JSON export, and optional Sentry/OpenTelemetry/Vue DevTools integrations. This is NOT a graph/topology visualizer — no SVG/Canvas/WebGL anywhere; the panel and its waterfall timeline are plain CSS-positioned DOM. Single peer dependency: `vue ^3.0.0` (real, not optional — `@vue/devtools-api` is the only truly optional integration, loaded dynamically). Version 0.4.0+ (this document reflects source with 5 real bugs fixed via PR #31 — 2 of them critical — if you're reading an installed `dist/` older than that PR, expect the specific behaviors called out as "fixed"/"as of this fix" below to instead match their "before" state, described alongside each one). This document is hand-written for AI agents and other tools that generate code against this package: every signature, default, and behavior note below is verified directly against the TypeScript source (not summarized from prose docs). For human-readable narrative docs, see the interactive site instead: - Full docs (EN): https://npm.vuecraft.ru/en/packages/vue-network-dashboard/guide/overview - Full docs (RU): https://npm.vuecraft.ru/packages/vue-network-dashboard/guide/overview - GitHub: https://github.com/macrulezru/vue-network-dashboard - npm: https://www.npmjs.com/package/vue-network-dashboard Links below starting with "/" are relative to https://npm.vuecraft.ru. --- ## 1. Package map | Import path | Contains | |---|---| | `vue-network-dashboard` | Everything runtime: `NetworkDashboard` class, formatters, `LogStore`, interceptors, sanitizer/size utils, the Vue plugin + composable, `NetworkDebugger` component, Sentry/OpenTelemetry adapters, `setupDevtools`. | | `vue-network-dashboard/nuxt` | Nuxt 3 module (`defineNuxtModule`) — auto-registers the plugin (client-only) + `useNetworkDashboard` + ``. | | `vue-network-dashboard/dist/vue-network-dashboard.css` | Panel styles — required import for the floating UI (not needed if you only use the programmatic API without ``). | **No `/devtools`, `/adapters/sentry`, or `/adapters/opentelemetry` subpaths exist**, despite `src/devtools.ts`/`src/adapters/sentry.ts`/ `src/adapters/opentelemetry.ts` each having an `@example` JSDoc comment showing exactly that kind of import. `setupDevtools`/ `createSentryAdapter`/`createOpenTelemetryAdapter` are real and usable — only reachable from the package root, same as everything else. Trust this document (and the README, which gets this right) over those three source comments. --- ## 2. Core class: `NetworkDashboard` (`core/NetworkDashboard.ts`) ```ts new NetworkDashboard(options?: NetworkDashboardOptions) interface NetworkDashboardOptions { enabled?: boolean // default true maxLogs?: number // default 1000 (FIFO, oldest dropped) router?: RouterInstance // minimal vue-router v4 shape — no hard dependency enrichWithRoute?: boolean // attach current route path to every log entry; requires `router` interceptors?: { fetch?; xhr?; websocket?; sse? } // each defaults true filters?: { urlPattern?: RegExp; excludeUrlPattern?: RegExp methods?: string[]; statusCodes?: number[] bodySizeThreshold?: number // entries below this combined req+res size are dropped entirely } sanitization?: { sensitiveHeaders?; sensitiveFields?; maskFields?: string[] } // ADDED to (not replacing) the built-in default lists — section 8 metrics?: { calculateTTFB?: boolean; trackRetries?: boolean } // BOTH default true here (NetworkDashboard's own constructor) — note this differs from LogFormatter/HTTPFormatter's own bare default of false when used standalone (section 3) callbacks?: { onLog?: (entry) => void; onError?: (error) => void; onFlush?: (logs) => void } devOnly?: boolean // default false persistToStorage?: boolean // default false — logs AND mock groups both persist to localStorage when true ui?: { hotkey?: string; hotkeyModifiers?: {ctrl?;alt?;shift?;meta?}; theme?: 'dark'|'light'|'auto' } // theme default 'dark' } ``` Public instance surface (selected — see `core/types.ts` for full return types): `enable()`/`disable()`/`getEnabled()`, `getLogs*` family (`getLogsRef`, `getLogsByType`, `getLogsByUrl`, `getLogsByStatus`, `getLogsByMethod`, `getErrorLogs`), `queryLogs()`, `getStats()`/`getStatsSummary()`, `export(format, customLogs?)`, `subscribe(cb)`, mock-groups CRUD (`addMockGroup`/`toggleMockGroup`/ `addMockToGroup`/…) + a legacy flat mock API (`addMock`/`updateMock`/ `removeMock`/`clearMocks`/`getMocks`, all delegating to a `'default'` group created automatically), breakpoints (`addBreakpointRule`/ `checkBreakpoint`/`releaseBreakpoint`/`cancelBreakpoint`), `setThrottle`/`getThrottle`, `updateOptions`, `destroy`. **`shouldAutoEnable()`/constructor auto-`enable()` has no `typeof window` guard.** If auto-enable conditions are met (`enabled: true` and either `devOnly: false` or actually running in dev), the constructor calls `enable()` synchronously, which constructs `FetchInterceptor` — whose constructor does `window.fetch.bind(window)` unconditionally. **This throws under Node/SSR** unless the caller passes `enabled: false` or the integration layer avoids constructing it server-side. The Nuxt module (section 9) sidesteps this correctly via `mode: 'client'` on both the plugin and component — a hand-rolled Vue SSR setup outside Nuxt must guard this itself. --- ## 3. Log formatting (`core/formatters.ts`) ```ts class LogFormatter { public http: HTTPFormatter public websocket: WebSocketFormatter public sse: SSEFormatter constructor(options?: FormatOptions) updateOptions(options: FormatOptions): void // rebuilds all three sub-formatters } interface FormatOptions { sanitizeHeaders?: (headers) => headers sanitizeBody?: (body) => body calculateTTFB?: boolean // default false when FormatOptions is used directly (NOT via NetworkDashboard, which defaults both metrics to true — see section 2) trackRetries?: boolean // default false, same caveat } class HTTPFormatter { formatRequest(params: FormatHTTPParams): UnifiedLogEntry // clientType: 'fetch'|'xhr' formatResponse(entry, params): UnifiedLogEntry // merges status/headers/body/ttfbTime into the request entry formatError(params, error, endTime): UnifiedLogEntry } class WebSocketFormatter { /* one method per WS event type — connection/open/message/error/close */ } class SSEFormatter { /* same shape for SSE readystate/message/error events */ } ``` - **`trackRetries`**: `HTTPFormatter` keeps a `Map<"METHOD url", number>` incremented on HTTP ≥400 responses (`formatResponse`) and on network errors (`formatError`), cleared on success — surfaced as `entry.metadata.retryCount`. - **`calculateTTFB`**: `formatResponse` accepts an optional `ttfbTime` and computes `ttfb = ttfbTime - startTime`, stored in `entry.metadata.ttfb`. `fetchInterceptor.ts` sets `ttfbTime = Date.now()` right after `await fetch(...)` resolves (headers-received timing, before reading the body); `xhrInterceptor.ts` tracks it from the `readystatechange` → `HEADERS_RECEIVED` event. Mock/transform response paths correctly omit `ttfbTime` (`ttfb: null` for mocked entries — no fake network timing). --- ## 4. `UnifiedLogEntry` — the shape every interceptor normalizes into ```ts interface UnifiedLogEntry { id: string; type: 'http' | 'websocket' | 'sse' startTime: number; endTime: number | null; duration: number | null url: string; method: string http: { status: number|null; statusText: string|null; protocol: string|null } | null websocket: { readyState; eventType: 'connection'|'open'|'message'|'error'|'close'; direction: 'incoming'|'outgoing'|null; code; reason; wasClean } | null sse: { readyState: number; eventType: string|null; lastEventId: string|null } | null requestHeaders: Record; responseHeaders: Record request: { body; bodyRaw: string|null; bodySize: number|null; bodyType: string|null } response: { body; bodyRaw: string|null; bodySize: number|null; bodyType: string|null } error: { occurred: boolean; message; name; stack: string|null } metadata: { clientType: 'fetch'|'xhr'|'websocket'|'eventsource' redirected: boolean; retryCount: number; timestamp: string pending?: boolean // true only during the request lifecycle — see section 5 mocked?: boolean connectionId?: string // WebSocket: shared across all events of one connection ttfb?: number | null } route?: string // present only when enrichWithRoute + router are both set } ``` --- ## 5. The pending → complete lifecycle, and `callbacks.onLog` — READ THIS BEFORE WIRING A CUSTOM LOGGER For a real (non-mocked) fetch/XHR request, the interceptor calls `onLog(entry)` **once immediately** with `metadata.pending: true` and `http.status`/`duration`/`error` all still `null` (the request hasn't resolved yet) — this is what makes the reactive panel show a request "in flight." When the request actually resolves, the interceptor calls `onUpdateLog(id, completedFields)` instead of a second `onLog` — this updates the reactive store in place (`LogStore.updateLog`, a shallow `Object.assign` onto the existing entry object). **As of this fix**, `NetworkDashboard`'s internal `handleUpdateLog` (wired to `onUpdateLog`) *also* re-invokes `options.callbacks.onLog` with the now-complete, merged entry — so a `callbacks.onLog` consumer sees exactly two calls per real HTTP request: once pending, once complete (`metadata.pending: false`, real `http.status`/`duration`/ `error`). **Before this fix**, `handleUpdateLog` only updated the store and never touched `callbacks.onLog` at all — any custom `onLog` callback (and both bundled adapters, see section 10) only ever received the pending snapshot, with status/duration/error permanently `null`, for every real fetch/XHR request. The reactive UI panel was never affected by this (it reads the store directly, not the callback) — only `callbacks.onLog` consumers were blind to completion. **If you supply `callbacks.onLog` yourself, check `entry.metadata.pending` to distinguish the two calls** — most consumers (logging, analytics, observability) only want the completed one: ```ts callbacks: { onLog(entry) { if (entry.metadata.pending) return // skip — wait for the real outcome // entry.http.status / entry.duration / entry.error are now populated } } ``` **Exceptions to this pending/complete split**: - **Mocked responses** (`handleMock`/`handleTransform` in `fetchInterceptor.ts`) skip the pending phase entirely — a single, already-complete `onLog` call delivers the mocked entry directly (`metadata.mocked: true`), no `onUpdateLog` involved. - **WebSocket and SSE events** have no pending/complete split at all — every connection/open/message/error/close event is its own self-contained, already-complete `onLog` call. `entry.metadata.pending` is simply never set (`undefined`) for these two types. - **Request breakpoints**: if the user cancels a paused request, the cancellation is delivered via `onUpdateLog` too (an `AbortError` entry) — same completion path as a normal response. --- ## 6. Mock rules (`getMockForRequest`, `checkBreakpoint`, `openApiParser.ts`) ```ts interface MockRule { id: string; name?: string; enabled: boolean urlPattern: string | RegExp method?: string // omit → matches any method mode?: 'mock' | 'transform' // default 'mock' conditions?: { queryParams?; headers?; bodyFields?: Record } // ALL must pass response: { status; statusText?; headers?; body?; delay? } // used when mode:'mock' transform?: { status?; headers?; bodyMerge?; bodyDelete?: string[] } // used when mode:'transform', applied to the REAL response } ``` **`urlPattern: string` matching, as of this fix**: a shared `stringUrlPatternToRegExp()` helper treats `{param}` segments (e.g. `/users/{id}`) as a `[^/]+` wildcard; every other character is escaped and matched literally. **Before this fix**, the whole string — `{`/`}` included — was escaped and matched literally, so a pattern like `/users/{id}` required the URL to literally contain the text `{id}`, which no real request URL ever has. This made every OpenAPI-imported mock rule for a parameterized path silently unmatchable (`parseOpenApi()` emits `urlPattern: path` verbatim from the spec's path templates, e.g. `/users/{id}`). **`urlPattern` stays a plain `string`, not a `RegExp` object** — deliberately, because mock groups persist to `localStorage` via `JSON.stringify()` (`saveMockGroups`/`DEFAULT_MOCK_GROUPS_KEY`), and a `RegExp` value silently serializes to `{}`, which would corrupt the rule on the very next reload. **`getMockForRequest`/`checkBreakpoint` fully escape a string pattern plus wildcard `{param}` segments** — but `LogStore.getLogsByUrl()`/ `queryLogs({url})` treat a string `url` filter as a **raw, unescaped regex source** (`new RegExp(urlPattern, 'i')`, no escaping at all). These are two different, intentionally-different matching semantics for "a string that looks like it could be a pattern" — don't assume the same string behaves identically in a mock rule vs. a log-search filter. `parseOpenApi(raw): { title: string; rules: Omit[] }` — parses OpenAPI 3.x or Swagger 2.x, synthesizes example response bodies from schemas (handles `$ref`, `enum`, `format` like `date`/`email`/ `uuid`, `allOf`/`oneOf`/`anyOf` composition by taking the first alternative, recursion capped at depth 5), picks the best 2xx status code per operation (falls back to the first status present). --- ## 7. `LogStore` (`store/logStore.ts`) — plain `ref<>`, no Pinia/Vuex `addLog()` uses `unshift()`+`pop()` (mutation, not full-array reallocation) to maintain `maxLogs` FIFO — newest first. `updateLog(id, updates)` is a shallow `Object.assign` onto the existing entry object found by `.find()` — silently a no-op if `id` isn't present (e.g. the entry was filtered out at the pending stage and never added — see `filters.statusCodes`/`bodySizeThreshold`, which are checked BEFORE `store.addLog`, so a filtered-out pending entry's later completion update is a harmless no-op). `export('har')`: hardcodes `creator: { name: 'vue-network-dashboard', version: '0.1.0' }` in the HAR output regardless of the actual installed package version — cosmetic, but don't trust a HAR file's own declared creator version for anything. `getStats()`: `averageDuration` is `0`-guarded for an empty log list; `slowestRequests`/`largestRequests` are each capped to the top 10. --- ## 8. Sanitization (`utils/sanitizer.ts`) Default lists (all substring-matched, case-insensitive, via `.toLowerCase().includes()` — NOT exact key match): 11 sensitive headers (`authorization`, `cookie`, `x-api-key`, …), 15 sensitive fields removed entirely (`password`, `token`, `secret`, `creditCard`, `ssn`, …), 9 mask-partially fields (`email`, `phone`, `address`, `firstName`, …). **User-supplied `sensitiveHeaders`/`sensitiveFields`/ `maskFields` are ADDED to these defaults, never replace them** (`getSanitizationRules`: `[...DEFAULT, ...custom]`) — there is no way to opt out of redacting `authorization`/`cookie` short of not using sanitization filtering at that layer at all. Pipeline order for bodies: `sanitizeBody()` first calls `removeSensitiveFields()` (→ `'[REMOVED]'`) THEN `maskSensitiveData()` on what's left (→ partially masked or `'[MASKED]'` for non-strings) — a field name matching both a `sensitiveFields` AND a `maskFields` entry is removed, never reaches the masking pass. Because substring matching is used for BOTH, a field like `emailAddress` matches the default `address` mask-field even though it's semantically an email — masked using whichever content-detection branch (`@` → email style, digit-ish → phone style, else → default) `maskString()` picks, not necessarily the "email" style you'd expect from the field name. --- ## 9. Nuxt 3 module (`vue-network-dashboard/nuxt`) ```ts // nuxt.config.ts export default defineNuxtConfig({ modules: ['vue-network-dashboard/nuxt'], networkDashboard: { devOnly: true, maxLogs: 500, ui: {...} } // configKey: 'networkDashboard'; module defaults: enabled:true, devOnly:true, maxLogs:500 }) ``` - `if (options.devOnly && !nuxt.options.dev) return` — the module-level gate bails entirely in production builds when `devOnly` (module default `true`), so nothing (plugin, CSS, composable) is registered at all in a prod build unless you explicitly set `devOnly: false`. - Options flow to the client exclusively via `nuxt.options.runtimeConfig.public.networkDashboard` — **forced to `devOnly: false`** in that copy specifically so the client-side `NetworkDashboard` constructor doesn't apply its own `import.meta.env.DEV` check a second time (which would always be `false` in a pre-built library anyway — the module's own dev/prod gate above is the real check). - Plugin + `` component are both registered `mode: 'client'` only — SSR-safe by construction, never constructed server-side (see section 2's SSR caveat, which doesn't apply through this module). - **`router`/`enrichWithRoute` aren't wired through this module** — `runtimeConfig` values are meant for serializable data and get JSON-transferred to the client during SSR hydration; a live Vue Router instance can't survive that. If you need route-enriched logs in a Nuxt app, install `NetworkDashboardPlugin` yourself in a custom plugin instead of relying on the auto-registered one. - CSS is auto-injected (`nuxt.options.css.push(...)`) — no manual import needed, unlike the plain-Vue setup (section 1). --- ## 10. Sentry / OpenTelemetry adapters (`adapters/sentry.ts`, `adapters/opentelemetry.ts`) ```ts createSentryAdapter(sentry: SentryLike, options?: { filter?: (entry) => boolean errorStatusThreshold?: number // default 500 — triggers sentry.captureMessage(), not just a breadcrumb includeBodies?: boolean // default false }): Pick createOpenTelemetryAdapter(tracer: OTelTracerLike, options?: { httpOnly?: boolean // default true — skips websocket/sse entries includeBodySize?: boolean // default true }): Pick ``` Both are thin `callbacks.onLog` factories, wired in via `app.use(NetworkDashboardPlugin, { callbacks: createSentryAdapter(Sentry) })`. **As of this fix, both skip the pending call** (`if (entry.metadata?.pending) return`) so each real HTTP request produces exactly one breadcrumb/span — built from the real outcome, once section 5's fix made completion visible to `onLog` at all. **Before this fix**, since `onLog` only ever received the pending snapshot for real fetch/XHR requests (section 5's original bug), both adapters were non-functional for their entire stated purpose: Sentry never saw a real status code (so its 5xx-triggers-`captureMessage` logic never fired based on status, only on `entry.error.occurred`, which was also always `false` at pending time) and OTel spans always reported `status: OK` with near-zero duration (ended immediately at the pending call, using `Date.now()` since `entry.endTime` was still `null`). `createSentryAdapter`'s breadcrumb `level`: `'error'` if `entry.error.occurred` or status ≥ `errorStatusThreshold`, `'warning'` if status ≥ 400, else `'info'`. --- ## 11. Vue plugin & composable (`plugins/vuePlugin.ts`) ```ts app.use(NetworkDashboardPlugin, options?: NetworkDashboardOptions) // provides 'networkDashboard' (the reactive instance) and 'networkDashboardUi' (options.ui ?? {}) // also sets app.config.globalProperties.$networkDashboard, and wraps app.unmount to call logger.destroy() first useNetworkDashboard(): VueNetworkDashboardInstance // inject-based; throws if the plugin isn't installed createNetworkDashboard(options?): VueNetworkDashboardInstance // standalone — no Vue plugin/app.use() needed ``` `VueNetworkDashboardInstance` wraps `NetworkDashboard` with Vue reactivity: `logs`/`totalRequests`/`totalErrors`/`averageDuration`/ `totalDataSent`/`totalDataReceived` (`Ref`s, the last 5 derived via `computed(() => logger.getStats())`), `mockGroups`/`mocks`/ `breakpointRules`/`activeBreakpoints` (readonly `Ref`s kept in sync via the logger's own change-subscription callbacks), plus every method from `NetworkDashboard`'s public surface (mock CRUD, breakpoint CRUD, throttle, export, query, subscribe) as plain functions. The raw `NetworkDashboard` instance is also exposed as `._logger` for anything not wrapped. `useNetworkDashboard()` and `createNetworkDashboard()` are two independent ways to get an instance — the former requires `app.use(NetworkDashboardPlugin)` first (inject-based, throws otherwise), the latter constructs a fresh, unconnected `NetworkDashboard` + reactive wrapper on its own, useful outside a Vue app entirely (a plain script, a test, a non-Vue context). --- ## 12. `NetworkDebugger` component & `setupDevtools` `NetworkDebugger.vue` is the floating panel — draggable/resizable/ fullscreen-able, tabs for logs/stats/mocks/breakpoints/timeline, filter bar (method/status/URL regex/route, debounced), HAR import, N+1 duplicate-request detection (5-second sliding window), Copy-as-cURL, request diff (a real O(m·n) LCS line-diff over headers+body, not a naive comparison), request replay (edit + resend). Exported prop type: `NetworkDebuggerProps`. `setupDevtools(app, instance): Promise` — dynamically imports `@vue/devtools-api` (via an indirect `new Function('m', 'return import(m)')` call specifically so bundlers don't statically pull it in as a hard dependency); silently no-ops if the import fails (package not installed) or `typeof window === 'undefined'`. Adds a custom "Network" inspector tab (tree view, up to 200 entries, filterable by URL substring) and a timeline layer (one event per completed HTTP request, skips `pending`/non-http entries) to the Vue DevTools panel. --- ## 13. Fixed-bug history (verify against your installed version) All five fixed via PR #31 (branch `fix/broken-types-and-openapi-mock-regex`), verified by toggling `git stash` between pre-fix and post-fix source and confirming each behavior flips; full suite 196/196 passing: 1. **Published TypeScript types were completely broken** — `vite-plugin-dts`'s `rollupTypes: true` used a bundled API Extractor engine (TS 5.4.2) older than the project's installed TypeScript (5.9.3); a clean build silently produced no `dist/index.d.ts` at all, leaving `dist/types/index.d.ts` (what `package.json`'s `types`/`exports` point every consumer at) as a dangling re-export of a nonexistent file. Fixed by dropping `rollupTypes: true` — section (build config, not covered elsewhere in this document since it's not part of the runtime API surface). 2. **`callbacks.onLog` never received a completed fetch/XHR entry** — section 5. 3. **OpenAPI-imported mocks unmatchable for any parameterized path** — section 6. 4. **`parseHeaders()` dropped headers whose value contains `": "`** — internal helper (`utils/helpers.ts`), used by `xhrInterceptor.ts` to parse `xhr.getAllResponseHeaders()`; not part of the public API surface but affects real XHR response header capture. 5. **`SSEInterceptor`/`SSEFormatter` weren't exported from the package root** — sections 1, 3 (now fixed, both present in `dist/index.d.ts`). --- ## 14. Consolidated gotcha list 1. `callbacks.onLog` fires twice per real HTTP request (pending, then complete) — check `entry.metadata.pending` if you only want the final outcome (section 5). 2. Mocked responses and WebSocket/SSE events have NO pending phase — exactly one, already-complete `onLog` call each (section 5). 3. `urlPattern: string` supports OpenAPI-style `{param}` wildcards in mock/breakpoint rules, but is matched completely differently (raw unescaped regex source, no wildcard support) by `LogStore.getLogsByUrl()`/`queryLogs({url})` — don't assume parity between the two (section 6). 4. Sanitization's default sensitive/mask lists cannot be disabled — custom lists only ADD to them (section 8). 5. Sanitization matching is substring-based and case-insensitive on field/header NAMES — `emailAddress` matches the default `address` mask field, not necessarily formatted as an email (section 8). 6. The Nuxt module doesn't wire `router`/`enrichWithRoute` — install the plugin yourself in a custom Nuxt plugin if you need route enrichment (section 9). 7. `NetworkDashboard`'s constructor auto-`enable()`s with no `typeof window` guard — safe through the Nuxt module (client-only registration) but a hand-rolled non-Nuxt SSR setup must guard it itself (section 2). 8. `FormatOptions.calculateTTFB`/`trackRetries` default to `false` when `LogFormatter`/`HTTPFormatter` are used standalone (advanced usage, both exported), vs. `true`/`true` when going through `NetworkDashboard`'s own constructor defaults (section 3) — don't assume the same default in both contexts. 9. `LogStore.export('har')` hardcodes the HAR creator version as `'0.1.0'` regardless of the actual installed package version — cosmetic only (section 7). 10. `src/view/composables/useHotkey.ts` and `useLogFilter.ts` are exported but effectively dead code — `NetworkDebugger.vue` reimplements both hotkey handling and (a more-featured) filtering inline rather than using either composable. 11. This package's own `npm run type-check` script currently fails (`NetworkDebuggerProps` not recognized as a named export of a `.vue` module by the plain `*.vue` shim) — does not affect the actual published `.d.ts` output (`vite-plugin-dts`/vue-tsc resolve it correctly; confirmed present in `dist/view/index.d.ts`), only the package's own dev/CI typecheck script gives a false failure.