How do I trace a TypeScript AI agent running in Vercel functions?
Rius traces a TypeScript agent in a Vercel function when you call init() once at module scope and call await client.flush() at the end of every invocation. The flush matters because the TypeScript SDK installs no process-exit hook, and a serverless runtime freezes the process between invocations, so queued spans would otherwise not leave. Turn the heartbeat off, since a frozen process cannot send it. If your app uses pnpm, you may also need to pass your instrumentation modules to init().
How it works
- Initialize once per process. Put
init()at the top of the module that holds your handler. Module-level code runs once per process, and later invocations that reuse the process reuse the same client. A second globalinit()only logs a warning and returns the existing client. - Spans export in the background. The SDK queues spans and sends them in batches on a timer. A frozen process runs no timer, so a batch that has not been sent yet waits until the process thaws, if it ever does.
- Flush at the invocation boundary.
client.flush()forces the export and returnsfalseif the most recent export failed, sotruemeans your spans actually left. Call it once, as the last step of the invocation, never inside a hot path. - Instrumentation attaches asynchronously.
init()returns at once, andclient.readyresolves with the integrations that attached. Await it before your first model call so that call is traced. - The heartbeat does not fit. The heartbeat is a long-lived-process signal. Between invocations the process is suspended, pings stop, and the instance would flap between alive and
staleorgone. Setheartbeat: false. - pnpm needs extra care. The SDK finds the OpenAI and Anthropic instrumentation by importing it relative to its own files. A file-tracing deployment of a pnpm app may not ship those links, so no model-call spans appear while everything else looks fine.
instrumentModulesfixes that.
Set it up
- Install the SDK and the instrumentation for the client library your agent calls. This example uses Anthropic. For OpenAI, install
@arizeai/openinference-instrumentation-openaiinstead. Details are in Installation and configuration.
npm install @glassflow-ai/rius @opentelemetry/api
npm install @arizeai/openinference-instrumentation-anthropic- Add
RIUS_API_KEY(your key starts withri_) to the environment of your function. The SDK reads it, so it stays out of your code. - Initialize at module scope, wrap the work in a named function, and flush in a
finallyblock so spans are exported even when the run throws:
import Anthropic from '@anthropic-ai/sdk'
import { init, observe } from '@glassflow-ai/rius'
const rius = init({ serviceName: 'my-agent', heartbeat: false })
const claude = new Anthropic() // reads ANTHROPIC_API_KEY
const handle = observe(async function handle(query: string) {
const message = await claude.messages.create({
model: 'claude-haiku-4-5-20251001',
max_tokens: 200,
messages: [{ role: 'user', content: query }],
})
const [block] = message.content
return block.type === 'text' ? block.text : ''
})
export async function run(query: string) {
await rius.ready // auto-instrumentation is attached before the first call
try {
return await handle(query)
} finally {
const delivered = await rius.flush() // export before the function freezes
if (!delivered) console.warn('the most recent span export failed')
}
}- If you deploy a pnpm app and
rius.readyleaves outanthropic, import the modules yourself and hand them toinit(). Your own imports are what file tracing follows. Thesdkmodule must be the copy your client calls go through. This needs@glassflow-ai/rius1.1.0 or later:
import * as anthropicSdk from '@anthropic-ai/sdk'
import * as anthropicInstrumentation from '@arizeai/openinference-instrumentation-anthropic'
import { init } from '@glassflow-ai/rius'
init({
serviceName: 'my-agent',
heartbeat: false,
instrumentModules: {
anthropic: { instrumentation: anthropicInstrumentation, sdk: anthropicSdk },
},
})The Integrations page covers the pnpm case in full. The Reliability page covers flushing, retries and the serverless rules.
What you get
- One trace per invocation, with a span for each model call, its token counts and a cost estimate. Costs are list-price estimates in US dollars.
- Your own
handlefunction as the root of the trace, so model calls sit under a named step. - A clear signal when export fails:
flush()returnsfalseinstead of failing silently. - Optional partial spans. With
partialSpans: true(and apartialSpansDelayto keep volume low), each span sends a content-free snapshot when it starts, so a run that crashes leaves a record of what it was doing.
When something else fits
- On a long-running Node server rather than a function, skip the per-request flush and call
await client.shutdown()on graceful shutdown, for example in aSIGTERMhandler. - If a request is refused with
402or429, flushing cannot help. Check your plan and ingest rate as described in Reliability. - If your agent calls a library with no TypeScript integration, the Integrations page points to manual generations for those calls.
Related
Last updated on