Skip to main content
Every Raindrop SDK tracks the same things the same way: AI events, interactions you update as they happen, tool calls, feedback signals and users. Pick your language in any code sample; every sample and language note on this page switches with it, and the choice is remembered.
Using a browser app? See the Browser SDK: it sends each call right away instead of queueing, so its delivery model differs. Framework integrations (LangChain, CrewAI, the Vercel AI SDK, …) are under Integrations.
No write key yet? Agents can create one now, without a browser: run npx -y create-raindrop --org-name '<project name>' and give the user the claim link. Steps: raindrop.ai/auth.md. See Agent signup.

Install

Other package managers: yarn add raindrop-ai, pnpm add raindrop-ai or bun add raindrop-ai.

Initialize

Create one client at startup and reuse it. Pass the write key straight in: without one, the SDK sends nothing and prints how to get a key, so you can add the code first and the key later.

Track an interaction

An interaction is one AI event you build up while it happens: begin it with what you know, update it as work progresses, and finish it with the output. Everything you set lands on the same event. Events views show an interaction once it’s finished.
begin() needs an eventId. Generate one (as in the sample) and keep it if you want to update or resume the event later.The Interaction API uses a simple three-step pattern:
  1. begin(): Create an interaction and log the initial user input
  2. Update: Optionally call setProperty, setProperties, or addAttachments
  3. await finish(): Record the AI’s final output and close the interaction. finish() returns a promise that resolves after the final event is sent, so await it.
An interaction that has begun but not finished is not shown on the Events page. Event views only show finalized events.In a short-lived script or CLI, call await raindrop.close() before the process exits. In a long-running server, reuse one client across requests and call close() only when the server shuts down, not after each request.
Using Vercel AI SDK? Check out our automatic integration to track AI events and traces with zero configuration.
The event’s start time is when you call begin(). To record an earlier start, for example when the user’s message arrived before your handler ran, pass timestamp (an ISO 8601 string) to begin(). Every later update keeps that start time.Update an interaction at any point using setProperty, setProperties, or addAttachments:

Resume an interaction

Updates can come later, even from another process: reopen the event by the id you saved when you began it, and keep updating it. Signals attach to an event the same way, by its id.
If you no longer have the interaction object returned from begin(), resume it by event ID with resumeInteraction(). This works in any process, so one service (or request) can begin() an event and a queue worker or later request can update and finish() it.The second argument is optional and takes userId, convoId and eventName, for whatever this process knows. Fields you leave out are not sent, and Raindrop keeps the values it stored when the event began. Without a userId the update is still sent, with a warning.

Track a single event

When the input and output are known together, send the whole event in one call.
We recommend using begin() → finish() for new code to take advantage of partial-event buffering, tracing, and upcoming features like automatic token counts.
trackAi() queues the event and sends it in a batch in the background. An event without a userId is not sent; a warning is logged instead.

Attachments

Attachments add context to an event: code, text, images or embedded pages (iframes), each marked as part of the input or the output. Images and iframes take a URL.
Attachments let you include additional context—documents, images, code, or embedded content—with your events. They work with both begin() interactions and trackAi() calls.

Tool spans

Record the tools an agent calls as tool spans on its interaction: each shows in the event’s trace with its input, output, duration and error. trackTool records a call that already happened; give its duration, or its start and end times. A tool call can also be recorded later, or from another process, under a saved parent span.
For more control over tool span tracking, use trackTool or startToolSpan.trackTool: retroactive loggingstartToolSpan: real-time trackingUse startToolSpan to track a tool as it executes:
Direct tool spans in stateless handlersbypassOtelForTools: true changes how tool spans are shipped, but they still read their parent from the active @opentelemetry/api context. trace.setSpanContext() with context.with() is sufficient when the handler, Sentry, and Raindrop share the same OpenTelemetry API singleton and context manager.If a stateless handler cannot rely on ambient context, pass the propagated (or saved) W3C hexadecimal IDs directly. This works with or without bypassOtelForTools, and attaches a tool call recorded later, or in another process, under the span that started it. Both values must be supplied together, and give the tool’s real times with startTime and endTime:
Invalid or incomplete overrides are ignored, and the SDK falls back to the active OpenTelemetry context.For withTool and tracing setup, see Tracing.

Track feedback (signals)

Attach feedback, such as a thumbs-down with a comment, to an event by its id.
Signals capture quality ratings on AI events. Use trackSignal() with the same eventId from begin() or trackAi().

