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

@crow-agent/telemetry

v0.2.0

Published

Vendor-neutral telemetry contracts with a reference adapter, conformance suite, and typed schemas.

Readme

@crow-agent/telemetry

Telemetry without a vendor. This package defines the contracts Crow packages use to record what they did — spans, attributes, events, statuses — plus typed schema helpers for keeping that vocabulary consistent. There is no exporter here, no ambient "current span", and no backend dependency: you bring the backend (OpenTelemetry, Sentry, logs, …) by implementing the adapter contract, or you use the bundled in-memory reference for tests and local diagnostics.

The core idea

A span is a timed record of one operation: it opens before the work and closes when the work finishes. Spans nest — a request span can contain a cache lookup and a database query — forming a tree that shows where time went. Each span carries attributes (named facts like account.id=123), events (point-in-time occurrences like cache.lookup, optionally with their own attributes), and a status (ok, or error with a name and message).

A TelemetryContext is the handle that says where new work belongs in that tree. Starting a span from a context makes the new span its child. The API is callback-based and explicit: contexts are passed as arguments, never stashed in ambient state.

import {
  NOOP_TELEMETRY_CONTEXT,
  type TelemetryContext,
} from "@crow-agent/telemetry";

async function fetchProfile(
  profileId: string,
  telemetryContext: TelemetryContext = NOOP_TELEMETRY_CONTEXT,
) {
  return telemetryContext.startSpan(
    {
      name: "example.profile.fetch",
      attributes: { "example.profile.id": profileId },
    },
    async (span) => {
      const profile = await readProfile(profileId);
      span.setAttributes({ "example.profile.found": profile !== undefined });
      return profile;
    },
  );
}

Hand the callback's span deeper to nest explicitly — a span doubles as the child context for its own children:

return telemetryContext.startSpan({ name: "example.parent" }, async (parent) => {
  return parent.startSpan({ name: "example.child" }, async (child) => {
    child.addEvent("example.cache.lookup", { "example.cache.hit": true });
    return performWork();
  });
});

There is no public end(). startSpan() owns settlement: the span stays open until the callback's value or promise settles. When a failure is an expected outcome rather than a throw, set the status yourself:

return telemetryContext.startSpan({ name: "example.save" }, async (span) => {
  const result = await save();
  if (!result.ok) {
    span.setStatus({
      status: "error",
      error: { name: "SaveError", message: result.reason },
    });
  }
  return result;
});

Recording must never change the work: a span is diagnostic data, not business state. Whether the account load runs, succeeds, fails, or persists must be identical with telemetry on or off.

Writing an adapter

An adapter implements TelemetryContext and translates the generic API into a backend's native concepts. The contract is behavioral — these are the observable semantics your adapter must honor:

  • Invoke the callback synchronously, exactly once.
  • Preserve the callback's value and rejection identity: a synchronous throw becomes a promise rejected with the same value.
  • Keep the native span open until a returned promise settles.
  • Default normal completion to ok and throws/rejections to error, unless an explicit status was set.
  • setStatus() is last-write-wins.
  • setAttributes() merges: later defined values replace earlier ones, undefined is ignored.
  • Recording methods are synchronous, passive, and never throw.
  • Calls after settlement are inert.
  • A failed recording call is ignored atomically — backend failures are suppressed — while the business callback still runs exactly once.

Adapters may use backend-native ambient context internally for automatic instrumentation, but Crow code always passes the parent explicitly. Exporter buffering, flushing, sampling, backend IDs, and backend-specific context objects are the adapter's business, not the contract's. The conformance suite checks all of this for you.

The no-op context

When telemetry is optional, default to NOOP_TELEMETRY_CONTEXT:

import { NOOP_TELEMETRY_CONTEXT } from "@crow-agent/telemetry";

const noopResult = await NOOP_TELEMETRY_CONTEXT.startSpan(
  { name: "example.operation" },
  () => runOperation(),
);

It invokes callbacks synchronously, preserves values and rejections (turning sync throws into rejections with the same value), and reuses one shared frozen inert span for every nesting level. It never inspects or retains names, attributes, events, or statuses.

The in-memory reference

InMemoryTelemetryContext is the backend-neutral reference implementation — handy for tests, local diagnostics, or any app that deliberately wants process-local capture with no exporter:

import { InMemoryTelemetryContext } from "@crow-agent/telemetry";

const recorder = new InMemoryTelemetryContext();

await recorder.startSpan(
  { name: "example.operation", attributes: { input: "demo" } },
  async (span) => {
    span.addEvent("example.started");
    span.setAttributes({ "example.results": 3 });
  },
);

console.log(recorder.getSpans());

getSpans() returns detached snapshots in span-start order. Each RecordedTelemetrySpan has a deterministic numeric ID, parent ID, merged attributes, ordered events, final status, and settlement state — but no timestamps, by design, so snapshots compare cleanly.

Storage is unbounded and process-local: create a fresh instance per test or recording scope, and don't record sensitive attributes unless the caller's data policy allows it.

