Overview

A local-first inspector for Vercube applications - dependency graph, routes, request waterfalls, bootstrap profiler and audits

Devtools serve an inspector from your running application: the DI container, the route table, request waterfalls, logs, storage, resolved config, a bootstrap profile and an audit. It is one self-contained HTML page served by the app itself, with no outbound traffic and no separate process.

Installation

$ pnpm add -D @vercube/devtools

Register the plugin in vercube.config.ts and open http://localhost:3000/_devtools.

vercube.config.ts
import { defineConfig } from '@vercube/core';
import { DevtoolsPlugin } from '@vercube/devtools';

export default defineConfig({
  plugins: [DevtoolsPlugin],
});
app.addPlugin(DevtoolsPlugin) inside createApp({ setup }) also works, but attaches after the container is built, so the bootstrap profile will be incomplete.

Panels are in the left rail, with number shortcuts 1 to 9. The active panel is written to the URL hash, so /_devtools#requests is shareable. The buttons at the bottom pause the live stream, download a JSON snapshot of everything, and switch the theme.

Overview

Answers "what am I connected to and is it healthy": name, version, mode, runtime, uptime, counts of routes, services and plugins, live throughput, CPU, heap, event loop delay and active handles. Every summary cell opens the panel behind it.

Handy when several apps run on neighbouring ports and you are about to debug the wrong one.

Requests

Answers "where did the time go". Every request is recorded with the status the client observed, and opening one gives a waterfall: a span per middleware, one for the handler, and the framework time left over for routing, argument resolution and serialization. The log lines written during that request, the bodies and the headers are below it.

Bodies are read from a clone of the message, so recording never touches the stream your app consumes. Headers such as authorization and cookie are redacted, but bodies are not, so on an app that posts secrets:

vercube.config.ts
plugins: [withPluginOptions(DevtoolsPlugin, { captureBodies: false })],

Logs

Everything written through the Logger service, filterable by level and stamped with the request that produced it. The search matches structured context, not just the message, so given:

this.logger.info('order created', { orderId, userId });

typing an order id finds that line, and the request id next to it takes you to the call that wrote it. A bare console.log bypasses the Logger service and never shows up here.

Storage

Keys held by every @vercube/storage mount, with the value behind a key on click, plus the defaults @vercube/cache runs with. Read-only: it lists and previews, it never writes.

The usual use is confirming that a @Cache({ maxAge: 300 }) entry is really in the store, under the key you expect, with the value you expect. Values under credential-looking keys are never read back.

Routes

The route table as the router sees it, grouped by controller. Opening a route shows its decorated arguments, whether each one is validated, and the middleware chain in execution order with phases and priorities, which is the fastest answer to "why is this middleware running here" and "why does this path 404".

An argument marked unvalidated means the payload reaches the handler unchecked:

@Post('/orders')
public create(@Body({ validationSchema: OrderSchema }) body: Order) {}

Graph

The container drawn as a graph: nodes by role, arrows from a dependent to its dependency, hollow nodes for bindings nothing has resolved, rings around dependency cycles. Selecting a service lists what it injects and what injects it, both clickable, which is the blast radius to check before changing a constructor.

An @Inject of a key nothing binds is marked unbound here before it throws at runtime. Inspecting never instantiates anything.

Config

The configuration the framework actually resolved, after defaults, files and environment were merged, flattened to dotted paths such as server.port. Search matches paths and values, so a port number finds whichever setting holds it.

Values whose key reads like a credential are replaced with <redacted>. The rule is name-based, so a secret inside a connection string is still shown.

Bootstrap

A flamegraph of container construction: frame width is total time, nesting is injection depth. Below it, services ranked by self time, which excludes the time spent building their dependencies and is therefore the list worth acting on.

The typical finding is a constructor doing I/O at boot. Profiling starts in the plugin's configure phase and stops on the first request, so the profile always describes startup.

Audit

Rules over the container, the router, the bootstrap profile and recorded traffic, scored 0-100:

RuleSeverityWhat it catches
di/unbound-dependencyerror@Inject of a key nothing binds, so resolution will throw
router/duplicate-routeerrorThe same method and path registered twice
runtime/server-errorserrorRecorded requests that returned 5xx
di/circular-dependencywarningDependency loops in the container
validation/missing-schemawarning@Body() or @QueryParams() reaching a handler unvalidated
bootstrap/slow-servicewarningConstructors that dominate startup
runtime/slow-requestswarningRequests over 500ms
di/unbound-optional-dependencyinfo@InjectOptional that will always resolve to null
di/unused-serviceinfoBindings nothing has ever resolved

Runtime rules only see what devtools recorded, so drive some traffic before trusting them.

Snapshot and JSON

Snapshot downloads one JSON file with the overview, graph, routes, bootstrap profile, audit, config, requests and logs: the exact state the UI is showing, and the right thing to attach to a bug report.

Everything the UI reads is plain JSON under the mount, so the same data works outside the browser:

curl -s localhost:3000/_devtools/api/audit | jq '{score, errors: .counts.error}'
curl -s localhost:3000/_devtools/api/routes | jq 'length'
Snapshots contain configuration, storage keys, headers and bodies. Credential-looking entries are redacted, application data is not.

Overhead

Three hooks, confined to development: one on container construction, one pass over the route table on the first request, and a wrap of the HTTP entry point for status and duration. Bootstrap profiling stops on the first request and the buffers are bounded, so a long-running dev server does not grow without limit.

Devtools are off in production builds unless you enable them explicitly, and then only behind a token. See Configuration.

Previous

Configuration

Plugin options, access tokens and what devtools refuse to do outside development

Next