@meistrari/usage
v1.0.0
Published
TypeScript SDK for emitting usage events to the usage-ingestor service.
Maintainers
Keywords
Readme
@meistrari/usage
TypeScript SDK for emitting usage events to the usage-ingestor service.
Installation
bun add @meistrari/usageQuick 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, unknowneventType, invalid UUID/timestamp) originconflict 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) inerr.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 # everythingTest 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.).
