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

@zevruna/observe

v0.2.0

Published

Execution traces for AI agents on the Zevruna protocol — models, tools, APIs, databases and MCP on one timeline. Timings and step names only, never arguments.

Readme

@zevruna/observe

Execution traces for AI agents, models, tools, APIs, databases and MCP, on one timeline.

One run, one trace: what the agent did, how long each step took, and whether the run actually achieved anything. Including the runs that failed while every call returned 200.

The zevruna CLI covers the other half, the tool contracts your agents depend on, and CI gating when one moves.

Since 0.2.0 this SDK speaks the Zevruna protocol — one versioned event schema shared with the Python SDK and the ingestion API, down to retry backoff, batch size and redaction. The API surface below is unchanged.

Install

npm i @zevruna/observe

In a monorepo, install it in the workspace that imports it, not at the root. A root install still resolves by hoisting, so the import works and tsc says nothing, until a clean install somewhere else, where it does not. zevruna doctor checks for exactly this.

npm i @zevruna/observe -w @your/api

Node only

This package imports node:async_hooks and node:crypto, so it cannot run in a browser bundle: a bundler stubs those builtins to an empty object, the build passes, and the chunk throws on evaluation, new undefined.AsyncLocalStorage.

If your agent's turn loop runs client-side, wrap the server route it calls instead. That is the honest boundary anyway: the server is where the model call happens, and where a token can live.

Usage

One boundary, wrapped. Instrumenting the MCP client covers every tool call it makes, so these two lines are usually the whole integration.

import { observeAgent, instrumentMcpClient } from "@zevruna/observe";

const client = instrumentMcpClient(mcpClient, "acme-crm");

await observeAgent({ name: "support-agent" }, () => runSupportAgent(input));

observeAgent({ name, runId?, attributes? }, fn)

One complete agent execution. Everything instrumented inside fn, including MCP calls through a wrapped client, attaches to this run automatically. Returns whatever fn returns; a throw is recorded as a failed run and rethrown.

instrumentMcpClient(client, serverName)

Wraps an MCP client's callTool so every call becomes a step, tool name, timing, outcome. Call sites don't change. A 200 carrying isError is recorded as a failure, because it is one.

observeStep({ kind, name, attempt?, attributes? }, fn)

Any other boundary worth timing. kind is model, tool, mcp, http, db, agent or approval. Nested calls become child steps automatically. Outside an observeAgent there is no run to attach to, so the step runs untraced rather than starting one.

await observeStep({ kind: "model", name: "plan", attributes: { model: "claude-opus-5" } }, () =>
  callModel(prompt)
);

markSuccess({ outcome? }) / markFailure({ reason, outcome? })

The business outcome, which is not the HTTP status. A run where every request succeeded and the ticket never got resolved is a failed run, and only your code knows that.

milestone(name, attributes?)

A zero-duration marker on the timeline, handoff, human approval, escalation.

flush() / shutdown()

Runs are batched and flushed on a timer. On serverless or any short-lived process, await shutdown() before exit or the queued runs are lost. flush() returns { sent, queued, events?, rejected?, dropped?, error?, errorKind? } and never throws, check it if you need to know delivery actually happened. errorKind is the machine-readable form, and is the same set of values in every Zevruna SDK: none, no_token, network, timeout, http_retryable, http_permanent, quota, rejected.

A 202 is not proof the runs were kept: over your plan's monthly run quota the API accepts the request and drops the batch. flush() reads the response, so that comes back as sent: 0 with the reason in error, and warns once on the console. It is not requeued, the quota will still be there on the next flush, and a growing queue would evict live runs.

Configuration

ZEVRUNA_TOKEN=zv_live_…                  # project token
ZEVRUNA_ENDPOINT=https://zevruna.com/api
ZEVRUNA_ENVIRONMENT=production           # else NODE_ENV
ZEVRUNA_DISABLED=1                       # no-op every wrapper
ZEVRUNA_SAMPLE_RATE=0.25                 # head-based, decided once per run
ZEVRUNA_BATCH_SIZE=50
ZEVRUNA_FLUSH_INTERVAL_MS=5000
ZEVRUNA_TIMEOUT_MS=10000
ZEVRUNA_MAX_RETRIES=2                    # after the first attempt
ZEVRUNA_MAX_BUFFER=500                   # events, oldest evicted at capacity
ZEVRUNA_CAPTURE_ERROR_TEXT=1             # opt in to server-authored error text (see below)
ZEVRUNA_PROTOCOL=legacy                  # force the pre-0.2 body

Or pass them to init({ token, endpoint, environment, enabled, flushIntervalMs, batchSize, maxBuffer, timeoutMs, maxRetries, sampleRate, protocol, redact, captureErrorText }). init is optional, the first wrapper call configures from the environment. Every name, default and clamp is identical in the Python SDK — see the behavioural spec.

Pointing at an older backend

0.2.0 posts to /events. An endpoint that has never heard of it answers 404, and the SDK falls back to the pre-protocol /runs body for the life of the process and warns once — so upgrading the SDK before the backend costs you a line on stderr rather than your telemetry. ZEVRUNA_PROTOCOL=legacy forces it from the start.

What is collected

Step names, kinds, timings, attempt counts, error classes, and the attributes you pass. Never tool arguments, never tool results, never payloads.

Two things are worth knowing before a security review:

  • Attributes are yours, so they carry whatever you put in them. Pass redact(key, value) to init to scrub them centrally.
  • Error text written by an MCP server can quote the arguments it rejected, so it is not transmitted by default, a failed step records a generic reason instead. captureErrorText opts in. Either way your own code always receives the server's full message.

Details: what we collect.

License

MIT