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

nitroping

v1.0.0

Published

TypeScript SDK for the NitroPing audit log — batching client, error capture, framework adapters, and GenAI cost tracing for eighteen providers.

Readme

nitroping

Audit logging and GenAI observability for TypeScript. One client, batched over HTTP, for NitroPing.

npm install nitroping    # or pnpm / bun / yarn

Audit events

import { NitroPing } from "nitroping"

const nitroping = new NitroPing({ apiKey: process.env.NITROPING_API_KEY! })

nitroping.track({
  action: "invoice.updated",
  actor_id: "usr_1",
  actor_type: "user",
  target_type: "invoice",
  target_id: "inv_42",
  severity: "info",
})

track returns immediately and queues; see Delivery for when the queue is flushed and what happens when it can't be. id, timestamp, tenant_id and ip are assigned by the server — a client cannot claim them.

Framework adapters

Subpath exports, so nothing you don't import is bundled. Each tracks every request as an http.request event with method, path, status and latency, and opens a span that later calls nest under.

import { nitropingMiddleware } from "nitroping/hono"

app.use(
  nitropingMiddleware({
    apiKey: process.env.NITROPING_API_KEY!,
    getActor: (c) => ({ id: c.get("user").id }),
    skip: (c) => c.req.path === "/health",
  }),
)
import { nitropingErrorHandler, nitropingMiddleware } from "nitroping/express"

app.use(nitropingMiddleware({ apiKey }))
app.use(nitropingErrorHandler({ apiKey })) // after the routes
import { nitropingNitroPlugin } from "nitroping/nitro"

export default defineNitroPlugin((nitroApp) => {
  nitropingNitroPlugin(nitroApp, { apiKey })
})

An inbound x-request-id is reused rather than replaced, so a call already traced upstream joins that trace instead of starting its own. The send is fire-and-forget: a slow or failing ingest never blocks the real response.

Errors

import { captureError, withErrorCapture } from "nitroping"

await captureError({ apiKey }, err, { actor: { id: "usr_1" }, category: "billing" })

const charge = withErrorCapture({ apiKey }, async (id: string) => {
  /* rethrows after capturing */
})

Both produce an error.captured event with the same error_type / error_message / stack metadata shape, which is what the panel's Errors view and the MCP find_errors tool group on.

Schema

The Zod schema the server validates against ships too, for callers who want to check an event before sending it:

import { auditEventInputSchema } from "nitroping/schema"

It is a subpath export on purpose — importing nitroping itself pulls in no zod at runtime.


Integrations

Eighteen, in three shapes. Wrap returns the client you passed in, so it is one line at construction with no call-site changes. Hook is a callback you hand to a framework that already has one. OTLP needs no SDK at all.

| | Integration | Shape | | --- | --------------------- | ------ | | 1 | OpenAI | wrap | | 2 | Anthropic | wrap | | 3 | Google Gemini | wrap | | 4 | Amazon Bedrock | wrap | | 5 | OpenRouter | wrap | | 6 | LiteLLM | wrap | | 7 | Azure OpenAI | wrap | | 8 | Vercel AI SDK | hook | | 9 | TanStack AI | hook | | 10 | Eve | hook | | 11 | Mastra | hook | | 12 | Claude Code | OTLP | | 13 | OpenClaw | OTLP | | 14 | Hermes Agent | OTLP | | 15 | Pydantic AI | OTLP | | 16 | LangChain / LangGraph | OTLP | | 17 | CrewAI | OTLP | | 18 | Custom agents | manual |

Wrap

1. OpenAI

const openai = nitroping.observeOpenAI(new OpenAI())
await openai.chat.completions.create({ model: "gpt-4o", messages })

Covers chat.completions.create, responses.create and embeddings.create. Reads both the Chat Completions naming (prompt_tokens) and the Responses naming (input_tokens); reading only one reports zero for the other. Cached and reasoning tokens land in their own columns.

2. Anthropic

const anthropic = nitroping.observeAnthropic(new Anthropic())

Cache writes are billed above the input rate, so they count as fresh input; cache reads are counted separately at the cache rate. Folding both into one bucket under-bills the write and over-bills the read.

3. Google Gemini

const genai = nitroping.observeGoogleGenAI(new GoogleGenAI({ apiKey }))

Gemini's promptTokenCount already includes the cached share, so it is subtracted out — otherwise a cache hit is billed twice, once at each rate. thoughtsTokenCount maps to reasoning.

4. Amazon Bedrock

const bedrock = nitroping.observeBedrock(new BedrockRuntimeClient({ region }))

Intercepts client.send and dispatches on the command: Converse* records as a chat, InvokeAgent* as an agent. InvokeModel passes through untraced — its body is a provider-specific blob with no common usage shape. Use Converse for tracing. cacheWriteInputTokens counts as input, as on Anthropic direct.

5. OpenRouter

const openrouter = nitroping.observeOpenRouter(
  new OpenAI({ baseURL: "https://openrouter.ai/api/v1", apiKey }),
)

OpenRouter returns its own usage.cost, and a provider-reported cost always beats an estimate — it reflects the actual route taken and your actual credits.

6. LiteLLM

const litellm = nitroping.observeLiteLLM(new OpenAI({ baseURL: proxyUrl, apiKey }))

One span per call across every provider the proxy fronts, since LiteLLM normalises usage to the OpenAI shape.

