npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@contract-kit/devtools

v1.0.0

Published

Development-time devtools for Contract Kit - mini Telescope for the server runtime

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/devtools

Next.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 correlation
  • spanId: span ID for the operation represented by the event
  • parentSpanId: parent span ID when the event is nested
  • traceparent: 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:

  • requests records HTTP request timing and contract route activity.
  • errors records unhandled errors, use case failures, and devtools failures.
  • useCases records application command and query execution.
  • eventBus records domain event publishing.
  • jobs records background job lifecycle events.
  • providers records provider setup, start, and stop activity.
  • db records database diagnostics from first-party providers.
  • cache records cache diagnostics from first-party providers.
  • mail records mail diagnostics from first-party providers.
  • auth records auth diagnostics from first-party providers.
  • rateLimit records rate limit diagnostics from first-party providers.
  • custom records 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/devtools serves the dashboard
  • GET /api/devtools/events returns JSON events
  • GET /api/devtools/stream returns a live Server-Sent Events stream
  • POST /api/devtools/clear clears the buffer

Event list query parameters:

  • type: request, error, usecase, eventBus, job, provider, or custom
  • requestId: correlation ID
  • traceId: W3C trace ID
  • limit: maximum events to return, default 200

Configuration

DEVTOOLS_ENABLED=true
DEVTOOLS_ENABLED=false
DEVTOOLS_MAX_EVENTS=1000

The 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