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

agentrail-node

v0.2.0

Published

Backend HTTP traces, structured logs and MCP journey context for Agentrail.

Readme

agentrail-node

Node 22+ backend evidence for Agentrail: HTTP and fetch traces, application-verified identity, explicit journey milestones and structured logs. Uses OpenTelemetry's HTTP/protobuf exporters. The receiver must support /ingest/otlp/v1/traces and /ingest/otlp/v1/logs before installing this package.

Standalone startup

// instrumentation.ts — load before application HTTP modules
import { init } from 'agentrail-node';

export const agentrail = await init({
  collector: {
    endpoint: 'https://api.example.com/ingest/otlp',
    secret: process.env.AGENTRAIL_SOURCE_SECRET!, // a Node source
  },
  hashKey: process.env.AGENTRAIL_HASH_KEY!,
  serviceName: 'orders-api',
  outboundOrigins: ['https://inventory.internal'],
  onError(error) {
    console.error(error);
  },
});

The application validates its environment before initialization. Source credentials and the workspace hash key stay on the server. Never put them in a page. Start with node --import ./instrumentation.js app.js. init installs one tracer provider, logger provider, async context manager and HTTP/fetch instrumentation. Calling it again with the same options object returns the same client. It refuses an existing provider/context/propagator; use construction-time integration below. Shutdown is terminal for this installation.

A valid edge handoff continues its trace. Direct public calls and refused handoffs start fresh traces; a caller's trace is a directional link only. The HMAC binds method, exact forwarded path/query, parent, workspace key ID and a 60-second issue time. Keep clocks synchronized. Configure edge path/query rewrites with its prepareOriginRequest hook before signing. Arbitrary baggage and trace state do not leave managed requests. Only exact outboundOrigins receive signed context; the actual network destination determines admission, rather than an overridden Host header. Configure the same workspace key at each participating service. Exporter requests and this collector's incoming ingestion traffic are excluded.

Verified context and milestones

// After authenticating the application request:
agentrail.identify({
  kind: 'agent_registration',
  id: authenticatedAgent.id,
  actingUserId: authenticatedAgent.userId,
});

const result = await agentrail.milestone(
  'checkout.confirm',
  { surface: 'custom', kind: 'checkout.confirm' },
  async () => confirmOrder(),
);

identify uses the active managed request, hashes IDs before export and never promotes generic OTel user.id or caller-declared attributes into verified identity. Outside a managed request it reports a capture error. Repeated identical assertions do nothing. A different assertion creates a distinct auth milestone while preserving the first principal. Concurrent requests have independent identity state. milestone(name, context) records an instant step; its callback overload scopes child spans, preserves the callback's result and rethrows its original error. Milestone names and kinds are bounded to 256 characters and should be stable operation names, without arguments or identifiers.

Your own servers

Calls your own services make to this one are not visitors or agents. Leave them out with ignoreIncomingRequest, and check something only your servers can send, such as a shared secret: a user agent or an address is what any caller can claim.

const internalSecret = process.env.INTERNAL_SECRET;
if (!internalSecret) throw new Error('INTERNAL_SECRET is not set');

init({
  // ...
  ignoreIncomingRequest: (request) =>
    request.headers['x-my-internal'] === internalSecret,
});

Check the secret is set before init. Unset, a request with no such header would match it, and every ordinary request would go unrecorded.

Structured logs

import pinoLogger from 'pino';
import { pino } from 'agentrail-node/pino';
const logger = pinoLogger({ hooks: pino(agentrail) });
logger.error({ err: new TypeError('private detail') }, 'private detail');

agentrail.log({
  severityNumber: 17,
  errorType: 'PaymentDeclined',
  eventName: 'checkout.failed',
});

The optional Pino 9/10 hook captures synchronously in the caller's async context and composes an existing hook as its second argument. The original logger output is unchanged. The standard OTel log processor is also available. Default exports contain severity, error type, event name, a stable per-record ID and exact span correlation. Arguments, results, headers, arbitrary attributes, exception messages and message bodies are excluded. Generic logs never create journey proof. Set logs.uncorrelated: false to exclude logs outside managed requests.

Message text requires an explicit field allowlist, for example logs: { includeFields: ['msg'] } for Pino or ['message'] for a standard OTel body. client.log selects from fields. Only selected top-level scalar fields are serialized, with a 4096-character body limit. Fields are considered in allowlist order. String values are shortened before serialization, including JSON escaping and complete Unicode code points; fields whose keys or scalar values cannot fit are omitted, so the exported object stays valid JSON. Do not select credentials, raw identities or sensitive customer content. Span names for supporting operations are neutralized, exception events and status messages are dropped, URLs retain only valid h markers; a sanitized URL over 4096 characters is omitted while the entry and its trace evidence are retained, and resource exports contain service/component/hash-key metadata. Service names/instance IDs, routes, operation names, user agents and explicitly selected fields are customer-controlled context: choose non-sensitive values.

Existing OpenTelemetry provider

Create the processors before constructing the customer's provider. A running provider cannot recover spans its sampler already dropped.

import { createProcessors } from 'agentrail-node';
import {
  TracerProvider,
  ParentBasedSampler,
  TraceIdRatioBasedSampler,
} from '@opentelemetry/sdk-trace';
import { LoggerProvider } from '@opentelemetry/sdk-logs';
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
import { UndiciInstrumentation } from '@opentelemetry/instrumentation-undici';

