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

agent-telemetry

v0.10.0

Published

Lightweight JSONL telemetry for AI agent backends. Zero deps, framework adapters included.

Readme

agent-telemetry

Lightweight JSONL telemetry for easier AI agent consumption. Zero runtime dependencies.

Writes structured telemetry events to rotating JSONL files in development. Falls back to console.log in runtimes without filesystem access (Cloudflare Workers). Includes framework adapters for Hono, Inngest, Express, Fastify, Next.js, Prisma, Supabase, a generic traced fetch wrapper, and browser trace-context helpers.

Spec Conformance

This package implements the Agent Telemetry Specification v1.

| Profile | Status | |---------|--------| | Core | Conformant | | File | Conformant | | Browser | Conformant | | Async | Conformant | | Rotation | Conformant | | Agent Consumption | Conformant | | OTel Projection | Not claimed (planned post-1.0) |

Roles: producer, writer, consumer

All emitted records include record_type: "event", spec_version: 1, and an ISO 8601 timestamp. Field names use snake_case to match the spec wire format. The _trace continuation envelope uses the W3C traceparent string format.

Install

bun add agent-telemetry

Node.js users: This package ships TypeScript source (no build step). You'll need a bundler that handles .ts imports (esbuild, tsup, Vite, etc.).

Demo App

This repo includes a runnable browser-to-backend demo in demo/.

bun run demo

Then open http://localhost:3001 and click Run Demo Request. Telemetry is written to .agent-telemetry/{session}/server-{pid}.jsonl by default. The demo emits correlated http.request, db.query, and external.call events. The demo page includes a built-in Recent Telemetry panel and a temporal trace timeline view, and you can follow logs with bun run demo:tail. The timeline uses configurable synthetic delays so spans are easier to distinguish visually. The demo binds to 127.0.0.1 by default; set DEMO_HOST=0.0.0.0 if you explicitly want LAN access.

Quick Start

import { createTelemetry, type PresetEvents } from 'agent-telemetry'

// createTelemetry is async (one-time runtime probe). The returned emit() is synchronous.
const telemetry = await createTelemetry<PresetEvents>()

telemetry.emit({
  kind: 'http.request',
  trace_id: generateTraceId(),
  span_id: generateSpanId(),
  method: 'GET',
  path: '/api/health',
  status_code: 200,
  outcome: 'success',
  duration_ms: 12,
})

Each call to emit() appends a JSON line to .agent-telemetry/{session}/server-{pid}.jsonl with auto-injected record_type, spec_version, and timestamp:

{"kind":"http.request","trace_id":"0a1b2c3d4e5f67890a1b2c3d4e5f6789","span_id":"a1b2c3d4e5f67890","method":"GET","path":"/api/health","status_code":200,"outcome":"success","duration_ms":12,"record_type":"event","spec_version":1,"timestamp":"2026-02-24T21:00:00.000Z"}

How It Works

The library connects every layer of your stack through a shared trace_id:

Inbound HTTP  ->  Database Queries  ->  External API Calls  ->  Background Jobs
  Hono            Prisma               Traced Fetch           Inngest
  Express         Supabase (PostgREST)  Supabase (auth/
  Fastify                                storage/functions)
  Next.js

One trace_id follows a request from the HTTP boundary through database queries, external API calls, and into background job execution. span_id/parent_span_id fields preserve parent-child relationships inside that trace. HTTP adapters use the W3C traceparent header for propagation, enabling interop with OpenTelemetry and other standards-compliant tools. Query your JSONL logs by trace_id to see the full chain.

Full-Stack Example

Create one telemetry instance and share it across adapters:

// lib/telemetry.ts
import { createTelemetry, type PresetEvents } from 'agent-telemetry'

export const telemetry = await createTelemetry<PresetEvents>()
// server.ts
import { Hono } from 'hono'
import { Inngest } from 'inngest'
import { createHonoTrace, getTraceContext } from 'agent-telemetry/hono'
import { createInngestTrace } from 'agent-telemetry/inngest'
import { telemetry } from './lib/telemetry'

