Skip to content

Gettier for Runtime

The Node SDK for a deployed application: what your production AI actually did, correlated back to the agent that wrote the code.

npm install @gettier/node

Not published yet

@gettier/node is built and smoke-tested but not on npm yet, and the hosted ingestion endpoint is not serving. Until both land, point endpoint at your own collector — a connection error reaches you through onError, never through your application.

The guarantee that matters

This package fails open. No entry point throws. instrument rethrows the caller's original error unchanged. If Gettier is down, misconfigured, or having its worst day, your request path does not notice.

That is a deliberate asymmetry with the other surface. Gettier for Agents fails closed — holding the gate is the entire point of a gate. Instrumentation is never why a customer request fails. Only explicit enforcement APIs and release gates may fail closed.

Setup

import { Gettier } from '@gettier/node';

const gettier = Gettier.init({
  dsn: process.env.GETTIER_DSN,           // omit and events stay in memory
  environment: 'production',
  release: process.env.GIT_SHA,           // the correlation key — see below
});
Option Meaning
dsn Ingestion credential. With one, batches are POSTed; without one nothing is transmitted and transportName says so rather than implying delivery.
endpoint Override the ingestion URL. Falls back to GETTIER_ENDPOINT, then the hosted API.
environment Required. production, staging, …
release Commit SHA or version. Correlates runtime behaviour to the change that caused it.
project Optional. Ingestion takes the authoritative value from the credential.
maxQueue Default 500. Bounded by construction — a runaway queue must not become a memory leak.
transport Supply your own. An explicit transport wins over a DSN.
onError Where transport failures go. They go nowhere else.

Endpoint resolution never falls back to localhost

Explicit endpoint, then GETTIER_ENDPOINT, then the hosted API. A published package that defaulted to localhost would post your telemetry to a port on your own machine without telling you. For local development set GETTIER_ENDPOINT=http://localhost:3000/api/v1/envelope.

Instrumenting a model call

const answer = await gettier.instrument(
  { provider: 'anthropic', model: 'claude-sonnet-5' },
  () => client.messages.create(request),
);

instrument times the call, records provider, model, latency and any error, and returns whatever your function returned. On a throw it records the error and rethrows the original — not a wrapped one, not a Gettier error.

For a call you have already made:

gettier.recordModelCall({
  provider: 'openai',
  model: 'gpt-5',
  latencyMs: 812,
  inputTokens: 1_204,
  outputTokens: 316,
  traceId: currentTraceId(),
  sessionId: conversationId,
});
Field Notes
provider, model Required.
latencyMs, inputTokens, outputTokens Metadata. Cheap, safe, and what most dashboards want.
error Recorded on failure; instrument fills it for you.
traceId Joins this call to a distributed trace and to agent-time work.
sessionId Groups a conversation.
attributes Opt-in only. Prompts and responses are never captured unless you pass them here.

Privacy is the default, not a setting

  • Metadata before content. Prompts and responses are not captured unless you put them in attributes yourself.
  • Secrets are redacted in core before an event leaves the process — pattern matching over key formats, tokens and connection strings, plus an entropy scan. An event that never carries a secret cannot leak one through a log line, a retry buffer, or a crash dump.
  • Redaction happens twice. The SDK's pass keeps a secret out of your own process's retry buffer; ingestion redacts again on arrival, because POST /api/v1/envelope is a documented path and not every caller uses this SDK.

Delivery, and what happens when it fails

gettier.transportName   // 'http' | 'memory'
gettier.pending         // events queued
gettier.dropped         // events discarded on overflow
await gettier.flush();  // send now — call before exit

Batches are POSTed with a bearer credential, a per-attempt timeout, and bounded retries with exponential backoff. 429 and 5xx retry; other 4xx do not — a permanent refusal is not retried, and a billing refusal arrives as 402 with its reason (trial_expired, turn_limit_reached, …) rather than as a 503 that looks like an outage.

Overflow increments dropped rather than growing without limit or failing silently. Nothing in this list reaches your application; it reaches onError.

Correlating the two surfaces

This is why the runtime surface exists. Agent events and runtime events join on:

  • project id and environment — taken from the ingestion credential, never the body, so a client cannot claim to be another project;
  • release / commit SHA — the change that produced the behaviour;
  • trace id and session id — the individual request.

Set release to the same SHA your CI passes to gettier check, and the ledger can answer the question the two surfaces exist to answer: the premise the agent declared when it wrote this code — did production agree?

Verifying it end to end

Unit tests prove the event shapes match what ingestion accepts. This proves the wire does — real HTTP, real bearer auth, real parseTelemetryEnvelope, real storage:

# 1. a dashboard with an ingestion key (scope must include telemetry:write)
GETTIER_INGESTION_KEYS='{"gtr_local_ingest_key_0001":{"tenantId":"local","organizationSlug":"local","projectSlug":"default","environment":"production","scopes":["telemetry:write"]}}' \
  GETTIER_DATA_DIR=./data pnpm --filter @gettier/web dev

# 2. send a real batch
pnpm --filter @gettier/node e2e:ingest

A pass means the batch was accepted, both events stored with source: runtime and type: model.call, release and traceId preserved, organization/project/environment taken from the credential rather than the body, and a planted secret redacted before it reached the ledger.