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.
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
attributesyourself. - 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/envelopeis 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.