@contract-kit/devtools
v1.0.0
Published
Development-time devtools for Contract Kit - mini Telescope for the server runtime
Maintainers
Readme
@contract-kit/devtools
Development-time event timeline for Contract Kit apps. It records HTTP requests, errors, use case runs, domain events, jobs, and provider activity in a bounded in-memory buffer, then serves a live dashboard from your app.
Devtools is enabled outside production by default and returns a no-op port in production unless you explicitly enable it.
Install
bun add @contract-kit/devtoolsNext.js setup
Register the provider and server hook:
import {
createDevtoolsHooks,
createDevtoolsProvider,
} from "@contract-kit/devtools";
import { createNextServer } from "@contract-kit/next";
export const server = await createNextServer({
ports,
providers: [createDevtoolsProvider(), ...providers],
hooks: [createDevtoolsHooks()],
createContext: ({ req, ports }) => ({
requestId: req.headers.get("x-request-id") ?? crypto.randomUUID(),
ports,
}),
});Add a catch-all route:
// app/api/devtools/[[...path]]/route.ts
import { createDevtoolsRoute } from "@contract-kit/devtools";
import { server } from "@/server";
export const { GET, POST } = createDevtoolsRoute(server.ports.devtools, {
basePath: "/api/devtools",
});Open /api/devtools.
The dashboard connects to the event stream with Server-Sent Events and falls
back to polling when EventSource is unavailable. It includes tabs for the
timeline, requests, use cases, errors, domain events, jobs, providers, and
provider-owned events such as database/cache/mail/auth/rate limit activity, and
custom events.
Request rows expand into correlated events that share the same traceId or
requestId.
Use the toolbar to search across event summaries, paths, messages, names, watchers, IDs, and details. The dashboard also includes method, status, and watcher filters for narrowing noisy timelines.
Trace context
Devtools is OpenTelemetry-compatible without depending on the OpenTelemetry SDK.
createDevtoolsHooks() reads incoming W3C traceparent headers, creates a
local span when one is missing, exposes the current traceparent response
header, and adds trace fields to captured events.
All captured events can include:
traceId: W3C trace ID for distributed correlationspanId: span ID for the operation represented by the eventparentSpanId: parent span ID when the event is nestedtraceparent: W3C header value for the current span
For object contexts, the hook also adds these fields before the handler runs so use case instrumentation can attach nested spans to the request trace.
Watchers
Devtools is organized around watchers. A watcher owns one category of capture and records typed events into the shared timeline.
Built-in watchers:
requestsrecords HTTP request timing and contract route activity.errorsrecords unhandled errors, use case failures, and devtools failures.useCasesrecords application command and query execution.eventBusrecords domain event publishing.jobsrecords background job lifecycle events.providersrecords provider setup, start, and stop activity.dbrecords database diagnostics from first-party providers.cacherecords cache diagnostics from first-party providers.mailrecords mail diagnostics from first-party providers.authrecords auth diagnostics from first-party providers.rateLimitrecords rate limit diagnostics from first-party providers.customrecords application and integration-specific diagnostic events.
Configure watchers through the provider:
createDevtoolsProvider({
watchers: {
requests: true,
useCases: true,
eventBus: false,
jobs: false,
db: true,
},
});Disabled watchers do not store matching events. The installed watcher metadata
is available through ctx.ports.devtools.getWatchers() and the dashboard API.
Custom integrations can also register watcher metadata for their own event
types. Custom watcher tabs appear in the dashboard when they own custom
events:
createDevtoolsProvider({
watchers: {
search: {
label: "Search",
description: "Search query and indexing diagnostics.",
eventTypes: ["custom"],
},
},
});Then record events with watcher: "search" so the custom watcher controls
whether they are stored.
Use case instrumentation
Bridge the application package's onRun hook once in your shared use case
factory:
import { createUseCase } from "@contract-kit/application";
import { createDevtoolsUseCaseObserver } from "@contract-kit/devtools";
export const useCase = createUseCase<AppContext>({
onRun: createDevtoolsUseCaseObserver<AppContext>(),
});The observer reads ctx.ports.devtools, ctx.requestId, and trace context
fields by default. Use case start, end, and error phases share the same
span when they run with the same request context.
Provider instrumentation
First-party and app-level providers should use createProviderDevtools() instead
of reaching into ports.devtools manually. The helper accepts either a ports
object or a devtools port, records through record(), and adds provider metadata
to custom events.
import { createProviderDevtools } from "@contract-kit/devtools";
import { createProvider } from "@contract-kit/ports";
export const searchProvider = createProvider({
name: "search",
setup({ ports }) {
const devtools = createProviderDevtools(ports, {
providerName: "search",
watcher: "custom",
});
return {
ports: {
search: {
async query(text: string) {
const results = await runSearch(text);
devtools.custom({
name: "search.query",
label: "Search query",
summary: `${results.length} results`,
details: { resultCount: results.length },
});
return results;
},
},
},
};
},
});Use a built-in watcher such as db, cache, mail, auth, or rateLimit
when the provider belongs to one of those categories. Use custom or a custom
watcher name for application-specific integrations.
Manual events
Use record() for application-specific events. It fills id and timestamp.
ctx.ports.devtools.record({
type: "custom",
watcher: "search",
name: "search.query",
label: "Search query",
summary: "24 results in 18ms",
details: {
query,
resultCount: 24,
durationMs: 18,
},
});log() is still available when you already have a complete DevtoolsEvent.
Endpoints
GET /api/devtoolsserves the dashboardGET /api/devtools/eventsreturns JSON eventsGET /api/devtools/streamreturns a live Server-Sent Events streamPOST /api/devtools/clearclears the buffer
Event list query parameters:
type:request,error,usecase,eventBus,job,provider, orcustomrequestId: correlation IDtraceId: W3C trace IDlimit: maximum events to return, default200
Configuration
DEVTOOLS_ENABLED=true
DEVTOOLS_ENABLED=false
DEVTOOLS_MAX_EVENTS=1000The default buffer keeps the latest 500 events.
The provider controls whether events are recorded. The HTTP route controls
whether those events are exposed. Both default to development-only behavior.
Route handlers return 404 when NODE_ENV === "production" unless explicitly
enabled:
export const { GET, POST } = createDevtoolsRoute(server.ports.devtools, {
basePath: "/api/devtools",
enabled: process.env.DEVTOOLS_ENABLED === "true",
authorize: (req: Request) =>
req.headers.get("x-devtools-token") === process.env.DEVTOOLS_TOKEN,
});If authorize returns false, devtools responds with 404. If it returns a
Response, that response is used, which lets applications return their own
403 or redirect response.
Redaction
Devtools applies default redaction before events are stored. Sensitive keys such
as authorization, cookie, set-cookie, x-api-key, token, password,
and secret are replaced with [redacted].
Request hooks record request headers for debugging, but do not record request or response bodies by default.
You can add a custom redactor:
createDevtoolsHooks({
redact: (event) => ({
...event,
details: scrub(event.details),
}),
});The in-memory store also accepts a redactor for custom setups:
const devtools = createInMemoryDevtools({
redact: (event) => event,
});API
interface DevtoolsPort {
log(event: DevtoolsEvent): void;
record(event: DevtoolsEventInput): DevtoolsEvent;
subscribe(listener: DevtoolsListener): () => void;
getEvents(filter?: DevtoolsFilter): DevtoolsEvent[];
getWatchers(): DevtoolsWatcher[];
isWatcherEnabled(name: DevtoolsWatcherName): boolean;
clear(): void;
}function createProviderDevtools(
target: unknown,
options: {
providerName: string;
watcher?: DevtoolsWatcherName;
},
): ProviderDevtools;type DevtoolsEvent =
| RequestEvent
| ErrorEvent
| UseCaseEvent
| EventBusEvent
| JobEvent
| ProviderEvent
| CustomDevtoolsEvent;All events include id, timestamp, optional requestId, optional watcher,
optional traceId, optional spanId, optional parentSpanId, optional
traceparent, and optional redacted details.
createDevtoolsHooks() accepts:
type DevtoolsHooksOptions<Ctx> = {
basePath?: string;
requestIdHeader?: string | false;
traceContextHeader?: string | false;
getRequestId?: (args: {
req: HttpRequestLike;
ctx?: Ctx;
response?: HttpResponseLike;
}) => string | undefined;
getTraceContext?: (args: {
req: HttpRequestLike;
ctx?: Ctx;
response?: HttpResponseLike;
}) => DevtoolsTraceContextInput | string | undefined;
redact?: DevtoolsRedactor;
};createDevtoolsRoute() and handleDevtoolsRequest() accept:
type DevtoolsRequestOptions = {
basePath: string;
enabled?: boolean;
authorize?: (
req: Request,
) => boolean | Response | Promise<boolean | Response>;
};Production safety
The HTTP handlers return 404 when NODE_ENV === "production" by default. The
provider also installs a no-op devtools port in production by default so app
code does not need null checks.
Devtools can contain sensitive request, error, and domain data. Keep it on local development routes unless you intentionally add authentication and redaction.
License
MIT