// --- Background job tracing (define client before use) ---
const inngestTrace = createInngestTrace({
  telemetry,
  entityKeys: ['userId'],
})

const inngest = new Inngest({ id: 'my-app', middleware: [inngestTrace] })

// --- HTTP tracing ---
const trace = createHonoTrace({
  telemetry,
  entityPatterns: [
    { segment: 'users', key: 'userId' },
  ],
})

const app = new Hono()
app.use('*', trace)

// Propagate trace context into background job dispatch
app.post('/api/users/:id/process', async (c) => {
  await inngest.send({
    name: 'app/user.process',
    data: { userId: c.req.param('id'), ...getTraceContext(c) },
  })
  return c.json({ ok: true })
})

The getTraceContext(c) call spreads { _trace: { traceparent: "00-..." } } into the dispatch payload. The Inngest middleware reads _trace.traceparent on the receiving end to continue the trace.

This produces a correlated trace:

{"kind":"http.request","trace_id":"aabb...","span_id":"cc11...","method":"POST","path":"/api/users/550e8400-e29b-41d4-a716-446655440000/process","status_code":200,"outcome":"success","duration_ms":45,"entities":{"userId":"550e8400-..."},"record_type":"event","spec_version":1,"timestamp":"..."}
{"kind":"job.dispatch","trace_id":"aabb...","span_id":"dd11...","parent_span_id":"cc11...","task_name":"app/user.process","outcome":"success","record_type":"event","spec_version":1,"timestamp":"..."}
{"kind":"job.start","trace_id":"aabb...","span_id":"ee22...","parent_span_id":"dd11...","task_name":"my-app/process-user","task_id":"run-abc","record_type":"event","spec_version":1,"timestamp":"..."}
{"kind":"job.end","trace_id":"aabb...","span_id":"ee22...","task_name":"my-app/process-user","task_id":"run-abc","duration_ms":230,"outcome":"success","record_type":"event","spec_version":1,"timestamp":"..."}

All four events share the same trace_id. Filter with jq 'select(.trace_id == "aabb...")' to see the full chain.

Custom Events

Extend the type system with your own event kinds:

import { createTelemetry, type HttpEvents, type JobEvents } from 'agent-telemetry'

type MyEvents = HttpEvents | JobEvents | {
  kind: 'custom.checkout'
  trace_id: string
  span_id: string
  orderId: string
  amount: number
}

const telemetry = await createTelemetry<MyEvents>()

telemetry.emit({
  kind: 'custom.checkout',
  trace_id: 'abc'.repeat(10) + 'ab',
  span_id: 'def'.repeat(5) + 'd',
  orderId: 'order-abc',
  amount: 4999,
})

Custom event kinds must use custom.* prefix (e.g. custom.checkout, custom.cache_hit).

Hono Adapter

import { createHonoTrace, getTraceContext } from 'agent-telemetry/hono'

const trace = createHonoTrace({
  telemetry,
  entityPatterns: [            // Extract entity IDs from URL path segments
    { segment: 'users', key: 'userId' },
    { segment: 'posts', key: 'postId' },
  ],
  sanitizePath: (path) =>      // Optional: sanitize paths before emission
    path.replace(/[0-9a-f-]{36}/gi, ':id'),
  isEnabled: () => true,       // Guard function (default: () => true)
})

app.use('*', trace)

The middleware:

  • Parses the incoming W3C traceparent header, or generates a fresh trace ID if absent/invalid
  • Sets traceparent on the response for client-side correlation (format: 00-{traceId}-{spanId}-01)
  • Emits http.request events with method, path, status_code, outcome, duration, extracted entities, and span linkage (span_id, parent_span_id)
  • Extracts entity IDs from URL paths -- looks for a matching segment, then checks if the next segment is a UUID
  • outcome is "error" for HTTP 5xx, "success" otherwise

