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

@credence/otel

v0.6.0

Published

OpenTelemetry bridge for Credence: epistemic events as spans, and a trace id as first-class provenance.

Readme

@credence/otel

Credence is not a second thing to instrument. Your observability stack — Langfuse, Dynatrace, Foundry, whatever — answers what ran. Credence answers what is known, on what evidence, and what is missing. This package makes those two answers the same trace.

Two directions, both optional, neither able to change what the ledger stores.

Emit — epistemic events as spans

import { createLedger } from "@credence/core";
import { createOtelObserver } from "@credence/otel";

const ledger = await createLedger({ observer: createOtelObserver() });

That is the integration. Twelve event types become spans, parented to whatever span your agent was already inside:

| Span | When | |---|---| | credence.claim.recorded | a claim is written (idempotent re-writes included, flagged) | | credence.claim.superseded | a correction supersedes an earlier claim | | credence.finding.opened | a gap / conflict / risk opens, or reopens because evidence moved | | credence.finding.resolved | the condition cleared | | credence.finding.dismissed | somebody dismissed it | | credence.obligation.blocked | a blocking obligation was opened | | credence.obligation.fulfilled | an obligation reached done or waived | | credence.judge.verdict | the judge decided — including abstained and deferred | | credence.snapshot.taken | an epistemic snapshot was frozen | | credence.decision.recorded | a decision is written, including the basis it was standing on | | credence.decision.stale | assessment flipped to STALE (emitted on transition, never polled) | | credence.decision.revalidated | a new decision superseded a stale one |

@opentelemetry/api is an optional peer dependency. Your application owns the SDK, the exporter and the version; this package never installs one, and installing any other Credence package never drags OpenTelemetry in. "Optional" means optional to install, not optional to work: importing @credence/otel without the api present fails loudly at module resolution rather than degrading into a bridge that silently records nothing.

With the api present but no SDK registered, the API hands back a no-op tracer. startSpan allocates a NonRecordingSpan and end() does nothing, so installing the observer is a genuine zero-behaviour-change addition — not a partially recorded trail.

A missing correlation is written down, not left blank

Every span carries credence.correlated. false means there was no valid caller span to attach to, so this span is a trace root and the write cannot be joined to whatever the agent was doing. Left implicit, that is indistinguishable from a parent a sampler dropped. The same instinct as indeterminate in verifyChain and unreported in the autonomy report: when the answer is "we cannot say", say which kind of "cannot".

Parenting only survives an await if a ContextManager is registered. NodeSDK.register() does this for you. A hand-built BasicTracerProvider does not — register AsyncLocalStorageContextManager yourself, or every Credence span comes out a trace root.

Consume — a trace id is provenance

import { traceEvidence } from "@credence/otel";

await ledger.recordClaim({
  caseId, key: "total_debt", value: 410_000, status: "verified",
  evidence: traceEvidence({ locator: { page: 4, bbox: [120, 300, 480, 330] } }),
});

This mints a source with the existing kind: "tool_receipt" — the source kind Credence already had for "the agent called something and this came back" — whose uri is otel://trace/<trace-id>/span/<span-id> and whose meta carries the W3C traceparent. There is no second provenance concept, so every read surface that already understood evidence understands this for free.

traceSourceId() returns the id that source will have, before the insert, because source identity is content-addressed. Pass it as toolReceiptId in the same recordClaim call and the join closes in both directions: the chain entry names the receipt, and the span's credence.audit.entry.id names the entry.

Outside a recorded span traceEvidence() returns [] rather than a receipt pointing at trace 0000…. A verified claim written that way fails with EvidenceRequiredError, which is correct: a receipt nobody can open is not evidence. When you need to know why there is no provenance, traceProvenanceState() answers with traced, untraced (nothing was tracing) or unrecorded (something was, but the span is non-recording — no SDK, or a sampler dropped it). Those call for opposite fixes, and one undefined cannot tell them apart.

Spans carry ids and hashes. Never values.

An observability backend is shared storage, read by more people than your database and retained longer than anyone intends. So spans carry refs, keys, statuses, content hashes and value hashes — enough to correlate and to confirm, never enough to leak. Locators (page, bbox, selector text) are dropped wholesale. Human actors are hashed; machine actors are emitted verbatim, because those are role names, not people — the prefixes come from DEFAULT_MACHINE_ACTOR_PREFIXES in @credence/core (judge:, agent:, bot:, system:), never a second copy that could drift.

If you genuinely want value-level detail, the opt-ins are named to look wrong in a code review:

createOtelObserver({
  dangerouslyRecordClaimValues: true,
  dangerouslyRecordLocators: true,
});

Attribute names

Credence-specific attributes are namespaced credence.*. Exactly one external convention name is borrowed — gen_ai.request.model, set only when a verdict actually came from a model — plus the W3C traceparent, which is a W3C Recommendation rather than an experimental convention.

Checked against the live upstream spec on 2026-07-20, not assumed:

  • The whole gen_ai.* namespace has moved out of open-telemetry/semantic-conventions; every row there is now marked Deprecated with the note "Moved to the OpenTelemetry GenAI semantic conventions repository".
  • In its new home, open-telemetry/semantic-conventions-genai, gen_ai.request.model is marked Development — not stable. So are gen_ai.provider.name, gen_ai.operation.name, gen_ai.agent.* and the whole gen_ai.evaluation.* family, which would otherwise be the natural home for a judge verdict. Those are not used; the verdict lives under credence.judge.*.
  • error.type is Stable, and would be safe to borrow — but nothing here has an error to name, so it is not in the registry either.

A name that has already relocated once and is still Development will move again. A wrong convention name is indistinguishable from a right one on a dashboard until it silently fails to join. src/attributes.ts records the reasoning per name, and test/attributes.test.ts asserts the borrowed list has not grown.

Determinism

Spans carry wall-clock times and random ids; Credence's renders and ids must not. The observer is void-returning, called after each write is decided, never awaited, and its exceptions are swallowed (warned once). test/determinism.test.ts runs the same scenario instrumented — with both dangerously* opt-ins on — and uninstrumented, and asserts byte-identical renderCaseIndex output and identical claim, finding and snapshot ids and snapshot payload hash.

test/entrypoints.test.ts holds the other half of the boundary: this package reaches no OTel SDK, no exporter and none of the heavy Credence packages, and @credence/core is imported for types and pure helpers only. Its mirror in packages/core/test/observer.test.ts asserts no file under core/src mentions @opentelemetry at all.

Testing

Everything runs against InMemorySpanExporter — no collector, no Docker, no network. One optional suite round-trips through a real OTLP endpoint. A collector is configured in this repo, on port 14318 so it never collides with one you are already running on 4318:

docker compose up -d otel-collector

CREDENCE_OTEL_ENDPOINT=http://localhost:14318/v1/traces \
CREDENCE_OTEL_SPAN_FILE=.otel-out/spans.jsonl \
  pnpm vitest run packages/otel

The endpoint alone proves the export was accepted. The span file is what makes it an assertion: the collector writes every span it received to .otel-out/spans.jsonl (see docker/otel-collector.yaml), and the suite reads them back to check that the attribute names and value types survived serialization — a boolean still a boolean, a count still an integer. That is the one claim InMemorySpanExporter structurally cannot make, because it never serializes anything.

Without those variables the suite skips and prints why; CREDENCE_OTEL_STRICT=1 turns the skip into a hard failure, so a skipped verification never reads as a passing one.

Worked example

examples/otel-agent-runpnpm --filter @credence/example-otel-agent-run start. It prints the span tree and walks from a span to the claim, and from the claim to the page and bounding box.