Identify users

Attach traits to a user id; they show on that user’s events. For plan, we recommend free, paid or trial.
The method is setUserDetails().

Configuration

Every SDK takes the same core options: the write key, a project, debug logging and the deadline for flushing. The full list for each language is below.
Call await raindrop.close() before your process exits to flush any buffered events.

Projects

Pass a project id when you create the client to scope every event from it to a specific project. It’s sent as the X-Raindrop-Project-Id header on each request. Omitting it (or passing "default") sends to the default Production project, so single-project orgs need nothing here. See Projects for isolation, archival, and the full behavior table.
Pass projectId to scope every event from a client to a specific project. Under the hood this sets the X-Raindrop-Project-Id header on each request.Omitting projectId (or passing "default") sends to the default Production project, which is the existing behavior. See Projects for isolation, archival, and the full behavior table.Multiple projects in one processWhen one service runs several agents that should report to different projects — e.g. an Express app serving a support agent and a billing agent — create one Raindrop client per project and reuse them. The instance is the routing decision; there is no shared mutable project state to race on, so concurrent requests handled by different agents route independently.
Tracing across multiple projects. OpenTelemetry auto-instrumentation is shared per process: the first client that enables tracing initializes it, and later clients share the pipeline. begin() scopes every span produced in the current request/task to its client’s project, until the matching finish() (an interaction that is never finished releases its scope once garbage-collected). For LLM calls made outside an interaction, scope them explicitly with asCurrent():
One tracing API key per process: all auto-instrumented spans export under the first tracing-enabled client’s write key. Clients created with a different writeKey (a different workspace) get a warning, and from then on only spans scoped to a client via begin()/asCurrent() are exported — the other workspace’s spans and any unscoped spans are dropped at export rather than delivered to the wrong workspace. Run a second workspace’s tracing in its own process. Single-key processes (the normal case) are unaffected. useExternalOtel is unsupported when more than one write key is used in a single process. Manual events (trackAi, begin/finish, signals, identify) always ship with each client’s own key and are unaffected.
On runtimes without AsyncLocalStorage.enterWith — notably Cloudflare Workers (workerd) — begin() cannot scope auto-instrumented spans: it still records the interaction and its manual withSpan/withTool/events, but ambient span routing is skipped (so it can’t cross-contaminate concurrent requests). Use asCurrent() (or interaction.withSpan(...)) to route auto-instrumented spans there — those are built on AsyncLocalStorage.run, which Workers support.

Application Git provenance

Each SDK reports the application’s revision as raindrop.app.commit_sha, with optional raindrop.app.commit_dirty and raindrop.app.branch, for reporting and filtering. Values you set on the client win over the RAINDROP_COMMIT_SHA, RAINDROP_COMMIT_DIRTY and RAINDROP_BRANCH environment variables, and properties you send on an event always win. Only full 40- or 64-character SHAs are used.
Raindrop reports the instrumented application’s revision as raindrop.app.commit_sha, with optional raindrop.app.commit_dirty and raindrop.app.branch. These properties are for reporting and filtering; they do not change simulation checkout or target selection, service.version, raindrop.app.release, or SDK identity.In ordinary Node.js applications, full 40- or 64-character commit SHA discovery is enabled by default when the application revision can be identified reliably. Local Git runs once in a bounded background task. Telemetry, flush(), and close() never wait for it, so an operation started while discovery is pending omits the metadata. Branch discovery is opt-in.commitSha, commitDirty, and branch are explicit overrides. sourceDirectory must identify the application-under-test (AUT), not an SDK checkout, observer process, or workspace being edited by a remote coding agent. Remote and coding integrations do not inspect local Git unless you explicitly provide that AUT directory. A non-empty sourceDirectory or RAINDROP_GIT_SOURCE_DIRECTORY is captured at client initialization and exclusively selects the automatic AUT checkout after explicit config and RAINDROP_* identity values. If that checkout is unavailable, metadata stays absent; ambient build, deployment, and CI identity is not substituted. Use appGit: false to disable all client-level enrichment without removing canonical properties explicitly supplied on an operation. By contrast, appGit: { autoDetect: false } disables automatic build, deployment, Git, and CI sources while retaining explicit config, canonical operation properties, and RAINDROP_COMMIT_SHA, RAINDROP_COMMIT_DIRTY, and RAINDROP_BRANCH.For explicit environment values, RAINDROP_COMMIT_SHA is a full 40- or 64-character hexadecimal SHA, RAINDROP_COMMIT_DIRTY is true or false, and RAINDROP_BRANCH is an ordinary branch string.Existing canonical properties on an event or owning span always win, even when null, empty, or invalid. Other raw properties are preserved. If an operation overrides only the SHA, Raindrop does not attach unrelated automatically inferred branch or dirty state from another source. Config wins over explicit Raindrop environment values. Without a selected source directory, those are followed by reliable build/deploy metadata, application Git, and provider-marked CI metadata. RAINDROP_GIT_DETECT_BRANCH=true enables branch discovery, and RAINDROP_GIT_AUTO_DETECT=false disables automatic sources only. Optional discovery failure simply leaves metadata absent; it is not an absolute guarantee against unrelated application failures.Replay identityConfigure replay identity on the Raindrop client, in CreateReplayOptions.appGit, or in the options passed to claimReplay(...) before a run begins. runReplay(...) uses the snapshot captured by create or claim; RunReplayOptions has no separate appGit override.Replay create, claim, and started-report payloads reuse the existing flat commit_sha, commit_dirty, and commit_branch fields. The report-only SHA accepts a trimmed 40- or 64-character hexadecimal value. A flat branch is trimmed and omitted when empty or longer than 400 characters; an ordinary raw raindrop.app.branch property remains caller data. These reporting rules do not replace the separate checkout selector’s existing strict 40-character validation, and Git metadata does not select the checkout.