7. Azure OpenAI

const azure = nitroping.observeOpenAI(new AzureOpenAI({ ... }), "azure")

Same wire shape as OpenAI; the second argument sets the provider label so Azure spend is separable from direct OpenAI spend in the panel.

Hook

8. Vercel AI SDK

await streamText({ model, prompt, onFinish: nitroping.onFinish() })

The only path that covers streaming: onFinish fires once the stream ends, with usage already assembled. Uses totalUsage where present, which covers every step of a multi-step tool loop rather than just the last one.

A multi-step run records as an agent rather than a chat, decided by the step count on the event.

9. TanStack AI

onFinish: nitroping.onFinish()

Same handler — TanStack AI passes a compatible finish event.

10. Eve

onFinish: nitroping.onFinish()

Same handler again.

11. Mastra

onFinish: nitroping.onFinish({ provider: "mastra" })

Mastra runs on the Vercel AI SDK underneath, so its finish event carries the same usage shape. provider and model fill in what the event does not carry.

OTLP

Anything with an OpenTelemetry exporter reaches us with no SDK. Point it at the ingest endpoint:

export OTEL_EXPORTER_OTLP_ENDPOINT=https://ingest.nitroping.dev/v1/otlp
export OTEL_EXPORTER_OTLP_PROTOCOL=http/json
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer np_..."

http/json, not protobuf. Most exporters default to protobuf and the endpoint answers 415 with that instruction — decoding protobuf in a Worker means a runtime dependency for a format the same exporters already emit as JSON.

Spans arrive on /v1/otlp/v1/traces, metrics on /v1/otlp/v1/metrics. Logs are accepted and discarded, so an exporter configured to send all three does not error.

12. Claude Code

export CLAUDE_CODE_ENABLE_TELEMETRY=1
export OTEL_METRICS_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=otlp

Claude Code reports tokens and cost as metrics, not spans, and puts its identity on the OTLP resource rather than the span — both handled. Token metrics route by their type attribute into the input / output / cache-read / reasoning columns.

13. OpenClaw

14. Hermes Agent

15. Pydantic AI

Standard GenAI semantic-convention spans. Set the three variables above and they appear as traces. Attribute names changed between convention releases, so both the old (gen_ai.usage.prompt_tokens) and current (gen_ai.usage.input_tokens) spellings are read.

16. LangChain / LangGraph

Via openinference-instrumentation-langchain or OpenLIT, then the same three variables. A LangGraph run keeps its parent links, so the graph shows as one tree in the panel.

17. CrewAI

Via OpenLIT with the same variables. Or, for control over what is recorded, use the manual agent API below.

Manual

18. Custom agents

For a hand-rolled loop, or any framework with no exporter:

const run = nitroping.startAgentRun({ name: "researcher", model: "gpt-4o" })
const results = await run.tool("web_search", () => search(query))
run.end()

tool() returns whatever its callback returned, so it wraps a call without restructuring it. Tool executions share the run's requestId and nest beneath its span, so the panel shows the whole agent as one trace. A tool that throws is recorded as failed and the error re-raised.

run.context carries { requestId, parentSpanId } for anything you record by hand that should hang off the same run.


Cost

cost_usd is computed by the SDK from the provider's token counts against a list-price table — the Worker never recomputes it, since only you know your negotiated rate. A provider-reported cost always wins over an estimate.

Model matching is longest-prefix: gpt-4o-mini-2024-07-18 resolves to gpt-4o-mini, not to gpt-4o which it also contains and which would bill it at sixteen times the real rate.

An unknown model leaves cost unset rather than 0 — a gap in the spend chart reads as a gap, where a zero reads as a real, free call.

Negotiated rates:

new NitroPing({
  apiKey,
  pricing: { "gpt-4o": { input: 1.0, output: 4.0, cachedInput: 0.5 } },
})

Prices are US dollars per million tokens.

Prompt content

Off by default — prompts are the field most likely to carry personal data.

new NitroPing({ apiKey, captureContent: true }) // truncated at 2000 chars

Delivery

Events are batched and flushed every 5s, on reaching maxBatchSize (50), on a browser tab going hidden, and on beforeExit/SIGINT/SIGTERM — so a script or a serverless invocation that ends with a partial batch still sends it. The flush timer is unref'd where the runtime allows, so a queued event can't hold a CLI open.

When the endpoint is unreachable the batch goes back on the queue. So does a 5xx or a 429 — those are the endpoint's problem, not the batch's. A 4xx is not retried: a bad key or a payload the schema rejects will never be accepted, and retrying it only loses the events behind it.

The queue is bounded at maxQueueSize (10,000). Past that the oldest are dropped — the newest events describe the failure in progress.

Loss is never silent:

new NitroPing({
  apiKey,
  maxQueueSize: 50_000,
  onError: ({ reason, dropped, detail }) => {
    // reason: "overflow" | "network" | "rejected"
    logger.warn({ reason, dropped, detail }, "nitroping")
  },
})

await client.close() flushes what is queued and stops accepting more — call it in a shutdown hook when you want the flush awaited rather than best-effort.

Streaming

Raw provider wrappers skip streamed calls: usage arrives in the final chunk, and reading it would consume the caller's stream. The Vercel AI SDK's onFinish is the exception — use it, or startAgentRun, when streaming matters.