Testing adapters

@crow-agent/telemetry/testing ships a runner-independent conformance suite. You supply a fixture — a fresh context plus a function that converts your backend's finished spans into normalized RecordedTelemetrySpan snapshots — and the suite supplies grouped cases:

import {
  createTelemetryAdapterConformance,
  type TelemetryAdapterFixture,
} from "@crow-agent/telemetry/testing";
import { describe, it } from "vitest";

const cases = createTelemetryAdapterConformance(async () => {
  const backend = createMyTelemetryAdapter();
  return {
    context: backend.context,
    getSpans: async () => backend.normalizedSpans(),
    async [Symbol.asyncDispose]() {
      await backend.close();
    },
  } satisfies TelemetryAdapterFixture;
});

for (const group of new Set(cases.map((c) => c.group))) {
  describe(group, () => {
    for (const testCase of cases.filter((c) => c.group === group)) {
      it(testCase.name, () => testCase.run());
    }
  });
}

Cases cover synchronous single admission, value and rejection identity, automatic vs. explicit status, attribute merging, event ordering, inert post-settlement calls, nested and concurrent parentage, and suppression of unreadable telemetry payloads. getSpans() may flush an async exporter before returning. The testing subpath uses Node's assertion APIs; the root package stays runtime-neutral.

Typed schemas

The span API deliberately accepts open names and attribute bags so adapters stay generic. Domain packages layer closed, serializable schemas on top and infer exact TypeScript types from them:

import {
  createTypedSpanStarter,
  defineTelemetrySchema,
} from "@crow-agent/telemetry";

export const EXAMPLE_TELEMETRY_SCHEMA =
  defineTelemetrySchema({
  version: 1,
  spans: {
    "example.read": {
      description: "Read one resource",
      parents: { kind: "any" },
      startAttributes: {
        "example.resource": {
          type: "string",
          required: true,
          values: ["account", "project"],
          description: "Resource kind",
        },
      },
      endAttributes: {
        "example.item_count": {
          type: "number",
          description: "Number of returned items",
        },
      },
      events: {
        "example.cache": {
          description: "Cache lookup result",
          attributes: {
            "example.cache.hit": {
              type: "boolean",
              required: true,
              description: "Whether the cache contained the resource",
            },
          },
        },
      },
      status: {
        default: "ok",
        errorWhen: "The read throws or returns an error result",
      },
    },
  },
} as const);

const startExampleSpan = createTypedSpanStarter(
  telemetryContext,
  [EXAMPLE_TELEMETRY_SCHEMA],
);

The starter exposes one overload per span and checks names and attributes at compile time. Its callback receives a child starter over the same schemas, already bound to the callback's span:

await startExampleSpan(
  "example.read",
  { "example.resource": "account" },
  async (span, startChildSpan) => {
    span.addEvent("example.cache", { "example.cache.hit": true });
    const accounts = await readAccounts();
    span.setAttributes({ "example.item_count": accounts.length });

    await startChildSpan(
      "example.read",
      { "example.resource": "project" },
      async (childSpan) => {
        const projects = await readProjects();
        childSpan.setAttributes({ "example.item_count": projects.length });
      },
    );

    return accounts;
  },
);

defineTelemetrySchema() is a typed identity function: it returns ordinary JSON-serializable data and performs no runtime validation or parent-rule enforcement. Schemas are not merged, inspected, or retained at runtime, and duplicate literal span names across the array are a compile error.

Start attributes vs. end attributes

startAttributes and endAttributes describe when an attribute is normally known — not separate storage. Both become ordinary attributes on the same backend span:

| Declared in | Passed via | Requiredness | |---|---|---| | startAttributes | The starter's attributes argument, when the span is created | Each definition sets required: true or false | | endAttributes | The schema-scoped span's setAttributes(), any time before settlement | Always optional |

So example.resource is known when example.read starts, while example.item_count is only known after readAccounts() returns. "End" here means completion enrichment: an end attribute may be set at any point while the callback is active and omitted when unavailable — calling setAttributes() zero times is valid, which matters for early failures, cancellation, and provider-specific data that may not exist on every path.

Repeated setAttributes() calls merge into the same bag: a later defined value replaces an earlier one for the same key, undefined is ignored, and the schema-scoped method only accepts the current span's declared end attributes. Attributes never end the span — returning, resolving, throwing, or rejecting from the callback does; startSpan() performs the actual end operation, and adapter calls after settlement are inert.

A starter composes multiple independently versioned schemas:

import { AGENT_TELEMETRY_SCHEMAS } from "@crow-agent/agent-core";

const startCrowAgentSpan = createTypedSpanStarter(
  telemetryContext,
  AGENT_TELEMETRY_SCHEMAS,
);

Inline schema arrays keep their tuple types automatically; separately declared arrays need as const. Union-valued names must be narrowed before a call so the runtime name stays tied to its attribute schema. The schema-derived types reject missing required attributes, unknown keys, invalid closed-set values, undeclared events, and attributes on empty schemas.

Schema metadata