getTraceContext(c) returns { _trace: { traceparent: "00-..." } } for spreading into dispatch payloads. Returns {} if no trace middleware is active.

Inngest Adapter

import { createInngestTrace } from 'agent-telemetry/inngest'

const trace = createInngestTrace({
  telemetry,
  name: 'my-app/trace',               // Middleware name (default: 'agent-telemetry/trace')
  entityKeys: ['userId', 'orderId'],   // Keys to extract from event.data (default: [])
})

const inngest = new Inngest({ id: 'my-app', middleware: [trace] })

The middleware:

  • Emits job.start and job.end events for function lifecycle (with duration and error tracking)
  • Emits job.dispatch events for outgoing event sends (with outcome: "success")
  • Reads trace context from _trace.traceparent in event.data (set by getTraceContext() at the dispatch site)
  • Generates a new trace_id when no _trace is present, so every function run is always traceable
  • Uses spec field names: task_name (function ID), task_id (run ID)

Fetch Adapter

Wraps any fetch call with telemetry. Does not monkey-patch the global -- returns a new function with identical semantics.

import { createTracedFetch } from 'agent-telemetry/fetch'

const fetch = createTracedFetch({
  telemetry,
  baseFetch: globalThis.fetch,       // Optional -- default: globalThis.fetch
  getTraceContext: () => ctx,         // Optional -- correlate with parent request
  propagateTo: (url) => url.origin === 'https://api.my-app.com', // Optional header allowlist
  onResponseTraceparent: (tp) => {    // Optional response callback
    console.log(tp)
  },
  isEnabled: () => true,             // Optional guard
})

const res = await fetch('https://api.stripe.com/v1/charges', { method: 'POST' })
  • Emits external.call events with service (hostname), operation (METHOD /pathname), and span linkage (span_id, optional parent_span_id)
  • duration_ms measures time-to-headers (TTFB) -- the Response body is returned untouched for streaming
  • Handles all three fetch input types: string, URL, Request
  • Can inject outbound traceparent headers using propagateTo (default: same-origin only in browser, disabled elsewhere)
  • HTTP 5xx responses get outcome: "error"; 1xx-4xx get outcome: "success"
  • Network errors re-throw after emitting with outcome: "error"

Browser Trace Context

The browser module connects client-side user actions to server-side traces through a three-step flow:

  1. Server renders a meta tag with the current traceparent during SSR/page render
  2. Browser bootstraps from that meta tag, inheriting the same trace
  3. Traced fetch injects traceparent on outgoing requests, closing the loop

Step 1: Server-Side -- Inject the Meta Tag

Your page handler renders a <meta> tag containing the current trace context. This bridges the server render and client-side JavaScript.

// server.ts (Hono example -- same pattern works with Express/Fastify/Next.js)
import { createHonoTrace, getTraceContext } from 'agent-telemetry/hono'
import { formatTraceparent, parseTraceparent } from 'agent-telemetry'

app.use('*', createHonoTrace({ telemetry }))

app.get('/', (c) => {
  // getTraceContext returns { _trace: { traceparent: "00-..." } }
  const ctx = getTraceContext(c)
  const traceparent = '_trace' in ctx ? ctx._trace.traceparent : ''

  return c.html(`<!doctype html>
<html>
<head>
  <meta name="agent-telemetry-traceparent" content="${traceparent}" />
</head>
<body>
  <div id="app"></div>
  <script type="module" src="/client.js"></script>
</body>
</html>`)
})

Step 2: Browser Bootstrap

The browser reads the meta tag automatically. No manual parsing needed -- createBrowserTraceContext() checks <meta name="agent-telemetry-traceparent"> by default.

// client.ts
import { createBrowserTraceContext, createBrowserTracedFetch } from 'agent-telemetry/browser'

// Bootstraps from <meta name="agent-telemetry-traceparent"> automatically
const trace = createBrowserTraceContext()

// Wrap fetch to inject traceparent on same-origin requests
const tracedFetch = createBrowserTracedFetch({
  trace,
  propagateTo: (url) => url.origin === window.location.origin,
})

