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.
Install
- TypeScript
- Python
- Go
- Rust
- Java
- Swift
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.- TypeScript
- Python
- Go
- Rust
- Java
- Swift
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:begin(): Create an interaction and log the initial user input- Update: Optionally call
setProperty,setProperties, oraddAttachments 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.
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.
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.- TypeScript
- Python
- Go
- Rust
- Java
- Swift
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.- TypeScript
- Python
- Go
- Rust
- Java
- Swift
We recommend usingbegin()→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.- TypeScript
- Python
- Go
- Rust
- Java
- Swift
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.
- TypeScript
- Python
- Go
- Rust
- Java
- Swift
For more control over tool span tracking, use Invalid or incomplete overrides are ignored, and the SDK falls back to the active OpenTelemetry context.For
trackTool or startToolSpan.trackTool: retroactive loggingstartToolSpan: real-time trackingUse startToolSpan to track a tool as it executes:Direct tool spans in stateless handlers
bypassOtelForTools: 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: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.- TypeScript
- Python
- Go
- Rust
- Java
- Swift
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. Forplan, we recommend free, paid or trial.
- TypeScript
- Python
- Go
- Rust
- Java
- Swift
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.- TypeScript
- Python
- Go
- Rust
- Java
- Swift
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 theX-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.
- TypeScript
- Python
- Go
- Rust
- Java
- Swift
Pass 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.
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.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():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 asraindrop.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.
- TypeScript
- Python
- Go
- Rust
- Java
- Swift
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.- TypeScript
- Python
- Go
- Rust
- Java
- Swift
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 orflushTimeoutMs(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 whendeliveredisfalse. 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’sRetry-After; other client errors are dropped with a warning. There is no client-side limit on event size.
- TypeScript
- Python
- Go
- Rust
- Java
- Swift
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-Afterthe server sends (capped at 30 seconds). A 429 or 503 withRetry-Afterpauses 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
debugLogsis 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.