TypeScript support is easy to claim. Publish an npm package, add a few type declarations, and a tracing product can put “JavaScript and TypeScript” on its integration list.
Native support is a higher bar.
AI applications in the TypeScript ecosystem run inside concurrent Node.js servers, serverless functions, edge runtimes, background workers, test runners, and streaming web frameworks. They cross promise chains, callbacks, tool adapters, async iterators, and package boundaries. A useful tracing tool must fit those execution models without breaking types or producing disconnected spans.
The question is therefore not only, “Does this tool have a TypeScript SDK?” It is, “Does it preserve how a TypeScript agent actually runs?”
The Runtime Is Part of the Product
Consider a small agent:
async function supportAgent(question: string) {
const category = await classifyQuestion(question);
const documents = await retrieveDocuments(question, category);
return generateAnswer(question, documents);
}
Enter fullscreen mode Exit fullscreen mode
The source looks sequential, but a production server may execute hundreds of these functions concurrently. A flat event stream cannot tell which retrieval or model call belongs to which request.
request A: classify -> retrieve -> generate
request B: classify -> retrieve -> generate
Enter fullscreen mode Exit fullscreen mode
The tracer needs a request-level trace ID and a parent span for each nested operation. More importantly, those identifiers must remain available after every asynchronous handoff.
Async Context Must Be Correct Under Concurrency
In Node.js, AsyncLocalStorage is the usual foundation for request-scoped context. It propagates state through normal promise chains and many asynchronous resources, which is much safer than storing a current trace ID in a module-level variable.
import { AsyncLocalStorage } from 'node:async_hooks';
type TraceContext = {
traceId: string;
spanId: string | null;
};
const context = new AsyncLocalStorage<TraceContext>();
export function withTraceContext<T>(
value: TraceContext,
work: () => T,
): T {
return context.run(value, work);
}
export function currentTraceContext(): TraceContext {
const value = context.getStore();
if (!value) throw new Error('No active trace context');
return value;
}
Enter fullscreen mode Exit fullscreen mode
await by itself does not cause context loss when AsyncLocalStorage is used correctly. Problems usually appear when instrumentation relies on global mutable state, registers work outside the active context, crosses an unsupported runtime boundary, or integrates with a library that manages its own scheduling.
A TypeScript tracing library should test at least these cases:
- Multiple agent runs executing concurrently
- Nested tools and model calls
- Timers, event emitters, and queued callbacks
- Detached background work
- Retries that start new asynchronous branches
- Tests running in parallel workers
The acceptance criterion is simple: every span belongs to exactly one trace and has the expected parent.
Streaming Changes the Span Lifecycle
Many AI routes return a stream before generation has finished. The HTTP handler may complete from the framework’s perspective while tokens, tool calls, and usage data are still in flight.
A model span should therefore not end merely because the route returned a Response. It should end when the stream completes, fails, or is cancelled.
type StreamHooks<T> = {
onStart(): Promise<void> | void;
onChunk(chunk: T): Promise<void> | void;
onComplete(): Promise<void> | void;
onError(error: unknown): Promise<void> | void;
onCancel(): Promise<void> | void;
};
async function* traceStream<T>(
source: AsyncIterable<T>,
hooks: StreamHooks<T>,
): AsyncGenerator<T> {
await hooks.onStart();
let completed = false;
try {
for await (const chunk of source) {
await hooks.onChunk(chunk);
yield chunk;
}
completed = true;
await hooks.onComplete();
} catch (error) {
await hooks.onError(error);
throw error;
} finally {
if (!completed) await hooks.onCancel();
}
}
Enter fullscreen mode Exit fullscreen mode
Real integrations also need to avoid double-finalizing a span when an error and cancellation happen close together. They should record time to first chunk, completion status, tool activity, and final token usage without storing every chunk by default.
Streaming support is not a cosmetic feature. Without it, latency is measured incorrectly, cancellations disappear, and partial responses look like successful completions.
Runtime Compatibility Is a Matrix
“TypeScript runtime” can mean several different environments:
| Environment | Important tracing constraint |
|---|---|
| Long-running Node.js service | Async context, concurrency, graceful flush on shutdown |
| Serverless function | Cold starts, short lifetime, bounded flush time |
| Edge runtime | Web APIs, limited or absent Node built-ins |
| Background worker | Detached jobs, queue context propagation, retries |
| Browser | Bundle size, user privacy, no server credentials |
| Test runner | Isolation across files and parallel workers |
A library that imports node:async_hooks or node:fs from its main entry point may fail when bundled for an edge runtime even if those features are never called. Runtime-specific code should live behind explicit exports so bundlers can exclude it.
{
"exports": {
".": "./dist/core.js",
"./node": "./dist/node.js",
"./web": "./dist/web.js"
}
}
Enter fullscreen mode Exit fullscreen mode
The core event model can be portable. Context propagation, persistence, and flush behavior may need runtime-specific implementations.
Framework Hooks Should Follow Real Lifecycles
Framework integrations are valuable when they attach to stable lifecycle hooks rather than patching private internals. For an AI route, useful boundaries include:
- Request accepted and validated
- Agent run started
- Retrieval started and completed
- Tool call requested, executed, retried, or rejected
- Model stream opened, produced its first chunk, and completed
- Client disconnected or cancelled
- Final usage became available
- Trace flush succeeded or timed out
Different frameworks expose these boundaries differently. A TypeScript-native tracer should make the manual API first-class, then add thin adapters for frameworks such as Vercel AI SDK, LangChain.js, or OpenAI Agents SDK. The adapter should translate framework events into one internal trace model instead of forcing the core to depend on every framework.
Type Preservation Is Part of Developer Experience
Instrumentation should not erase the function signature it wraps. A generic wrapper can preserve argument and return types:
type AsyncFunction<Args extends unknown[], Result> = (
...args: Args
) => Promise<Result>;
function withTracing<Args extends unknown[], Result>(
name: string,
fn: AsyncFunction<Args, Result>,
): AsyncFunction<Args, Result> {
return async (...args: Args): Promise<Result> => {
return traceStep(name, () => fn(...args));
};
}
type AgentResult = {
answer: string;
sources: string[];
};
const tracedAgent = withTracing(
'support_agent',
async (question: string): Promise<AgentResult> => {
return runAgent(question);
},
);
const result = await tracedAgent('Where is my order?');
result.answer; // string
result.sources; // string[]
Enter fullscreen mode Exit fullscreen mode
Overloaded functions, methods that depend on this, and streaming return types need more careful adapters. A library should document those boundaries instead of falling back to any.
Strong types also improve trace quality. Tool names, span kinds, metadata fields, and completion states can be controlled unions, which catches instrumentation mistakes before runtime.
ESM, CommonJS, and Bundlers Still Matter
Modern TypeScript packages are consumed through ESM, CommonJS, transpilers, monorepo build systems, and framework bundlers. Tracing libraries are especially sensitive because they often initialize early and integrate across package boundaries.
A production-ready package should make these behaviors clear:
- Which module formats are published and tested
- Whether initialization has side effects
- How duplicate package copies affect global registration
- Whether Node-only modules can be excluded from web and edge bundles
- How source maps affect stack and code-location metadata
- Whether instrumentation works before and after bundling
Automatic monkey-patching can be convenient, but explicit wrappers and adapters are easier to reason about across module systems. If automatic instrumentation is offered, it should be optional and observable.
Vendor-Neutral Events Keep the Core Flexible
TypeScript-native does not need to mean backend-specific. The instrumentation layer can emit a small internal event model, while sinks translate those events to local files, OpenTelemetry, or a hosted platform.
type TraceSink = {
write(event: TraceEvent): Promise<void>;
flush(options?: { timeoutMs?: number }): Promise<void>;
};
Enter fullscreen mode Exit fullscreen mode
This separation lets teams use local traces for development, short-lived artifacts in CI, and centralized observability in production without rewriting every adapter.
It also creates one place to enforce privacy policy. Framework integrations should emit approved metadata into the core; destinations should not decide what sensitive payloads to collect.
How to Evaluate a TypeScript Tracing Tool
Use representative tests rather than a single hello-world script.
| Test | What a passing result looks like |
|---|---|
| Two concurrent requests | Separate trace trees with no mixed spans |
| Nested tool call | Correct parent and timing under the model or agent step |
| Streaming response | First-chunk, completion, error, and cancellation are distinct |
| Serverless invocation | Events flush within a bounded time without delaying every request |
| Edge build | Node-only modules are absent from the bundle |
| Type check | Wrapped functions preserve arguments and results |
| Parallel tests | Trace state is isolated by test and worker |
| Privacy check | Raw prompts and tool payloads are absent by default |
Also inspect failure behavior. Observability should not crash the agent because a sink is unavailable, but silent data loss is not acceptable either. Libraries should expose dropped-event counts, flush failures, and backpressure policy.
A Better Definition of TypeScript-Native
A TypeScript-native tracing tool should be:
- Async-aware: Correct under concurrent and nested execution.
- Stream-aware: Accurate through completion, failure, and cancellation.
- Runtime-aware: Explicit about Node, serverless, edge, browser, and worker support.
- Framework-adaptable: Integrated through stable lifecycle hooks.
-
Type-preserving: Safe wrappers without unnecessary
any. - Module-conscious: Predictable across ESM, CommonJS, and bundlers.
- Privacy-conscious: Metadata-first with explicit payload capture.
- Backend-flexible: Able to send one event model to multiple sinks.
An npm package is only the delivery mechanism. Native support is the accumulated quality of these runtime and developer-experience decisions.
Final Thought
TypeScript agents are asynchronous, streaming, and frequently deployed across more than one runtime. Their observability tools need to understand those constraints as first-class design inputs.
When evaluating a tracer, ask whether it preserves execution context, lifecycle, types, and privacy under the conditions your application actually uses. That is the difference between an SDK that compiles and instrumentation you can trust.
The next article will build the core mechanics directly: a TypeScript execution tree using AsyncLocalStorage, parent-child spans, safe completion handling, and pluggable trace sinks.
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.