Step 3: Traced API Calls

Every tracedFetch call sends the traceparent header. The server adapter picks it up and continues the same trace.

// Simple fetch -- traceparent injected automatically
const response = await tracedFetch('/api/users/123')

// Group multiple calls under a named span
const result = await trace.withSpan('checkout', async (ctx) => {
  // Both calls share the same parent span
  const cart = await tracedFetch('/api/cart')
  const order = await tracedFetch('/api/orders', {
    method: 'POST',
    body: JSON.stringify({ items: await cart.json() }),
  })
  return order.json()
})

What the Trace Looks Like

A single user action produces a connected trace across browser and server:

Browser                          Server
-------                          ------
[page load]
  meta tag: 00-{traceId}-{spanA}-01
                                 GET / -> http.request (spanA)

[user clicks "checkout"]
  withSpan("checkout")
    tracedFetch /api/cart ------> GET /api/cart -> http.request (spanB, parent=spanA)
                                   db.query (spanC, parent=spanB)
    tracedFetch /api/orders ----> POST /api/orders -> http.request (spanD, parent=spanA)
                                   db.query (spanE, parent=spanD)
                                   external.call (spanF, parent=spanD)

All spans share one trace_id. Query with jq 'select(.trace_id == "...")' to see the full chain from page render through checkout.

API Reference

  • createBrowserTraceContext(options?) -- creates the trace context manager

    • Bootstraps from: initialTraceparent option -> <meta name="agent-telemetry-traceparent"> -> fresh IDs
    • trace.getTraceparent() returns the current W3C traceparent string
    • trace.withSpan(name, fn) creates a child span, restores the parent after completion
    • Custom meta tag name: pass metaName: "my-custom-name" for backwards compatibility
  • createBrowserTracedFetch(options?) -- wraps fetch with trace propagation

    • propagateTo controls which origins receive the traceparent header (default: same-origin only)
    • Response adoption is disabled by default -- set updateContextFromResponse: true to enable
    • Does not emit telemetry events (the server adapter handles that)

Prisma Adapter

Traces all Prisma model operations via $extends(). No runtime @prisma/client import -- the extension is structurally compatible.

import { createPrismaTrace } from 'agent-telemetry/prisma'

const prisma = new PrismaClient().$extends(createPrismaTrace({
  telemetry,
  getTraceContext: () => ctx,         // Optional -- correlate with parent request
  isEnabled: () => true,             // Optional guard
}))
  • Emits db.query events with provider: "prisma", model (e.g. "User"), operation (e.g. "findMany"), and span linkage (span_id, optional parent_span_id)
  • Requires Prisma 5.0.0+ (stable $extends API)
  • No access to raw SQL at the query extension level -- model and operation names only

Express Adapter

Standard Express middleware with the same tracing pattern as Hono. No express or @types/express runtime dependency.

import { createExpressTrace, getTraceContext } from 'agent-telemetry/express'

app.use(createExpressTrace({
  telemetry,
  entityPatterns: [
    { segment: 'users', key: 'userId' },
  ],
  sanitizePath: (path) => path.replace(/[0-9a-f-]{36}/gi, ':id'),
  isEnabled: () => true,
}))

app.post('/api/users/:id', (req, res) => {
  // Propagate trace context to downstream services
  const ctx = getTraceContext(req)
  // ctx = { _trace: { traceparent: "00-..." } }
  res.json({ ok: true })
})
  • Emits http.request events with method, path (query string stripped), status_code, outcome, duration, entities, and span linkage
  • Parses/sets W3C traceparent header for propagation
  • Uses req.route.path for parameterized patterns (e.g. /users/:id), falls back to req.originalUrl
  • Handles both res.on("finish") and res.on("close") to capture aborted requests

Fastify Adapter

Fastify plugin using onRequest/onResponse hooks. No fastify runtime dependency -- uses Symbol.for("skip-override") instead of fastify-plugin.