const capture = createProcessors({
  ...options,
  propagator: customerPropagator,
});
const provider = new TracerProvider({
  sampler: capture.sampler(
    new ParentBasedSampler({ root: new TraceIdRatioBasedSampler(0.1) }),
  ),
  spanProcessors: [customerProcessor, capture.spanProcessor],
});
const logProvider = new LoggerProvider({
  processors: [customerLogProcessor, capture.logProcessor],
});
// In the existing startup, register provider/logProvider, its async context manager,
// capture.propagator and these request-aware hooks once:
const http = new HttpInstrumentation(capture.httpOptions);
const fetch = new UndiciInstrumentation(capture.undiciOptions);
http.setTracerProvider(provider);
fetch.setTracerProvider(provider);

Pass the existing TextMapPropagator as options.propagator, then register the returned capture.propagator once. It delegates extraction/injection for unmanaged work and prevents a late customer injection from re-adding headers to managed fetches. Keeping the original propagator globally bypasses this boundary: Undici injects after its request hook. For multiple installations, chain the earlier installation's returned propagator as the next one's base. Request-aware hooks still bind the actual destination/method/target before signing.

Retain other customer instrumentation; install only one HTTP and one Undici instrumentation, passing these options to them. Keep the request-aware propagator and HTTP hooks together: extraction alone cannot verify method and path. The factory registers no globals and never changes another exporter's input. The sampler promotes dropped managed spans to record-only, preserving the customer's sampled flag. Standard sampled-only exporters retain their sampling; custom exporters that export record-only spans may see additional records. Without this sampler wrapper, dropped entries report entry_not_recording, and their ancestry is unavailable. Ordinary spans outside managed requests are not captured by the Agentrail span processor.

For Sentry, compose this sampler around SentrySampler while constructing the single customer-owned provider, retaining SentrySpanProcessor and its context manager, with new SentryPropagator() passed as the base and the returned capture propagator registered globally. A 100% Sentry sample rate still drops HEAD/OPTIONS; merely adding a processor cannot retain those spans. Agentrail's packed installation gate exercises this public composition. In framework admission mode, use the supplied request-aware incoming hook/propagator in the admitted scope and disable automatic incoming instrumentation, so there is one backend entry per request. Handler work after admission is captured; earlier authentication/body parsing is outside that scope.

Pinned compatible OTel family: API 1.9.1, core/trace/resources/context 2.10.0, logs/exporters/HTTP instrumentation 0.221.0, Undici instrumentation 0.31.0. This release supports those APIs; check dependency compatibility when composing a distribution.

Delivery and shutdown

Defaults per signal: 2048 records including the in-flight batch, batches of 128, a 1000 ms schedule, 5000 ms export timeout, and 10000 ms flush/shutdown deadlines. Override with queue. Positive integer limits are required, batch size cannot exceed capacity and flush/shutdown deadlines cannot be shorter than the export timeout. One batch is in flight per signal. The official exporter retries retryable OTLP responses within its export deadline; permanent rejections and exhausted retries are reported and dropped. This is a bounded memory queue, without disk persistence.

await agentrail.flush() drains capture within the configured bound. await agentrail.shutdown() stops instrumentation and drains once; call it during application shutdown. diagnostics() reports accepted, exported, dropped and pending records per signal. Overflow, export failure, deadline loss, refused handoff and missing capture context go to onError (console by default). SDK error reporting runs outside capture, including when routed through the same Pino or OTel logger, so diagnostics cannot feed their own full queue. If a synchronous onError callback throws, its exception and the original capture error are reported to the console outside capture, and queue cleanup continues. These capture outcomes do not replace an application response. Flush completion means attempts ended; inspect diagnostics/errors for delivery loss.

MCP callback adapter

import { McpServer, type ToolCallback } from '@modelcontextprotocol/server';
import { mcp } from 'agentrail-node/mcp';
const server = new McpServer({ name: 'orders', version: '1' });
const capture = mcp({ client: agentrail });
const checkout: ToolCallback = async (ctx) => {
  // Authentication happens in the application's HTTP/auth boundary.
  return { content: [{ type: 'text', text: 'done' }] };
};
server.registerTool('checkout', {}, capture.tool('checkout', checkout));
// capture.resource(registeredName, handler) and capture.prompt(registeredName, handler)
// wrap their respective public registration callbacks too.

The optional adapter targets @modelcontextprotocol/server 2.3.1 and public registration callbacks, without modifying private handlers or transports. Use a callback typed with the SDK's schema, for example ToolCallback<typeof inputSchema>, for typed tool arguments. With HTTP capture installed, each execution becomes a nested MCP step with a distinct span, exact log correlation and inherited application-verified identity. Outside an HTTP request, it starts its own managed operation. It captures the registered operation name, method, client name/version, protocol and error outcome; arguments, result text, resource URIs and prompt content stay off. Pass a stable registered resource name rather than the caller's URI.

MCP 2026-07-28 metadata comes from the request's public context envelope. Legacy 2025-11-25 HTTP calls retain protocol metadata; a stateless server may have no client name after its discarded initialize request. The adapter keeps no initialize cache across callers. Client info and supplied trace metadata remain untrusted and cannot assert a principal. isError results mark the operation as failed while preserving the original result. Thrown application errors retain their original identity. Tests exercise modern tool/resource/prompt requests and the official client's legacy HTTP exchange; other MCP SDK versions/transports have not been verified in this release.