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

@meistrari/usage

v1.0.0

Published

TypeScript SDK for emitting usage events to the usage-ingestor service.

Readme

@meistrari/usage

TypeScript SDK for emitting usage events to the usage-ingestor service.

Installation

bun add @meistrari/usage

Quick Start

import { UsageClient } from '@meistrari/usage'

const client = new UsageClient({
    baseUrl: 'https://usage.example.com',
    origin: 'my-service',
})

await client.emit(dataToken, {
    workspaceId: '550e8400-e29b-41d4-a716-446655440000',
    eventType: 'chat.message',
    model: 'gpt-4o',
    inputAmount: 1024,
    outputAmount: 256,
    cost: 0.005,
})

The dataToken is an auth token passed as Authorization: Bearer {token} on every request. It is not stored on the client.

Configuration

| Option | Type | Required | Default | Description | |---|---|---|---|---| | baseUrl | string | Yes | — | Base URL of the usage-ingestor service | | origin | string | Yes | — | Identifies the calling service. Injected into every event. | | timeoutMs | number | No | 5000 | Request timeout in milliseconds. Must be > 0. | | maxRetries | number | No | 3 | Total attempts (1 initial + N-1 retries). Must be an integer >= 1. |

Emitting Events

Single event

await client.emit(dataToken, {
    workspaceId: '550e8400-e29b-41d4-a716-446655440000',
    eventType: 'workflow.step',
    runId: 'a1b2c3d4-...',
    cost: 0.002,
})

Batch (emitMany)

await client.emitMany(dataToken, [
    { workspaceId: '...', eventType: 'workflow.step', runId: '...' },
    { workspaceId: '...', eventType: 'workflow.complete', runId: '...' },
])

Batches must contain between 1 and 500 events. An empty array or a batch exceeding 500 events throws immediately.

Origin Injection

The origin value set in the constructor is automatically injected into every event before sending. The original event objects passed by the caller are never mutated.

If an event already has an origin field set to a different value, the call throws a UsageError immediately (before any network request).

// This throws: event origin "other-service" conflicts with client origin "my-service"
await client.emit(dataToken, {
    workspaceId: '...',
    eventType: 'chat.message',
    origin: 'other-service',
})

Wire Format

The SDK's public interface uses camelCase property names (TypeScript convention), but the HTTP payload sent to the usage-ingestor uses snake_case (matching the Go server's JSON format). The transformation is handled internally — callers only interact with camelCase.

Error Semantics

Deterministic errors — always throws UsageError

These represent bugs or invalid input and are never retried:

  • Validation failure (missing workspaceId, unknown eventType, invalid UUID/timestamp)
  • origin conflict between event and client
  • Batch is empty or exceeds 500 events
  • HTTP 4xx response from the server
  • Server quarantined one or more events (response includes per-event details in err.details)
  • Protocol error (response body could not be parsed)

Error field names

  • Local SDK validation errors report field names in camelCase (e.g., workspaceId, eventType) — matching the public interface.
  • Server-side errors (422, quarantine) preserve the server's snake_case field names (e.g., workspace_id, run_id) in err.details.

Transient errors — retry then fire-and-forget

These are retried with exponential backoff. After all attempts are exhausted, the failure is logged and the call returns without throwing:

  • HTTP 5xx response
  • HTTP 429 (rate limited) — see retry policy below
  • Network error
  • Request timeout

Retry Policy

| Scenario | Behavior | |---|---| | 5xx / network / timeout | Exponential backoff: 2^attempt * 1000ms between attempts | | 429 rate limited | Sleeps for the value in the Retry-After header (seconds); falls back to 5 s if the header is absent or invalid. Does not count as an attempt. | | 429 cap | After 3 consecutive 429 responses, gives up and logs the failure. The counter resets on any non-429 response. | | Retries exhausted | Logs the failure (with masked token) and returns. Does not throw. |

Default retry schedule for 5xx (with maxRetries: 3):

| Attempt | Delay before next | |---|---| | 1 | 1 s | | 2 | 2 s | | 3 (last) | — |

Event Types

type UsageEventType =
    | 'canvas.execution'
    | 'workflow.step'
    | 'workflow.complete'
    | 'chat.message'
    | 'document.parse'
    | 'embedding.generate'
    | 'external.legal'
    | 'external.search'

Testing

This package is Bun-first for development. The official test runner is bun test.

bun run test              # unit tests (default)
bun run test:contract     # contract tests (HTTP loopback)
bun run test:all          # everything

Test categories

| File | Type | Description | |---|---|---| | validate.test.ts | Unit | Event validation rules (required fields, formats, enums) | | transform.test.ts | Unit | camelCase-to-snake_case wire format transformation | | client.test.ts | Unit | Client logic: batching, retries, backoff, error handling (uses mocked fetch) | | contract.test.ts | Contract | Spins up a local HTTP server and exercises the full emit/emitMany flow over loopback |

Contract tests use server.listen(0) for a random port. In restricted sandboxes this may fail with EADDRINUSE. Use test:contract separately if needed.

Error Class

import { UsageError } from '@meistrari/usage'

try {
    await client.emit(dataToken, event)
}
catch (err) {
    if (err instanceof UsageError) {
        console.error(err.message, err.details)
    }
}

UsageError extends Error with an optional details field that carries structured error information (validation errors, server quarantine details, etc.).