The @raindrop-ai/eve package instruments Vercel’s Eve agent framework using its native agent/instrumentation.ts entry point. No wrapping, no per-call setup — Eve auto-discovers the integration at server startup and every agent turn flows to Raindrop.
What gets tracked:
- Every agent turn as a single Raindrop event, grouped by the Eve session
- Multi-step
streamText / generateText / generateObject calls with full attribute capture
- Tool calls (inputs, outputs, durations, errors)
- Token usage per step and per turn
- Sub-agent dispatches as nested
agent.subagent spans — even across Eve’s V8 sandbox boundary
- Cross-sandbox parent linkage (
raindrop.parent.eventId / sessionId / turnId / turnSequence)
- Model name, finish reason, errors, and full AI SDK telemetry attributes
- Automatic mirroring to the local Workshop daemon during development
Installation
pnpm add @raindrop-ai/eve @vercel/otel @opentelemetry/api @opentelemetry/sdk-trace-base
eve is expected to already be in your agent project. The integration declares eve >= 0.11.0 as an optional peer dependency and reads only Eve’s documented runtime surface, so a single defineRaindropInstrumentation() call works without any per-version configuration.
- Cross-sandbox sub-agent linkage is sourced from the
events["step.started"] session.parent lineage Eve forwards on every dispatch.
- Per-turn dynamic metadata (the
step.started callback — see Identifying Users) is wired through Eve’s events config and runs once per model call.
Quick Start
Drop a single file into your Eve project at agent/instrumentation.ts:
// agent/instrumentation.ts
import { registerOTel } from "@vercel/otel";
import { defineRaindropInstrumentation } from "@raindrop-ai/eve";
export default defineRaindropInstrumentation({
registerOTel,
writeKey: process.env.RAINDROP_WRITE_KEY,
});
That’s it. Eve auto-discovers agent/instrumentation.ts and runs it at server startup before any agent code. Every turn from every session in your agent will land in Raindrop.
How It Works
The package does two things:
-
Implements a
SpanExporter (RaindropEveSpanExporter) that serializes OTel spans to OTLP/HTTP JSON and ships them to https://api.raindrop.ai/v1/traces. It also mirrors every export to a local Workshop daemon when one is reachable (http://localhost:5899/), so the same agent run shows up in raindrop workshop AND hosted Raindrop without changing code.
-
Registers a Raindrop
TelemetryIntegration with Vercel AI SDK v7’s global registry so every streamText / generateText / tool call in your agent becomes a proper Raindrop LLM / tool span — and a track_partial semantic event — instead of opaque AI SDK spans. This is what hydrates the Workshop Overview tab and the hosted Raindrop event feed. All AI SDK calls within one Eve turn are grouped under a single eventId derived from Eve’s active OTel trace, so a single agent turn shows up as a single Raindrop event in both UIs.
For users who’d rather wire @vercel/otel themselves, RaindropEveSpanExporter is exported separately for use with any registerOTel({ traceExporter }) call.
Sub-Agents
Eve runs each sub-agent in its own V8 sandbox. The integration detects when the current sandbox was dispatched as a sub-agent and lifts the parent’s turn identity onto every sub-agent event, sourcing the lineage from the events["step.started"] session.parent Eve forwards on dispatch:
| Attribute | Description |
|---|
raindrop.agent.role | "subagent" when the current sandbox is a sub-agent dispatched by another agent; "root" otherwise. |
raindrop.subagent.name | The sub-agent’s agentName (e.g. weatherResearcher). |
raindrop.parent.sessionId | The parent agent’s (bare) Eve session id. |
raindrop.parent.eventId | The cross-sandbox link key. The dispatching parent turn’s per-turn raindrop.eventId (the eve:<sessionId>:<turnId> composite); the dashboard stitches a sub-agent under its parent by matching this against that turn’s own raindrop.eventId. |
raindrop.parent.turnId | The parent’s turn id that dispatched this sub-agent. |
raindrop.parent.turnSequence | The parent’s turn sequence number. |
These attributes power the AGENT block in Workshop’s Overview tab and let the Raindrop dashboard stitch sub-agent events under the parent turn that dispatched them (matching on raindrop.parent.eventId).
Configuration
defineRaindropInstrumentation({
registerOTel,
writeKey: process.env.RAINDROP_WRITE_KEY,
endpoint: "https://api.raindrop.ai/v1/", // optional, default
localWorkshopUrl: undefined, // optional, see below
serviceName: "support-agent", // optional, defaults to the Eve agent name
staticMetadata: { team: "support", tier: "prod" }, // optional, static, attached to every event
events: { // optional, per-step callback
"step.started"(input) {
return {
runtimeContext: {
"raindrop.userId": String(input.session.id),
},
};
},
},
recordInputs: true, // optional, forwarded to Eve
recordOutputs: true, // optional, forwarded to Eve
debug: false, // optional, or set RAINDROP_AI_DEBUG=1
});
Two ways to attach metadata:
| Field | When it runs | Use it for |
|---|
staticMetadata | Set once at startup | Values that are the same on every event — team, tier, environment, or a fixed raindrop.userId. |
events["step.started"] | Eve callback, runs once per model call | Values that change per turn — e.g. the user who triggered this request. Returns { runtimeContext }. |
Both feed the same event fields — the events callback just lets you compute them per turn.
Identifying Users
Raindrop sets each event’s user and conversation from two reserved keys: raindrop.userId and raindrop.convoId. Put them on staticMetadata if they never change, or return them from the events["step.started"] callback to compute them per turn.
Same user every time — put the keys on staticMetadata:
defineRaindropInstrumentation({
registerOTel,
writeKey: process.env.RAINDROP_WRITE_KEY,
staticMetadata: {
"raindrop.userId": "user_123",
"raindrop.convoId": "convo_456",
team: "support",
},
});
Different user per turn — return the keys from the events["step.started"] callback. Eve runs it once per model call with the live input, so you can derive identity from the request that triggered the turn — e.g. map the Slack user onto the Raindrop event userId:
defineRaindropInstrumentation({
registerOTel,
writeKey: process.env.RAINDROP_WRITE_KEY,
staticMetadata: { app: "clay-data-agent", team: "data" },
events: {
"step.started"(input) {
const m = input.channel.metadata;
return {
runtimeContext: {
// reserved keys -> Raindrop event identity
"raindrop.userId": m.triggeringUserId, // becomes the event userId
"raindrop.convoId": m.threadTs, // becomes the event convoId
// any other key -> Raindrop event properties
"slack.user_id": m.triggeringUserId,
"slack.channel_id": m.channelId,
"slack.team_id": m.teamId,
"slack.thread_ts": m.threadTs,
},
};
},
},
});
How the keys map onto the event:
raindrop.userId → the event’s userId; raindrop.convoId → the event’s convoId.
- Every other key → the event’s
properties.
The same values are also attached to the AI SDK spans (as ai.settings.context.*).
When the same key is set in more than one place, the most specific value wins: per-call AI SDK metadata > the events["step.started"] callback > staticMetadata. If no raindrop.userId is set anywhere, Raindrop falls back to the Eve session.id.
Using the Slack user’s name / email / channel name
Whatever string you return as raindrop.userId becomes the event’s userId — so to make Raindrop track e.g. the Slack email instead of the user id, just return that string. What’s reachable from step.started:
- User id (
Uxxxx) — directly on input.channel.metadata.triggeringUserId.
- User’s name — already resolved, no API call: read
input.session.auth.current?.attributes and use full_name / user_name (Eve’s default Slack auth puts them there).
- Email / channel name — not in any Slack mention payload, so resolve them once on the async inbound side (
onAppMention) via the Slack Web API (users.info → user.profile.email, needs the users:read.email scope; conversations.info → channel.name), stash them on the session auth attributes, then read them back in step.started (which is synchronous and can’t call Slack itself):
// agent/channels/slack.ts — runs on the inbound webhook (async ok)
export default slackChannel({
async onAppMention(ctx, message) {
const auth = defaultSlackAuth(message, ctx);
const who = await ctx.slack.request("users.info", { user: message.author.userId });
return auth && {
auth: { ...auth, attributes: { ...auth.attributes, email: who?.user?.profile?.email ?? "" } },
};
},
});
// instrumentation.ts — synchronous, just reads what you stashed
"step.started"(input) {
const a = input.session.auth.current?.attributes ?? {};
return { runtimeContext: { "raindrop.userId": String(a.email || a.user_id || "") } }; // userId will be the email
},
Workshop / Production Mirroring
localWorkshopUrl controls Workshop mirroring:
| Value | Behavior |
|---|
string | Force-enable; mirror every export to that URL. |
false | Opt out entirely (skip env vars and auto-detect). |
null | Same as false. |
undefined (default) | Honor RAINDROP_WORKSHOP / RAINDROP_LOCAL_DEBUGGER, then auto-detect localhost during development. |
Run raindrop workshop locally to get a daemon at http://localhost:5899 — the integration starts mirroring as soon as the daemon is reachable.
Production-Only Mode
defineRaindropInstrumentation({
registerOTel,
writeKey: process.env.RAINDROP_WRITE_KEY,
localWorkshopUrl: false,
});
Workshop-Only Mode (no Raindrop account)
Leave writeKey undefined. Spans still flow to Workshop:
defineRaindropInstrumentation({
registerOTel,
localWorkshopUrl: "http://localhost:5899/v1/",
});
Captured Trace Hierarchy
A typical Eve turn produces this trace shape:
ai.eve.turn // session / turn metadata
ai.streamText // step
ai.streamText.doStream // model call
ai.toolCall { toolName: get_weather } // tool exec
ai.streamText
When a sub-agent is dispatched, the sub-agent’s sandbox opens its own OTel trace. The integration tags the sub-agent’s events with raindrop.parent.eventId (plus sessionId / turnId / turnSequence) so the Raindrop dashboard renders them nested under the parent’s turn — matching on raindrop.parent.eventId — even though they are on a different OTel trace.
Captured Properties
The following properties land on every event:
| Property | Description |
|---|
eve.session.id | Eve session id. |
eve.turn.id | Eve turn id (one per assistant response). |
eve.turn.sequence | Monotonic turn counter within the session. |
raindrop.agent.role | "root" or "subagent". |
raindrop.subagent.name | Sub-agent agentName (sub-agent turns only). |
raindrop.parent.sessionId | Parent’s (bare) session id (sub-agent turns only). |
raindrop.parent.eventId | Parent turn’s raindrop.eventId link key, eve:<sessionId>:<turnId> (sub-agent turns only). |
raindrop.parent.turnId | Parent’s dispatching turn id (sub-agent turns only). |
raindrop.parent.turnSequence | Parent’s dispatching turn sequence (sub-agent turns only). |
ai.usage.prompt_tokens | Aggregate input token count for the turn. |
ai.usage.completion_tokens | Aggregate output token count for the turn. |
Any keys you set on staticMetadata (or return from the events["step.started"] callback) are also forwarded to event.properties, except the reserved raindrop.* identity keys.
Flush & Shutdown
Eve’s BatchSpanProcessor flushes spans automatically at process exit, and the integration registers a shutdown() hook that drains the internal Raindrop client buffer before the process exits. Most users don’t need to call anything manually.
If you’re embedding RaindropEveSpanExporter directly, call await exporter.shutdown() before exit.
Examples
examples/eve-basic — single-agent example with one tool (get_weather) and an end-to-end dashboard verifier.
examples/eve-subagents — parent agent dispatching weatherResearcher + attractionsFinder sub-agents, verifying the cross-sandbox nesting against the production dashboard.
Known Limitations
- Streaming: The integration captures aggregate token usage and the final outputs of each step. Individual token-by-token deltas are not traced separately.
- OTel provider access: The integration calls
addSpanProcessor on the NodeTracerProvider returned by @vercel/otel. Custom OTel setups that hide the underlying provider behind a non-standard proxy will gracefully skip the ai.eve.turn session-id enrichment path and fall back to the events["step.started"] session input only.
Source