import { createFastifyTrace, getTraceContext } from 'agent-telemetry/fastify'

app.register(createFastifyTrace({
  telemetry,
  entityPatterns: [
    { segment: 'users', key: 'userId' },
  ],
  sanitizePath: (path) => path.replace(/[0-9a-f-]{36}/gi, ':id'),
  isEnabled: () => true,
}))
  • Emits http.request events using reply.elapsedTime for high-resolution duration, including span linkage
  • Strips query strings from emitted path values
  • Parses/sets W3C traceparent header for propagation
  • Uses request.routeOptions.url for parameterized route patterns
  • Requires Fastify 4.0.0+ (reply.elapsedTime not available in 3.x)

Next.js Adapter

Next.js middleware and route handlers run in separate execution contexts, so tracing is split into two pieces: middleware handles trace propagation, route handler wrappers handle timing and event emission. No next runtime dependency.

Middleware -- injects traceparent into request headers for downstream route handlers:

// middleware.ts
import { createNextMiddleware } from 'agent-telemetry/next'

const traceMiddleware = createNextMiddleware()

export function middleware(request: NextRequest) {
  return traceMiddleware(request)
}

Route handlers -- reads traceparent, measures duration, emits http.request events:

// app/api/users/route.ts
import { withNextTrace } from 'agent-telemetry/next'

export const GET = withNextTrace(async (request) => {
  const users = await db.query('SELECT * FROM users')
  return Response.json(users)
}, { telemetry })

Server Actions -- wraps actions with method: "ACTION" events:

// app/actions.ts
'use server'
import { withActionTrace } from 'agent-telemetry/next'

export const createPost = withActionTrace(async (formData: FormData) => {
  // ...
}, { telemetry, name: 'createPost' })
  • createNextMiddleware() parses incoming traceparent (or generates fresh IDs), creates a child span, and forwards the new traceparent via NextResponse.next({ request: { headers } })
  • withNextTrace(handler, options) reads the propagated traceparent, times the handler with performance.now(), and emits http.request with method, path, status_code, outcome, duration, entities, and span linkage
  • withActionTrace(action, options) creates a standalone span and emits events with method: "ACTION" and path: actionName
  • getTraceContext(request) parses traceparent from request headers and returns { _trace: { traceparent: "00-..." } } for passing to fetch/prisma/supabase adapters
  • Supports entityPatterns, sanitizePath, and isEnabled options on route handler wrappers (same as other HTTP adapters)
  • Uses only Web APIs (Headers, Response, performance.now) -- works in both Node and Edge runtimes

Supabase Adapter

A traced fetch that parses Supabase URL patterns to emit rich, service-aware telemetry. PostgREST calls become db.query events; auth/storage/functions calls become external.call events.

import { createClient } from '@supabase/supabase-js'
import { createSupabaseTrace } from 'agent-telemetry/supabase'

const tracedFetch = createSupabaseTrace({ telemetry })
const supabase = createClient(url, key, { global: { fetch: tracedFetch } })

URL pattern classification:

| Pattern | Event | Fields | |---------|-------|--------| | /rest/v{N}/{table} | db.query | model: table, operation: select\|insert\|update\|delete | | /auth/v{N}/{endpoint} | external.call | service: "supabase-auth" | | /storage/v{N}/object/{bucket} | external.call | service: "supabase-storage" | | /functions/v{N}/{name} | external.call | service: "supabase-functions" |

  • Each fetch invocation emits one event -- Supabase's built-in retry logic generates separate events per retry
  • Realtime (WebSocket) subscriptions are not intercepted (they don't use fetch)
  • external.call events use outcome: "error" for HTTP 5xx; db.query events use outcome: "error" for any non-2xx (PostgREST errors are query failures)

Consumer

The consumer module parses JSONL telemetry files and produces canonical trace summaries for AI agent consumption.

import {
  processTelemetry,
  processTelemetryDir,
  type TraceSummary,
} from 'agent-telemetry/consumer'

// From a string
const result = processTelemetry(jsonlContent)

// From a directory of .jsonl files
const result = await processTelemetryDir('.agent-telemetry/my-session/')

for (const summary of result.summaries) {
  console.log(summary.trace_id, summary.event_count)
  console.log(summary.uncertainties) // data quality signals
  console.log(summary.entities)      // aggregated entity values
}

The consumer pipeline runs six stages: parse (JSONL lines) -> validate (record_type, spec_version, known kinds) -> normalize (field truncation) -> reconstruct (span trees from trace_id/span_id/parent_span_id) -> uncertainty (data quality annotations) -> summary (canonical output with trust classification).

Each TraceSummary includes:

  • events with per-field trust classification (system_asserted, untrusted_input, derived, unknown)
  • uncertainties for data quality signals (malformed lines, missing parent spans, writer fallbacks)
  • entities aggregated across all events in the trace
  • Control characters escaped for prompt safety

Lower-level APIs are also exported: parseLine, parseContent, parseFile, parseDirectory, reconstructTraces, buildEntityIndex, lookupEntity, classifyTrust, escapeControlChars.

Configuration

const telemetry = await createTelemetry({
  logDir: '.agent-telemetry/my-session', // Directory for log files (default: auto-discovered)
  filename: 'telemetry.jsonl',           // Log filename (default: '{role}-{pid}.jsonl')
  maxSize: 5_000_000,                    // Max file size before rotation (default: 5MB)
  maxBackups: 3,                         // Number of rotated backups (default: 3)
  maxRecordSize: 1_048_576,              // Max record size before dropping (default: 1MB)
  prefix: '[TEL]',                       // Console fallback prefix (default: '[TEL]')
  isEnabled: () => true,                 // Guard function (default: () => true)
  sessionId: 'my-session',              // Session ID for directory structure
  role: 'worker',                        // Role identifier for filename (default: 'server')
  sanitizePath: (p) => p.replace(/[0-9a-f-]{36}/gi, ':id'), // Path sanitizer
})

Output path discovery order:

  1. Explicit logDir + filename config
  2. AGENT_TELEMETRY_FILE environment variable (single-file mode)
  3. AGENT_TELEMETRY_DIR environment variable
  4. {project_root}/.agent-telemetry/{session_id}/{role}-{pid}.jsonl (auto-discovered)

Project root is detected by walking up from cwd() looking for .git, package.json, or deno.json.

The .agent-telemetry/ directory is automatically added to .gitignore and created with restricted permissions (0o700 directory, 0o600 files on POSIX).

When isEnabled returns false, emit() is a no-op. Useful for environment-based guards:

const telemetry = await createTelemetry({
  isEnabled: () => process.env.NODE_ENV === 'development',
})

Field Truncation

Fields are automatically truncated to spec-defined byte limits before emission. Truncation is UTF-8 safe (never splits multi-byte characters) and appends ...[truncated] as a suffix.

Key limits: kind (64 bytes), path (1024 bytes), error_name (120 bytes), most other string fields (256 bytes). trace_id, span_id, and parent_span_id are never truncated.

Entity keys are limited to 64 bytes, entity values to 256 bytes.

Preset Event Types

| Type | Events | Description | |------|--------|-------------| | HttpEvents | http.request | HTTP request/response telemetry | | JobEvents | job.start, job.end, job.dispatch | Background job lifecycle | | ExternalEvents | external.call | External service calls | | DbEvents | db.query | Database query telemetry | | SupabaseEvents | db.query, external.call | Supabase-specific union | | PresetEvents | All of the above | Combined preset union |

Utilities

import {
  generateTraceId,
  generateSpanId,
  extractEntities,
  extractEntitiesFromEvent,
  formatTraceparent,
  parseTraceparent,
  truncateField,
} from 'agent-telemetry'

generateTraceId()  // -> '0a1b2c3d4e5f67890a1b2c3d4e5f6789' (32 hex chars)
generateSpanId()   // -> '0a1b2c3d4e5f6789' (16 hex chars)

// Format a W3C traceparent header
formatTraceparent(traceId, spanId, '01')
// -> '00-0a1b2c3d4e5f67890a1b2c3d4e5f6789-0a1b2c3d4e5f6789-01'

// Parse a traceparent header
parseTraceparent('00-0a1b...6789-0a1b...6789-01')
// -> { version: '00', traceId: '0a1b...', parentId: '0a1b...', traceFlags: '01' }

// Extract entity IDs from URL paths (matches UUID segments only)
extractEntities('/api/users/550e8400-e29b-41d4-a716-446655440000/posts/6ba7b810-9dad-11d1-80b4-00c04fd430c8', [
  { segment: 'users', key: 'userId' },
  { segment: 'posts', key: 'postId' },
])
// -> { userId: '550e8400-...', postId: '6ba7b810-...' }

extractEntities('/api/users/john', [{ segment: 'users', key: 'userId' }])
// -> undefined (non-UUID values are skipped)

// Extract entity IDs from event data objects
extractEntitiesFromEvent({ userId: 'abc', count: 5 }, ['userId', 'postId'])
// -> { userId: 'abc' }

// UTF-8 safe field truncation
truncateField('very long string...', 32)

Runtime Detection

The writer automatically detects the runtime environment:

| Runtime | Behavior | |---------|----------| | Bun / Node.js | Writes to filesystem with size-based rotation | | Cloudflare Workers | Falls back to console.log with [TEL] prefix |

Detection happens once during createTelemetry() -- it probes the filesystem by creating the log directory and verifying it exists. Cloudflare's nodejs_compat stubs succeed silently on mkdirSync but fail the existence check, triggering the console fallback.

The returned emit() function is synchronous, non-blocking, and never throws, even with malformed data or filesystem errors. Telemetry must not crash the host application.

Contract Pack

The contracts/agent-telemetry/v1/ directory contains machine-readable contract artifacts for the spec:

  • JSON Schemas for all event kinds, diagnostics, and trace summaries
  • Field byte limits, enums, and regex patterns
  • A glossary with field semantic descriptions
  • Negative test vectors for conformance testing
  • A manifest with SHA-256 hashes for integrity verification

Migrating from 0.5.x

If you're upgrading from agent-telemetry 0.5.x, the following breaking changes apply:

1. _trace envelope format

The _trace continuation envelope now uses a W3C traceparent string instead of decomposed fields.

// Before (0.5.x)
getTraceContext(c) // -> { _trace: { trace_id: "...", parent_span_id: "...", trace_flags: "01" } }

// After (0.6.0+)
getTraceContext(c) // -> { _trace: { traceparent: "00-{traceId}-{spanId}-01" } }

2. Job event field renames

Job events use spec-standard field names:

| 0.5.x field | 1.0 field | |-------------|-----------| | function_id | task_name | | run_id | task_id | | event_name | task_name |

job.dispatch events now always include an outcome field. New optional fields: queue, attempt.

3. external.call outcome for 5xx

The fetch adapter now sets outcome: "error" for HTTP 5xx responses. Previously all HTTP responses were outcome: "success".

4. Browser meta tag name

The default meta tag name changed from "traceparent" to "agent-telemetry-traceparent". Update your server-rendered HTML:

<!-- Before -->
<meta name="traceparent" content="00-...">

<!-- After -->
<meta name="agent-telemetry-traceparent" content="00-...">

Or pass metaName: "traceparent" to createBrowserTraceContext() for backwards compatibility.

5. Response adoption default

createBrowserTracedFetch() no longer adopts response traceparent headers by default. Set updateContextFromResponse: true explicitly if needed.

6. File directory structure

Default output path changed from logs/telemetry.jsonl to {project_root}/.agent-telemetry/{session_id}/{role}-{pid}.jsonl. Pass logDir and filename to keep the old behavior:

const telemetry = await createTelemetry({
  logDir: 'logs',
  filename: 'telemetry.jsonl',
})

License

MIT