Supported attribute types: string, number, boolean, and their array forms string[], number[], boolean[]. Attribute definitions support:

  • values — closed set for scalar values;
  • elementValues — closed set for array elements;
  • examples — documentation examples;
  • sensitive — marks data needing special handling;
  • cardinality — expected low or high cardinality.

required is declared on start and event attributes; end attributes are always optional (see above). Parent metadata is descriptive schema data:

  • { kind: "any" } — root or any caller span;
  • { kind: "root_or_external" } — root or a caller-owned span outside the schema;
  • { kind: "spans", spans: [...] } — only the listed schema spans.

Adapters never need to understand schema objects; instrumentation helpers and tests use them to keep emitted names and attributes consistent.

How Crow packages use it

Ownership is split on purpose:

  • @crow-agent/telemetry owns the vendor-neutral contract, the no-op and in-memory contexts, the schema utilities, and the conformance suite.
  • @crow-agent/ai accepts and propagates telemetryContext in provider request options but owns no telemetry schema.
  • @crow-agent/agent-core owns the AI-request and harness schemas, exports them as a combined readonly tuple, and provides typed span helpers:
import {
  AGENT_TELEMETRY_SCHEMAS,
  AI_TELEMETRY_SCHEMA,
  HARNESS_TELEMETRY_SCHEMA,
  startAiSpan,
  startHarnessSpan,
} from "@crow-agent/agent-core";

The Crow schemas speak a Crow-owned vocabulary — crow.ai.*, crow.harness.*, crow.session.*. Adapters may translate those names into backend conventions without changing what Crow emits.

Security and portability

Telemetry is process-local diagnostics, not durable application state. Never persist a TelemetryContext, a TelemetrySpan, or a backend-native trace object in records, messages, snapshots, or deferred handles.

Attribute values are intentionally limited to primitive scalars and arrays. Keep prompts, completions, tool arguments and output, file contents, provider payloads, headers, credentials, and free-form error details out of attributes unless a schema and data policy explicitly allow them.

The package avoids AsyncLocalStorage and every other runtime-specific ambient context API, so it runs on Node.js, Bun, browsers, and workers alike. Backend adapters own their own runtime compatibility.

API reference

Core types and values

| Export | Purpose | |---|---| | TelemetryContext | Opens callback-scoped child spans | | TelemetrySpan | Captures attributes, events, and status; doubles as a child context | | SpanOptions | Span name with optional starting attributes | | SpanAttributes / AttributeValue | Open attribute bag and its value types at the adapter level | | SpanStatus | Explicit ok / error outcome | | NOOP_TELEMETRY_CONTEXT | Shared inert context for when telemetry is off | | InMemoryTelemetryContext | Deterministic process-local reference adapter | | RecordedTelemetrySpan | Snapshot of one captured span | | RecordedTelemetryEvent | Snapshot of one captured event |

Schema definitions and inference

| Export | Purpose | |---|---| | defineTelemetrySchema() | Typed identity for serializable schema data | | createTypedSpanStarter() | Binds a parent context to one or more schema vocabularies | | TypedSpanStarter | Exact starter type; callbacks receive recursively child-bound starters | | TelemetrySchemaDefinition | Top-level schema shape | | TelemetrySpanDefinition | One span's metadata, parents, attributes, events, and status rule | | TelemetryAttributeType | Scalar and array type names | | TelemetryAttributeMetadata | Description, sensitivity, and cardinality notes | | TelemetryAttributeDefinition | Attribute type, allowed values, examples, metadata | | TelemetryEventAttributeDefinition | Event attribute definition, with requiredness | | TelemetryEventDefinition | Event description with its attribute definitions | | TelemetryParentDefinition | Open, external-root, or finite parent rule | | TelemetrySchemaSpanName | Union of the span names a schema declares | | TelemetrySchemaSpanStartAttributes | Exact inferred start attributes for a span | | TelemetrySchemaSpanEndAttributes | Optional inferred end attributes for a span | | TelemetrySchemaSpanEventName | Union of the events a span declares | | TelemetrySchemaSpanEventAttributes | Exact inferred attributes for an event | | SchemaTelemetrySpan | Span view narrowed to one schema span | | TelemetrySchemaSpanUnion | Discriminated union of every span in a schema | | InferStartAttributes | Required and optional values inferred from start definitions | | InferOptionalAttributes | Optional values inferred from end definitions | | InferEventAttributes | Required and optional values inferred from event definitions | | InferRequiredAndOptionalAttributes | Shared inference helper for definitions with requiredness | | ExactTelemetryAttributes | Rejects attribute keys outside the expected set |

Testing subpath

| Export | Purpose | |---|---| | createTelemetryAdapterConformance() | Builds runner-independent conformance cases | | TelemetryAdapterFixture | Fresh context plus normalized snapshot reader | | TelemetryAdapterFixtureFactory | Produces isolated fixtures | | TelemetryAdapterConformanceCase | One grouped case for a test runner to execute |

Development

From this package directory:

npm test
npm run build

License

MIT