Flushing and closing

Tracking calls only queue and return immediately; a background sender delivers them. Flush when you need what you’ve sent so far delivered before continuing (for example at the end of a serverless handler), and close the client before the process exits. Neither throws on delivery problems: they report what happened, and ignoring the result is safe.
Call await raindrop.forceFlush() when you need everything sent so far delivered before continuing, for example at the end of a serverless handler, and await raindrop.close() before your process exits.
  • forceFlush() sends buffered events, signals, user details, interaction updates (including ones still pending) and spans, and waits until they, and sends already in progress, reach Raindrop or flushTimeoutMs (default 10 seconds) passes.
  • If the deadline passes first, payloads that weren’t sent yet, or were waiting on a retry, stay queued and go out later. A request cut off mid-flight is dropped rather than sent twice.
  • forceFlush() never rejects. It resolves to { delivered, timedOut, dropped, queued, inFlight }, and logs a warning when delivered is false. Ignoring the result is safe.
  • close() works the same way under the same deadline, then shuts the client down. Whatever is still unsent at the deadline is dropped with a warning.
The framework integrations that wrap your client or agent (@raindrop-ai/ai-sdk, @raindrop-ai/langchain, …) work the same way. A wrapped call returns as soon as its event is queued, so retries and slow requests never delay your code. Their flush() and shutdown() wait for delivery within flushTimeoutMs, never reject, and resolve to the same result. Properties you pass to an integration win over keys it sets itself (such as ai.usage.* or error.type) when the names collide.

Delivery and retries

Delivery problems are logged, never raised into your code. Retryable failures (timeouts, rate limits, server errors) are retried, honouring the server’s Retry-After; other client errors are dropped with a warning. There is no client-side limit on event size.
Sending to Raindrop happens in the background, so tracking calls don’t throw when Raindrop can’t be reached. Delivery problems are logged with console.warn / console.error (rate-limited), and forceFlush() resolves to a result you can check; see Flushing and closing. You can ignore both without side effects.
  • 408, 429 and 5xx responses and network errors are retried, waiting at least the Retry-After the server sends (capped at 30 seconds). A 429 or 503 with Retry-After pauses all sending to Raindrop for that time.
  • Any other 4xx is a permanent rejection: the payload is dropped with a log message and never retried. It still counts as delivered in the flush result.
  • There is no client-side limit on event size; Raindrop decides what it accepts. Individual text fields are capped by maxTextFieldChars.
  • When Raindrop accepts a request but reports items it rejected or changed, they are logged as warnings.
  • Warnings are rate-limited and logged whether or not debugLogs is on. Besides delivery problems, they cover an event without a user ID (not sent), an event finished with no input, output or attachments (still sent), an update to a resumed event without a user ID (still sent), and an event finished twice in the same process (still sent).

More

Tracing

Spans, tool calls, model calls and sub-agents, in every SDK.

Tool catalog capture

How the tools a model was given are recorded.

Browser SDK

Track AI events from web apps.

HTTP API

Send events without an SDK.