> ## Documentation Index
> Fetch the complete documentation index at: https://raindrop.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# SDKs

> One guide for every Raindrop SDK. Pick a language in any code sample and the whole page follows.

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.

> [!NOTE]
>
> Using a browser app? See the [Browser SDK](https://raindrop.ai/docs/sdk/browser.md): 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](https://raindrop.ai/docs/integrations/overview.md).

> [!TIP]
>
> 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](https://www.raindrop.ai/auth.md). See [Agent signup](https://raindrop.ai/docs/integrations/agent-signup.md).

## Install

```bash TypeScript icon="https://mintcdn.com/dawn-a6c57108/72ImDkk3v1Xlc68q/images/icons/typescript.svg?fit=max&auto=format&n=72ImDkk3v1Xlc68q&q=85&s=d8ece0bb7b221c02bae30c5db9c578b3" theme={null}
npm install raindrop-ai
```

```bash Python icon="python" theme={null}
pip install raindrop-ai
```

```bash Go icon="golang" theme={null}
go get github.com/raindrop-ai/go@v0.1.8
```

```toml Rust icon="rust" theme={null}
[dependencies]
raindrop-ai = { git = "https://github.com/raindrop-ai/raindrop-rust", tag = "v0.0.10" }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
serde_json = "1"
```

```groovy Java icon="java" theme={null}
dependencies {
    implementation 'ai.raindrop:raindrop-java:0.0.5'
}
```

```swift Swift icon="swift" theme={null}
// Package.swift
dependencies: [
  .package(url: "https://github.com/raindrop-ai/raindrop-swift", from: "0.0.8")
],
targets: [
  .target(name: "MyApp", dependencies: [.product(name: "Raindrop", package: "raindrop-swift")])
]
```

> [tab] TypeScript
>
> Other package managers: `yarn add raindrop-ai`, `pnpm add raindrop-ai` or `bun add raindrop-ai`.

> [tab] Python
>
> Other package managers: `uv add raindrop-ai` or `poetry add raindrop-ai`.

> [tab] Go
>
> Source code and releases live in [raindrop-ai/go](https://github.com/raindrop-ai/go).

> [tab] Rust
>
> The crate is not published to crates.io yet, so install it from GitHub, pinned to the release tag (`v0.0.10`), which keeps builds reproducible. Add it to your `Cargo.toml` as shown above.
>
> Source code and releases live in [raindrop-ai/raindrop-rust](https://github.com/raindrop-ai/raindrop-rust).
>
> > [!NOTE]
> >
> > The Rust SDK requires Rust **1.88+** (MSRV). It is `async`-first and uses `tokio`. Most fallible methods return `Result<_, Error>` — `track_ai`, `track_event`, `identify`, `track_signal`, the `Interaction` mutators (`set_input`, `set_property`, `set_properties`, `add_attachments`, `patch`, `finish`), and `Client::flush` / `Client::close`. Calls that record telemetry only queue it and return right away, so they don't fail because of the network; `finish()` and `close()` return `Ok(())` even when something couldn't be delivered. `flush()` can return `Error::Undelivered` (see [Flushing and closing](#flushing-and-closing)): log that error rather than propagating it with `?`, since your own work didn't fail. The two constructors — `Client::begin(...).await` and `Client::resume_interaction(...)` — are **infallible**: they always return an `Interaction` (a no-op handle when the client is disabled), so don't put a `?` on those.
>
> > [!NOTE]
> >
> > The Rust SDK is at `0.0.10`. The wire contract against the Raindrop ingestion API is
> > stable and verified end-to-end against the live backend on every push, but the crate API may still
> > change in minor ways before `0.1.0`. Pin the release tag in your `Cargo.toml`.

> [tab] Java
>
> The SDK requires **Java 17+**. The artifact coordinates are `ai.raindrop:raindrop-java`.
>
> With Maven:
>
> ```xml theme={null}
> <dependency>
>     <groupId>ai.raindrop</groupId>
>     <artifactId>raindrop-java</artifactId>
>     <version>0.0.5</version>
> </dependency>
> ```
>
> `Raindrop` implements `AutoCloseable`, so try-with-resources flushes and stops the client for
> short-lived programs:
>
> ```java theme={null}
> try (Raindrop raindrop = new Raindrop(RaindropConfig.builder().writeKey(key).build())) {
>     // ... use raindrop ...
> }
> ```
>
> > [!NOTE]
> >
> > The Java SDK is at `0.0.5`. The wire contract against the Raindrop ingestion API is
> > stable and verified end-to-end against the live backend on every push, but the public API may
> > still change in minor ways before `0.1.0`. Pin an exact version in your build file.

> [tab] Swift
>
> Add the package with Swift Package Manager (Xcode: **File ▸ Add Package Dependencies…**).
>
> Requires Swift 6, iOS 16 / macOS 13 / tvOS 16 / watchOS 9 / visionOS 1, or Linux. No third-party dependencies.
>
> A missing or empty write key makes the client a no-op, so you can add the code first and the key later.
>
> **Validating your setup**
>
> ```swift theme={null}
> try await raindrop.validate()   // checks the write key and project; sends no data
> ```

## 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.

```typescript TypeScript icon="https://mintcdn.com/dawn-a6c57108/72ImDkk3v1Xlc68q/images/icons/typescript.svg?fit=max&auto=format&n=72ImDkk3v1Xlc68q&q=85&s=d8ece0bb7b221c02bae30c5db9c578b3" theme={null}
import { randomUUID } from "node:crypto";
import { Raindrop } from "raindrop-ai";

const raindrop = new Raindrop({ writeKey: process.env.RAINDROP_WRITE_KEY });
```

```python Python icon="python" theme={null}
import os
import raindrop.analytics as raindrop

raindrop.init(os.getenv("RAINDROP_WRITE_KEY"))
```

```go Go icon="golang" theme={null}
import (
	"context"
	"errors"
	"log"
	"os"
	"time"

	raindrop "github.com/raindrop-ai/go"
)

client, err := raindrop.New(raindrop.WithWriteKey(os.Getenv("RAINDROP_WRITE_KEY")))
if err != nil {
	log.Fatal(err)
}
defer client.Close()
ctx := context.Background()
```

```rust Rust icon="rust" theme={null}
use std::collections::BTreeMap;

use raindrop::{AiEvent, BeginOptions, Client, FinishOptions, ResumeOptions, Signal, SignalKind, User};
use serde_json::json;

let client = Client::builder()
    .write_key(std::env::var("RAINDROP_WRITE_KEY").unwrap_or_default())
    .build()?;
```

```java Java icon="java" theme={null}
import ai.raindrop.*;
import ai.raindrop.tracing.*;

import java.util.List;
import java.util.Map;

Raindrop raindrop = new Raindrop(RaindropConfig.builder()
        .writeKey(System.getenv("RAINDROP_WRITE_KEY"))
        .build());
```

```swift Swift icon="swift" theme={null}
import Foundation
import Raindrop

let raindrop = RaindropClient(writeKey: ProcessInfo.processInfo.environment["RAINDROP_WRITE_KEY"])
```

## 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 TypeScript icon="https://mintcdn.com/dawn-a6c57108/72ImDkk3v1Xlc68q/images/icons/typescript.svg?fit=max&auto=format&n=72ImDkk3v1Xlc68q&q=85&s=d8ece0bb7b221c02bae30c5db9c578b3" theme={null}
const prompt = "Can you suggest a calm Saturday morning in San Francisco?";

const interaction = raindrop.begin({
  eventId: randomUUID(), // required in TypeScript
  userId: "user-123",
  event: "chat_message",
  input: prompt,
  model: "gpt-4o",
  convoId: "conv-123",
  properties: { surface: "chat" },
});
const eventId = interaction.getEventId(); // save it to resume the event or attach signals later

interaction.setProperty("stage", "retrieving");
const output = await callLlm(prompt);

await interaction.finish({ output });
```

```python Python icon="python" theme={null}
prompt = "Can you suggest a calm Saturday morning in San Francisco?"

interaction = raindrop.begin(
    user_id="user-123",
    event="chat_message",
    input=prompt,
    model="gpt-4o",
    convo_id="conv-123",
    properties={"surface": "chat"},
)
event_id = interaction.id  # save it to resume the event or attach signals later

interaction.set_property("stage", "retrieving")
output = call_llm(prompt)

interaction.finish(output=output)
```

```go Go icon="golang" theme={null}
prompt := "Can you suggest a calm Saturday morning in San Francisco?"

interaction := client.Begin(ctx, raindrop.BeginOptions{
	UserID:     "user-123",
	Event:      "chat_message",
	Input:      prompt,
	Model:      "gpt-4o",
	ConvoID:    "conv-123",
	Properties: map[string]any{"surface": "chat"},
})
eventID := interaction.EventID() // save it to resume the event or attach signals later

_ = interaction.SetProperty("stage", "retrieving")
output := callLLM(prompt)

_ = interaction.Finish(raindrop.FinishOptions{Output: output})
```

```rust Rust icon="rust" theme={null}
let prompt = "Can you suggest a calm Saturday morning in San Francisco?";

let interaction = client
    .begin(BeginOptions {
        user_id: "user-123".into(),
        event: "chat_message".into(),
        input: prompt.into(),
        model: "gpt-4o".into(),
        convo_id: "conv-123".into(),
        properties: BTreeMap::from([("surface".into(), json!("chat"))]),
        ..Default::default()
    })
    .await;
let event_id = interaction.event_id().to_string(); // save it to resume the event or attach signals later

interaction.set_property("stage", "retrieving").await?;
let output = call_llm(prompt).await;

interaction
    .finish(FinishOptions { output, ..Default::default() })
    .await?;
```

```java Java icon="java" theme={null}
String prompt = "Can you suggest a calm Saturday morning in San Francisco?";

Interaction interaction = raindrop.begin(BeginOptions.builder()
        .userId("user-123")
        .event("chat_message")
        .input(prompt)
        .convoId("conv-123")
        .properties(Map.of("surface", "chat"))
        .build());
String eventId = interaction.getEventId(); // save it to resume the event or attach signals later

interaction.setModel("gpt-4o").setProperty("stage", "retrieving");
String output = callLlm(prompt);

interaction.setOutput(output).finish();
```

```swift Swift icon="swift" theme={null}
let prompt = "Can you suggest a calm Saturday morning in San Francisco?"

let interaction = raindrop.begin(BeginOptions(
  userId: "user-123",
  event: "chat_message",
  input: prompt,
  model: "gpt-4o",
  convoId: "conv-123",
  properties: ["surface": "chat"]
))
let eventId = interaction.eventId  // save it to resume the event or attach signals later

interaction.setProperty("stage", "retrieving")
let output = try await callLLM(prompt)

await interaction.finish(output: output)
```

> [tab] TypeScript
>
> `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.
>
> > [!NOTE]
> >
> > **Using Vercel AI SDK?** Check out our [automatic integration](https://raindrop.ai/docs/integrations/vercel-ai-sdk.md) 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`:
>
> ```typescript theme={null}
> interaction.setProperty("stage", "embedding");
> interaction.addAttachments([
>   {
>     type: "text",
>     name: "Additional Info",
>     value: "A very long document",
>     role: "input",
>   },
>   { type: "image", value: "https://example.com/image.png", role: "output" },
>   {
>     type: "iframe",
>     name: "Generated UI",
>     value: "https://newui.generated.com",
>     role: "output",
>   },
> ]);
> ```

> [tab] Python
>
> `begin()` generates the event id unless you pass `event_id`; read it from `interaction.id`.
>
> For multi-turn conversations or when event data arrives incrementally, you can use the `begin()` and `Interaction` object to send partial updates.
>
> `begin()` starts or resumes an interaction and returns an `Interaction` helper object:
>
> * `user_id` (str): The user's identifier.
> * `event` (str): The name of the event.
> * `event_id` (Optional\[str]): A unique ID for the event. If not provided, one is generated.
> * `properties` (Optional\[Dict\[str, Any]]): Initial properties for the event.
> * `input` (Optional\[str]): Initial input for the AI.
> * `attachments` (Optional\[List\[Attachment]]): Initial attachments.
> * `convo_id` (Optional\[str]): Conversation ID.
> * `model` (Optional\[str]): The model name.
> * `timestamp` (Optional\[Union\[str, datetime]]): When the interaction started (ISO 8601 string or `datetime`). Defaults to now. Every update sent for the interaction keeps this start time.
>
> The `Interaction` object has the following methods to update the event:
>
> * `interaction.set_input(text: str)`: Updates the AI input.
> * `interaction.add_attachments(attachments: List[Attachment])`: Adds more attachments.
> * `interaction.set_properties(props: Dict[str, Any])`: Merges new properties with existing ones.
> * `interaction.set_property(key: str, value: str)`: Convenience for setting a single property.
> * `interaction.finish(output: Optional[str] = None, **extra)`: Marks the interaction as complete.
>   * `output`: The final AI output.
>   * `**extra`: Any other top-level `TrackAIEvent` fields to update (e.g., `properties`, `attachments`).
>
> The SDK automatically sends updates to the backend after a short period of inactivity or when `finish()` is called.
>
> An interaction finished with no input, output or attachments is still sent, with a warning. Finishing the same event twice in one process also logs a warning (the second update is still sent).
>
> Example:
>
> ```python theme={null}
> # Start an interaction
> interaction = raindrop.begin(user_id="user789", event="code_generation", input="Write a python function for fibonacci")
>
> # ... later, user adds more context
> interaction.add_attachments([{"type": "text", "value": "It should be recursive", "role": "input"}])
>
> # ... AI generates output
> interaction.finish(output="def fib(n): if n <= 1: return n else: return fib(n-1) + fib(n-2)")
> ```

> [tab] Go
>
> Pass `EventID` to choose the id yourself; otherwise the SDK generates one.
>
> 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`, `SetInput`, or `AddAttachments`
> 3. **`Finish()`** – Record the AI's final output and close the interaction
>
> Every update for an interaction carries its start time: the `Timestamp` you pass to `BeginOptions`, otherwise the time `Begin()` was called. A later `Timestamp` only replaces it if it is earlier.
>
> Update an interaction at any point using `SetProperty`, `SetProperties`, `SetInput`, or `AddAttachments`:
>
> ```go theme={null}
> _ = interaction.SetProperty("stage", "retrieving")
> _ = interaction.SetProperties(map[string]any{
> 	"surface": "chat",
> 	"region":  "us-west",
> })
> _ = interaction.SetInput("Can you make it a little more local?")
> _ = interaction.AddAttachments([]raindrop.Attachment{{
> 	Type:  "text",
> 	Role:  "input",
> 	Name:  "preferences",
> 	Value: "Prefers coffee, a quiet walk, and no museum stops.",
> }})
> ```

> [tab] Rust
>
> Set `event_id` to choose the id yourself; otherwise the SDK generates one.
>
> The Interaction API uses a simple three-step pattern:
>
> 1. **`begin()`** – Create an interaction and log the initial user input
> 2. **Update** – Optionally call `set_property`, `set_properties`, `set_input`, or `add_attachments`
> 3. **`finish()`** – Record the AI's final output and close the interaction
>
> 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, set `timestamp` in `BeginOptions`. Every later update keeps that start time.
>
> Update an interaction at any point using `set_property`, `set_properties`, `set_input`, or `add_attachments`:
>
> ```rust theme={null}
> use raindrop::Attachment;
> use serde_json::json;
>
> interaction.set_property("stage", "retrieving").await?;
> interaction
>     .set_properties(BTreeMap::from([
>         ("surface".into(), json!("chat")),
>         ("region".into(), json!("us-west")),
>     ]))
>     .await?;
> interaction.set_input("Can you make it a little more local?").await?;
> interaction
>     .add_attachments(vec![Attachment {
>         kind: "text".into(),
>         role: "input".into(),
>         name: "preferences".into(),
>         value: "Prefers coffee, a quiet walk, and no museum stops.".into(),
>         ..Default::default()
>     }])
>     .await?;
> ```

> [tab] Java
>
> The model is set on the interaction (`setModel`), not on `BeginOptions`.
>
> `begin()` opens an interaction (a trace) and buffers a pending event. Cloud Events views
> show completed events after `finish()` finalizes the interaction. Update it as work progresses,
> then call `finish()` with the final output.
>
> `setInput`, `setOutput`, `setModel`, `setProperty`, and `setError` return the interaction, so they
> chain. Call `finish()` once; it finalizes the event and closes the root span. A repeated
> `finish()` is still sent as an update (any output or properties it carries land), with a warning,
> and an interaction finished with no input or output is still
> sent, with a warning. An interaction begun without a `userId` is not sent.
>
> 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 `BeginOptions.builder().timestamp(...)` (an
> `Instant` or an ISO 8601 string). Every later update keeps that start time.

> [tab] Swift
>
> `finish(output:)` is `async`: it returns once the event is delivered or the flush deadline passes.
>
> `interaction.eventId` is the event's id (a UUID unless you pass `eventId`). Use it for signals.
>
> 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:` to `BeginOptions`.
>
> ```swift theme={null}
> interaction.setProperty("stage", "retrieving")
> interaction.setProperties(["region": "us-west"])
> interaction.setInput("Can you make it a little more local?")
> interaction.addAttachments([
>   Attachment(kind: .text, role: .input, value: "Prefers coffee and a quiet walk.", name: "preferences")
> ])
> // or several fields at once:
> interaction.update(InteractionUpdate(model: "gpt-4o-mini", properties: ["retries": 1]))
> ```
>
> Each send carries the interaction's full state. The SDK holds pending updates until the interaction has input or output. An interaction begun without a `userId` isn't sent until an update sets one (a warning is logged). Finishing with no input or output still sends the event, with a warning. Calling `finish` again is a no-op.

## 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 TypeScript icon="https://mintcdn.com/dawn-a6c57108/72ImDkk3v1Xlc68q/images/icons/typescript.svg?fit=max&auto=format&n=72ImDkk3v1Xlc68q&q=85&s=d8ece0bb7b221c02bae30c5db9c578b3" theme={null}
// Later, possibly in another process: reopen the event by its id.
const resumed = raindrop.resumeInteraction(savedEventId, { userId: "user-123" });
await resumed.finish({ output: "Here is a shorter version of that itinerary." });
```

```python Python icon="python" theme={null}
# Later, possibly in another process: reopen the event by its id.
resumed = raindrop.resume_interaction(event_id=saved_event_id, user_id="user-123")
resumed.finish(output="Here is a shorter version of that itinerary.")
```

```go Go icon="golang" theme={null}
// Later, possibly in another process: reopen the event by its id.
resumed := client.Resume(savedEventID, raindrop.ResumeOptions{UserID: "user-123"})
_ = resumed.Finish(raindrop.FinishOptions{Output: "Here is a shorter version of that itinerary."})
```

```rust Rust icon="rust" theme={null}
// Later, possibly in another process: reopen the event by its id.
let resumed = client.resume_interaction_with(
    SAVED_EVENT_ID,
    ResumeOptions { user_id: "user-123".into(), ..Default::default() },
);
resumed
    .finish(FinishOptions {
        output: "Here is a shorter version of that itinerary.".into(),
        ..Default::default()
    })
    .await?;
```

```java Java icon="java" theme={null}
// Later, possibly in another process: reopen the event by its id.
Interaction resumed = raindrop.resume(ResumeOptions.builder(savedEventId)
        .userId("user-123")
        .build());
resumed.setOutput("Here is a shorter version of that itinerary.").finish();
```

```swift Swift icon="swift" theme={null}
// Later, possibly in another process: reopen the event by its id.
let resumed = raindrop.resumeInteraction(eventId: savedEventId)
await resumed.finish(output: "Here is a shorter version of that itinerary.")
```

> [tab] TypeScript
>
> 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.

> [tab] Python
>
> If you already have an `event_id` for an ongoing interaction, you can get an `Interaction` object for it:
>
> ```python theme={null}
> interaction = raindrop.resume_interaction(event_id="existing_event_id")
>
> # Optionally pass what you know about the event
> interaction = raindrop.resume_interaction(
>     event_id="existing_event_id",
>     user_id="user_123",
>     convo_id="convo_456",
>     event_name="support-chat",
> )
> ```
>
> This works in any process, so one service can `begin()` an event and a worker or later request can update and `finish()` it. `user_id`, `convo_id` and `event_name` are optional. Updates carry the ones you pass; for any you leave out, Raindrop keeps the values it stored when the event began. If you don't pass `user_id`, a warning notes that updates go without a user.

> [tab] Go
>
> If you no longer have the interaction object returned from `Begin()`, resume it by event ID. This works in the same process and from another one (a queue worker, a later request, another service):
>
> ```go theme={null}
> interaction := client.ResumeInteraction("evt_123")
> _ = interaction.SetProperty("stage", "follow-up")
> _ = interaction.Finish(raindrop.FinishOptions{
> 	Output: "Here is a shorter version of that itinerary.",
> })
> ```
>
> Within the process that called `Begin()`, `ResumeInteraction()` returns the open interaction. For an event begun elsewhere it continues that event by ID: later updates are sent even without a `UserID`, and they carry only the fields you set, so the user, conversation and event name Raindrop already has are kept. Use `Resume()` to supply any of those fields:
>
> ```go theme={null}
> interaction := client.Resume("evt_123", raindrop.ResumeOptions{
> 	UserID: "user-123", // optional
> })
> ```
>
> An update for a resumed event that has no user ID is still sent, and logged as a warning.

> [tab] Rust
>
> If you no longer have the interaction object returned from `begin()`, resume it by event ID with `resume_interaction()`. This works in any process, so one service can `begin()` an event and another can update and `finish()` it later:
>
> ```rust theme={null}
> let interaction = client.resume_interaction("evt_123");
> interaction.set_property("stage", "follow-up").await?;
> interaction
>     .finish(FinishOptions {
>         output: "Here is a shorter version of that itinerary.".into(),
>         ..Default::default()
>     })
>     .await?;
> ```
>
> When the event began in another process, this process doesn't know its user, name or start time, so updates are sent without them and Raindrop keeps the values it already stored (a warning notes the missing user). If this process does know some of that identity, pass it with `resume_interaction_with()`; fields left empty are never sent:
>
> ```rust theme={null}
> use raindrop::ResumeOptions;
>
> let interaction = client.resume_interaction_with(
>     "evt_123",
>     ResumeOptions {
>         user_id: "user-123".into(),
>         convo_id: "conv-123".into(),
>         ..Default::default()
>     },
> );
> ```
>
> | Field | Type | Description |
> | - | - | - |
> | `user_id` | `String` | User ID, when this process knows it |
> | `convo_id` | `String` | Conversation ID, when this process knows it |
> | `event` | `String` | Event name, when this process knows it |

> [tab] Java
>
> To continue an event begun in another process (a queue worker, a later request), resume it by event
> ID. Nothing is sent until you update it; updates then carry only the identity you supply, and
> Raindrop keeps the user, conversation, name and start time it already has:
>
> ```java theme={null}
> Interaction it = raindrop.resume("evt_123");
> it.setOutput("done").finish();
>
> // Or, when this process knows some of the identity:
> Interaction it2 = raindrop.resume(ResumeOptions.builder("evt_123")
>         .userId("user-123")
>         .convoId("conv-123")
>         .build());
> ```
>
> `ResumeOptions` also takes `event` and `timestamp`. An update to a resumed event without a `userId`
> is still sent, with a warning.

> [tab] Swift
>
> If this process still has the interaction open, you get the same object back. Otherwise you get a new handle for that id, which works in another process too: only the event id is needed, and updates merge into the existing event. Pass `beganAt` so the event keeps its original time. An update to a resumed interaction without a `userId` is still sent, with a warning; Raindrop keeps the user it already has.
>
> `resumeInteraction` in a new process can't recover fields set earlier in another process. Updates merge into the stored event server-side.

## Track a single event

When the input and output are known together, send the whole event in one call.

```typescript TypeScript icon="https://mintcdn.com/dawn-a6c57108/72ImDkk3v1Xlc68q/images/icons/typescript.svg?fit=max&auto=format&n=72ImDkk3v1Xlc68q&q=85&s=d8ece0bb7b221c02bae30c5db9c578b3" theme={null}
raindrop.trackAi({
  userId: "user-123",
  event: "chat_message",
  input: "Who won the 2023 AFL Grand Final?",
  output: "Collingwood by four points!",
  model: "gpt-4o",
  convoId: "conv-123",
});
```

```python Python icon="python" theme={null}
raindrop.track_ai(
    user_id="user-123",
    event="chat_message",
    input="Who won the 2023 AFL Grand Final?",
    output="Collingwood by four points!",
    model="gpt-4o",
    convo_id="conv-123",
)
```

```go Go icon="golang" theme={null}
_ = client.TrackAI(ctx, raindrop.AIEvent{
	UserID:  "user-123",
	Event:   "chat_message",
	Input:   "Who won the 2023 AFL Grand Final?",
	Output:  "Collingwood by four points!",
	Model:   "gpt-4o",
	ConvoID: "conv-123",
})
```

```rust Rust icon="rust" theme={null}
client
    .track_ai(AiEvent {
        user_id: "user-123".into(),
        event: "chat_message".into(),
        input: "Who won the 2023 AFL Grand Final?".into(),
        output: "Collingwood by four points!".into(),
        model: "gpt-4o".into(),
        convo_id: "conv-123".into(),
        ..Default::default()
    })
    .await?;
```

```java Java icon="java" theme={null}
raindrop.trackAi(AiTrackEvent.builder()
        .userId("user-123")
        .event("chat_message")
        .input("Who won the 2023 AFL Grand Final?")
        .output("Collingwood by four points!")
        .model("gpt-4o")
        .convoId("conv-123")
        .build());
```

```swift Swift icon="swift" theme={null}
raindrop.trackAi(AiEvent(
  userId: "user-123",
  event: "chat_message",
  input: "Who won the 2023 AFL Grand Final?",
  output: "Collingwood by four points!",
  model: "gpt-4o",
  convoId: "conv-123"
))
```

> [tab] TypeScript
>
> > 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.

> [tab] Python
>
> To track AI interactions, you can use the `track_ai` function. It takes the following parameters:
>
> * `user_id` (str): The unique identifier of the user.
> * `event` (str): The name of the AI event you want to track.
> * `event_id` (Optional\[str]): A unique identifier for this specific event. If not provided, a UUID will be generated.
> * `model` (Optional\[str]): The name of the AI model used.
> * `input` (Optional\[str]): The input provided to the AI model. (Either this or `output` is required if AI data is logged)
> * `output` (Optional\[str]): The output generated by the AI. (Either this or `input` is required if AI data is logged)
> * `convo_id` (Optional\[str]): The conversation ID associated with the interaction. Helpful in AI apps with multiple conversations per user (e.g. ChatGPT).
> * `properties` (Optional\[Dict\[str, Any]]): Additional properties associated with the AI event.
> * `timestamp` (Optional\[str]): An ISO 8601 formatted timestamp for the event. If not provided, the SDK generates a UTC timestamp.
> * `attachments` (Optional\[List\[Attachment]]): A list of attachments associated with the event. See the Attachments section below.
>
> Example:
>
> ```python theme={null}
> raindrop.track_ai(
>     user_id="user123",
>     event="user_message",
>     model="gpt_4",
>     input="What is the weather like today?",  # this or output is required
>     output="The weather is sunny and warm.",  # this or input is required
>     convo_id="conv789",  # optional
>     properties={
>         "system_prompt": "you are a helpful...",
>         "experiment": "experiment_a",
>     },
>     attachments=[
>         {
>             "type": "text",
>             "name": "Additional Info",
>             "value": "A very long document",
>             "role": "input",
>         },
>         {
>             "type": "image",
>             "value": "https://example.com/image.png",
>             "role": "output",
>         },
>         {
>             "type": "iframe",
>             "name": "Generated UI",
>             "value": "https://newui.generated.com",
>             "role": "output",
>         },
>     ],
> )
> ```
>
> For functions like `track_ai` and `track_signal`, you can optionally provide a `timestamp` parameter (an ISO 8601 formatted string) if you need to specify a custom time for the event. If not provided, the SDK generates a UTC timestamp at the moment of the call. For interactions, pass the start time to `begin(timestamp=...)`.

> [tab] Go
>
> `TrackAI()` and `TrackEvent()` only queue the event and return immediately: they never do network I/O on the calling goroutine. Events are sent in batches by a background goroutine, or by `Flush()` / `Close()`. An event without a `UserID` is not sent and is logged as a warning.
>
> > [!NOTE]
> >
> > Since v0.1.8, `TrackAI()`, `TrackEvent()`, `TrackSignal()` and `Identify()` queue their payload instead of sending it inline, so they no longer return HTTP errors. Delivery problems are logged as warnings and reported by `Flush()` and `Close()`.
>
> > We recommend using `Begin()` → `Finish()` for new code to take advantage of partial-event buffering, tracing, and upcoming features like automatic token counts.
>
> Use `TrackEvent()` for non-AI events:
>
> ```go theme={null}
> _ = client.TrackEvent(ctx, raindrop.Event{
> 	UserID: "user-123",
> 	Event:  "session_started",
> 	Properties: map[string]any{
> 		"entrypoint": "dashboard",
> 	},
> })
> ```

> [tab] Rust
>
> > We recommend using `begin()` → `finish()` for new code to take advantage of partial-event buffering and tracing.
>
> `track_ai()`, `track_event()`, `track_signal()` and `identify()` queue the data and return immediately; it is sent in batches in the background, on `flush()` and on `close()`. An event without a `user_id` is not sent (a warning is logged).
>
> Use `track_event()` for non-AI events:
>
> ```rust theme={null}
> use raindrop::Event;
>
> client
>     .track_event(Event {
>         user_id: "user-123".into(),
>         event: "session_started".into(),
>         properties: BTreeMap::from([("entrypoint".into(), json!("dashboard"))]),
>         ..Default::default()
>     })
>     .await?;
> ```

> [tab] Java
>
> At least one of `input` or
> `output` is required, along with `userId` and `event`. It returns the event id (or `null` if the
> event was invalid or the client is disabled):
>
> ```java theme={null}
> String eventId = raindrop.trackAi(AiTrackEvent.builder()
>         .event("chat_message")
>         .userId("user-123")
>         .input("Who won the 2023 AFL Grand Final?")
>         .output("Collingwood by four points!")
>         .properties(Map.of(
>                 "ai.usage.prompt_tokens", 10,
>                 "ai.usage.completion_tokens", 5))
>         .build());
> ```
>
> > [!NOTE]
> >
> > For new code we recommend the `begin()` → `finish()` interaction API on this page: it buffers a pending
> > event immediately and links any spans you create into the same trace.
>
> Use `track()` for non-AI product events:
>
> ```java theme={null}
> raindrop.track(TrackEvent.builder()
>         .event("session_started")
>         .userId("user-123")
>         .properties(Map.of("entrypoint", "dashboard"))
>         .build());
> ```

> [tab] Swift
>
> `trackAi` needs `input`, `output`, or both. Use `trackEvent` for non-AI events:
>
> ```swift theme={null}
> raindrop.trackEvent(Event(userId: "user-123", event: "session_started", properties: ["entrypoint": "dashboard"]))
> ```
>
> `trackAi` doesn't create a span on its own; use the [tracing](https://raindrop.ai/docs/sdk/tracing.md) APIs.

## 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 TypeScript icon="https://mintcdn.com/dawn-a6c57108/72ImDkk3v1Xlc68q/images/icons/typescript.svg?fit=max&auto=format&n=72ImDkk3v1Xlc68q&q=85&s=d8ece0bb7b221c02bae30c5db9c578b3" theme={null}
raindrop.trackAi({
  userId: "user-123",
  event: "code_review",
  input: "Review this snippet",
  output: "Looks good. Here is a diagram of the flow.",
  attachments: [
    { type: "code", role: "input", language: "python", name: "app.py", value: "print('hello')" },
    { type: "image", role: "output", value: "https://example.com/flow.png" },
  ],
});
```

```python Python icon="python" theme={null}
raindrop.track_ai(
    user_id="user-123",
    event="code_review",
    input="Review this snippet",
    output="Looks good. Here is a diagram of the flow.",
    attachments=[
        {"type": "code", "role": "input", "language": "python", "name": "app.py", "value": "print('hello')"},
        {"type": "image", "role": "output", "value": "https://example.com/flow.png"},
    ],
)
```

```go Go icon="golang" theme={null}
_ = client.TrackAI(ctx, raindrop.AIEvent{
	UserID: "user-123",
	Event:  "code_review",
	Input:  "Review this snippet",
	Output: "Looks good. Here is a diagram of the flow.",
	Attachments: []raindrop.Attachment{
		{Type: "code", Role: "input", Language: "python", Name: "app.py", Value: "print('hello')"},
		{Type: "image", Role: "output", Value: "https://example.com/flow.png"},
	},
})
```

```rust Rust icon="rust" theme={null}
use raindrop::Attachment;

client
    .track_ai(AiEvent {
        user_id: "user-123".into(),
        event: "code_review".into(),
        input: "Review this snippet".into(),
        output: "Looks good. Here is a diagram of the flow.".into(),
        attachments: vec![
            Attachment {
                kind: "code".into(),
                role: "input".into(),
                language: "python".into(),
                name: "app.py".into(),
                value: "print('hello')".into(),
                ..Default::default()
            },
            Attachment {
                kind: "image".into(),
                role: "output".into(),
                value: "https://example.com/flow.png".into(),
                ..Default::default()
            },
        ],
        ..Default::default()
    })
    .await?;
```

```java Java icon="java" theme={null}
raindrop.trackAi(AiTrackEvent.builder()
        .userId("user-123")
        .event("code_review")
        .input("Review this snippet")
        .output("Looks good. Here is a diagram of the flow.")
        .addAttachment(Attachment.builder()
                .type(Attachment.Type.CODE).role(Attachment.Role.INPUT)
                .language("python").name("app.py").value("print('hello')")
                .build())
        .addAttachment(Attachment.builder()
                .type(Attachment.Type.IMAGE).role(Attachment.Role.OUTPUT)
                .value("https://example.com/flow.png")
                .build())
        .build());
```

```swift Swift icon="swift" theme={null}
raindrop.trackAi(AiEvent(
  userId: "user-123",
  event: "code_review",
  input: "Review this snippet",
  output: "Looks good. Here is a diagram of the flow.",
  attachments: [
    Attachment(kind: .code, role: .input, value: "print('hello')", name: "app.py", language: "python"),
    Attachment(kind: .image, role: .output, value: "https://example.com/flow.png"),
  ]
))
```

> [tab] TypeScript
>
> Attachments let you include additional context—documents, images, code, or embedded content—with your events. They work with both `begin()` interactions and `trackAi()` calls.
>
> | Property | Type | Description |
> | - | - | - |
> | `type` | `string` | `"code"`, `"text"`, `"image"`, or `"iframe"` |
> | `name` | `string` | Optional display name |
> | `value` | `string` | Content or URL |
> | `role` | `string` | `"input"` or `"output"` |
> | `language` | `string` | Programming language (for `code` attachments) |

> [tab] Python
>
> Attachments allow you to include context from the user (e.g. an attached image), or stuff that the model outputted (whether that is an image, document, code, or even an entire web page).
>
> Each attachment is an object with the following properties:
>
> * `type` (string): The type of attachment. Can be "code", "text", "image", or "iframe".
>
> * `name` (optional string): A name for the attachment.
>
> * `value` (string): The content or URL of the attachment.
>
> * `role` (string): Either "input" or "output", indicating whether the attachment is part of the user input or AI output.
>
> * `language` (optional string): For code attachments, specifies the programming language.
>
> > [!NOTE]
> >
> > The SDK doesn't drop large events itself; Raindrop decides what it accepts. Long AI input/output text and tool or span content are truncated per field at `max_text_field_chars` (default 1,000,000 characters), which you can pass to `raindrop.init(...)`.

> [tab] Go
>
> Attachments let you include additional context—documents, images, code, or embedded content—with your events. They work with both `Begin()` interactions and `TrackAI()` calls.
>
> | Property | Type | Description |
> | - | - | - |
> | `Type` | `string` | `"code"`, `"text"`, `"image"`, or `"iframe"` |
> | `Name` | `string` | Optional display name |
> | `Value` | `string` | Content or URL |
> | `Role` | `string` | `"input"` or `"output"` |
> | `Language` | `string` | Programming language (for `code` attachments) |

> [tab] Rust
>
> Attachments let you include additional context — documents, images, code, or embedded content — with your events. They work with both `begin()` interactions and `track_ai()` calls.
>
> | Field | Type | Description |
> | - | - | - |
> | `kind` | `String` | `"code"`, `"text"`, `"image"`, or `"iframe"` (serialized as `type` on the wire) |
> | `role` | `String` | `"input"` or `"output"` |
> | `name` | `String` | Optional display name |
> | `value` | `String` | Content or URL |
> | `language` | `String` | Programming language (only meaningful for `"code"` attachments) |
> | `attachment_id` | `String` | Optional UUID. Backend auto-assigns one if empty. Set explicitly to round-trip with [`Signal::attachment_id`](#track-feedback-signals). |
>
> > [!NOTE]
> >
> > The dashboard's attachment viewer renders `text`, `image`, and `iframe` attachments. `code`
> > attachments survive ingestion and are searchable, but are not currently displayed in the visual
> > attachments tab.

> [tab] Java
>
> Attachments include extra context — documents, images, code, or embedded content — with an event.
> Add them to a `trackAi()` or `track()` event via the builder:
>
> | Field | Type | Description |
> | - | - | - |
> | `type` | `Attachment.Type` | `CODE`, `TEXT`, `IMAGE`, or `IFRAME` (serialized as `type` on the wire) |
> | `role` | `Attachment.Role` | `INPUT` or `OUTPUT` |
> | `name` | `String` | Optional display name |
> | `value` | `String` | Content or URL |
> | `language` | `String` | Programming language (only meaningful for `CODE` attachments) |
> | `attachmentId` | `String` | Optional UUID. The backend auto-assigns one if empty; set it explicitly to round-trip with a signal's `attachmentId`. |
>
> > [!NOTE]
> >
> > The dashboard's attachment viewer renders `TEXT`, `IMAGE`, and `IFRAME` attachments. `CODE`
> > attachments survive ingestion and are searchable, but are not currently displayed in the visual
> > attachments tab.

> [tab] Swift
>
> Pass `attachments:` to `AiEvent` or `BeginOptions`, or add them to a live interaction with `interaction.addAttachments([...])`. `Attachment(kind:role:value:name:language:attachmentId:)` takes a `kind` (`.text`, `.code`, `.image`, `.iframe`), a `role` (`.input`, `.output`), the content or URL as `value`, and optionally a display `name`, the `language` of `.code` attachments and an `attachmentId`.

## 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 TypeScript icon="https://mintcdn.com/dawn-a6c57108/72ImDkk3v1Xlc68q/images/icons/typescript.svg?fit=max&auto=format&n=72ImDkk3v1Xlc68q&q=85&s=d8ece0bb7b221c02bae30c5db9c578b3" theme={null}
const agent = raindrop.begin({
  eventId: randomUUID(),
  userId: "user-123",
  event: "agent_run",
  input: "What's the weather in NYC?",
});

// A tool call that already happened
agent.trackTool({
  name: "web_search",
  input: { query: "weather in NYC" },
  output: { results: ["Sunny, 72°F"] },
  durationMs: 150,
});

// A failed one
agent.trackTool({
  name: "database_query",
  input: { query: "SELECT * FROM users" },
  durationMs: 50,
  error: new Error("connection timeout"),
});

await agent.finish({ output: "It's sunny in NYC." });
```

```python Python icon="python" theme={null}
agent = raindrop.begin(
    user_id="user-123",
    event="agent_run",
    input="What's the weather in NYC?",
)

# A tool call that already happened
agent.track_tool(
    name="web_search",
    input={"query": "weather in NYC"},
    output={"results": ["Sunny, 72°F"]},
    duration_ms=150,
)

# A failed one
agent.track_tool(
    name="database_query",
    input={"query": "SELECT * FROM users"},
    duration_ms=50,
    error=ConnectionError("connection timeout"),
)

agent.finish(output="It's sunny in NYC.")
```

```go Go icon="golang" theme={null}
agent := client.Begin(ctx, raindrop.BeginOptions{
	UserID: "user-123",
	Event:  "agent_run",
	Input:  "What's the weather in NYC?",
})

// A tool call that already happened
agent.TrackTool(raindrop.TrackToolOptions{
	Name:     "web_search",
	Input:    map[string]any{"query": "weather in NYC"},
	Output:   map[string]any{"results": []string{"Sunny, 72°F"}},
	Duration: 150 * time.Millisecond,
})

// A failed one
agent.TrackTool(raindrop.TrackToolOptions{
	Name:     "database_query",
	Input:    map[string]any{"query": "SELECT * FROM users"},
	Duration: 50 * time.Millisecond,
	Error:    errors.New("connection timeout"),
})

_ = agent.Finish(raindrop.FinishOptions{Output: "It's sunny in NYC."})
```

```rust Rust icon="rust" theme={null}
use raindrop::TrackToolOptions;
use std::time::Duration;

let agent = client
    .begin(BeginOptions {
        user_id: "user-123".into(),
        event: "agent_run".into(),
        input: "What's the weather in NYC?".into(),
        ..Default::default()
    })
    .await;

// A tool call that already happened
agent.track_tool(TrackToolOptions {
    name: "web_search".into(),
    input: Some(json!({ "query": "weather in NYC" })),
    output: Some(json!({ "results": ["Sunny, 72°F"] })),
    duration: Some(Duration::from_millis(150)),
    ..Default::default()
});

// A failed one
agent.track_tool(TrackToolOptions {
    name: "database_query".into(),
    input: Some(json!({ "query": "SELECT * FROM users" })),
    duration: Some(Duration::from_millis(50)),
    error: Some("connection timeout".into()),
    ..Default::default()
});

agent
    .finish(FinishOptions {
        output: "It's sunny in NYC.".into(),
        ..Default::default()
    })
    .await?;
```

```java Java icon="java" theme={null}
Interaction agent = raindrop.begin(BeginOptions.builder()
        .userId("user-123")
        .event("agent_run")
        .input("What's the weather in NYC?")
        .build());

// A tool call that already happened
agent.trackTool(TrackToolOptions.builder("web_search")
        .input(Map.of("query", "weather in NYC"))
        .output(Map.of("results", List.of("Sunny, 72°F")))
        .durationMs(150)
        .build());

// A failed one
agent.trackTool(TrackToolOptions.builder("database_query")
        .input(Map.of("query", "SELECT * FROM users"))
        .durationMs(50)
        .error("connection timeout")
        .build());

agent.setOutput("It's sunny in NYC.").finish();
```

```swift Swift icon="swift" theme={null}
let agent = raindrop.begin(BeginOptions(
  userId: "user-123",
  event: "agent_run",
  input: "What's the weather in NYC?"
))

// A tool call that already happened
agent.trackTool(
  "web_search",
  input: ["query": "weather in NYC"],
  output: ["results": ["Sunny, 72°F"]],
  duration: .milliseconds(150)
)

// A failed one
agent.trackTool(
  "database_query",
  input: ["query": "SELECT * FROM users"],
  error: "connection timeout",
  duration: .milliseconds(50)
)

await agent.finish(output: "It's sunny in NYC.")
```

> [tab] TypeScript
>
> For more control over tool span tracking, use `trackTool` or `startToolSpan`.
>
> **`trackTool`: retroactive logging**
>
> | Parameter | Type | Description |
> | - | - | - |
> | `name` | `string` | Name of the tool |
> | `input` | `unknown` | Input passed to the tool |
> | `output` | `unknown` | Output returned by the tool |
> | `durationMs` | `number` | Duration in milliseconds |
> | `startTime` | `Date \| number` | When the tool started (defaults to `endTime - durationMs`) |
> | `endTime` | `Date \| number` | When the tool finished (defaults to `startTime + durationMs`, or now). Wins over `durationMs` when both are given |
> | `error` | `Error \| string` | Error if the tool failed |
> | `properties` | `Record<string, string>` | Additional metadata |
> | `traceId` | `string` | W3C trace ID override (requires `parentSpanId`; see Direct tool spans below) |
> | `parentSpanId` | `string` | W3C parent span ID override (requires `traceId`) |
>
> **`startToolSpan`: real-time tracking**
>
> Use `startToolSpan` to track a tool as it executes:
>
> ```typescript theme={null}
> const interaction = raindrop.begin({
>   eventId: "my-event",
>   event: "agent_run",
>   userId: "user_123",
>   input: "Process this data",
> });
>
> const toolSpan = interaction.startToolSpan({
>   name: "api_call",
>   properties: { endpoint: "/api/data" },
>   inputParameters: { method: "GET", path: "/api/data" },
> });
>
> try {
>   const result = await fetchData();
>   toolSpan.setOutput(result);
> } catch (error) {
>   toolSpan.setError(error instanceof Error ? error : String(error));
> } finally {
>   toolSpan.end();
> }
>
> await interaction.finish({ output: "Data processed" });
> ```
>
> | Method | Description |
> | - | - |
> | `setInput(input)` | Set the input (JSON stringified if object) |
> | `setOutput(output)` | Set the output (JSON stringified if object) |
> | `setError(error)` | Mark the span as failed |
> | `end()` | End the span (required when execution completes) |
>
> **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`:
>
> ```typescript theme={null}
> raindrop.resumeInteraction(eventId).trackTool({
>   name,
>   input,
>   output,
>   startTime,
>   endTime,
>   traceId,       // 32 hexadecimal characters
>   parentSpanId,  // 16 hexadecimal characters
> });
> ```
>
> Invalid or incomplete overrides are ignored, and the SDK falls back to the active OpenTelemetry context.
>
> For `withTool` and tracing setup, see [Tracing](https://raindrop.ai/docs/sdk/tracing.md#tool-calls).

> [tab] Python
>
> If your OTEL exporter pipeline is unavailable or flaky, set
> `bypass_otel_for_tools=True` in `raindrop.init(...)` to ship only
> `interaction.track_tool()` spans directly to `POST /v1/traces`.
>
> | Parameter | Type | Description |
> | - | - | - |
> | `name` | `str` | Name of the tool |
> | `input` | `Any` | Input passed to the tool |
> | `output` | `Any` | Output returned by the tool |
> | `duration_ms` | `float \| int` | Duration in milliseconds |
> | `start_time` | `datetime \| int \| float \| str` | When the tool started (defaults to `end_time - duration_ms`) |
> | `end_time` | `datetime \| int \| float \| str` | When the tool finished (defaults to `start_time + duration_ms`, or now). Wins over `duration_ms` when both are given |
> | `parent` | `Any` | A saved span to nest under: anything with `trace_id` and `span_id` (lowercase hex) as attributes or keys |
> | `error` | `BaseException \| str` | Error if the tool failed |
> | `properties` | `Dict[str, Any]` | Additional metadata |
> | `version` | `int` | Optional version number for the tool |
>
> Times can be `datetime` objects, ISO 8601 strings, or epoch seconds or milliseconds.
>
> To log a tool call later, or from another process, under a span recorded earlier, save that span's IDs (for example `span.trace_id` and `span.span_id` from `interaction.start_span()`) and pass them as `parent`:
>
> ```python theme={null}
> interaction = raindrop.resume_interaction(event_id=saved_event_id)
> interaction.track_tool(
>     name="web_search",
>     input={"query": "weather in NYC"},
>     output={"results": ["Sunny, 72°F"]},
>     start_time=started_at,
>     end_time=finished_at,
>     parent={"trace_id": saved_trace_id, "span_id": saved_span_id},
> )
> ```
>
> `track_tool` records nothing unless tracing is enabled (`tracing_enabled=True`).
>
> For the `tool_span` context manager and tracing setup, see [Tracing](https://raindrop.ai/docs/sdk/tracing.md#tool-calls).

> [tab] Go
>
> For more control over tool span tracking, use `TrackTool` or `StartToolSpan`.
>
> **`TrackTool`: retroactive logging**
>
> | Parameter | Type | Description |
> | - | - | - |
> | `Name` | `string` | Name of the tool |
> | `Input` | `any` | Input passed to the tool |
> | `Output` | `any` | Output returned by the tool |
> | `Duration` | `time.Duration` | Duration of the tool call |
> | `StartTime` | `time.Time` | When the tool started (defaults to `now - Duration`) |
> | `EndTime` | `time.Time` | When the tool ended (wins over `Duration` when both are set) |
> | `ParentContext` | `*raindrop.SpanContext` | A saved parent span (`TraceID`, 32 lowercase hex; `SpanID`, 16 lowercase hex), for example ids another process saved with the event |
> | `Error` | `error` | Error if the tool failed |
> | `Properties` | `map[string]any` | Additional metadata |
>
> **`StartToolSpan`: real-time tracking**
>
> Use `StartToolSpan` to track a tool as it executes:
>
> ```go theme={null}
> toolSpan := interaction.StartToolSpan("api_call", raindrop.ToolOptions{
> 	Input:      map[string]any{"method": "GET", "path": "/api/data"},
> 	Properties: map[string]any{"endpoint": "/api/data"},
> })
>
> result, err := fetchData()
> if err != nil {
> 	toolSpan.SetError(err)
> } else {
> 	toolSpan.SetOutput(result)
> }
> toolSpan.End()
> ```
>
> | Method | Description |
> | - | - |
> | `SetInput(any)` | Set the input (JSON stringified if object) |
> | `SetOutput(any)` | Set the output (JSON stringified if object) |
> | `SetError(error)` | Mark the span as failed |
> | `End()` | End the span (required when done) |
>
> For `WithTool` and the rest of tracing, see [Tracing](https://raindrop.ai/docs/sdk/tracing.md#tool-calls).

> [tab] Rust
>
> Tool spans use the dedicated wire format (`traceloop.span.kind=tool`) so they surface in the dashboard's `event.toolCalls[]` array.
>
> ```rust theme={null}
> use raindrop::ToolOptions;
> use serde_json::json;
>
> let tool = interaction.start_tool_span("weather_lookup", ToolOptions {
>     input: Some(json!({ "location": "San Francisco" })),
>     ..Default::default()
> });
> tool.set_output(&json!({ "forecast": "sunny" }));
> tool.end();
> ```
>
> | Field | Type | Description |
> | - | - | - |
> | `name` | `String` | Tool name |
> | `input` | `Option<Value>` | JSON input |
> | `output` | `Option<Value>` | JSON output |
> | `duration` | `Option<Duration>` | Total duration |
> | `start_time` | `Option<OffsetDateTime>` | When the tool started (defaults to `end_time - duration`) |
> | `end_time` | `Option<OffsetDateTime>` | When the tool ended (defaults to now) |
> | `error` | `Option<String>` | Error message; sets `status=ERROR` |
> | `parent` | `Option<Span>` | Parent span for nesting; may be a span saved by another process (see below) |
> | `properties` | `BTreeMap<String, _>` | Additional metadata |
>
> To log a tool call later, or from another process, under a span recorded earlier, save that span's trace and span IDs (lowercase hex, 32 and 16 characters) and rebuild a parent with `Span::from_saved_ids`. It returns `None` for malformed IDs. The rebuilt span is only a parent reference: ending it or setting attributes on it does nothing.
>
> ```rust theme={null}
> use raindrop::Span;
>
> let parent = Span::from_saved_ids(&saved_trace_id, &saved_span_id);
> interaction.track_tool(TrackToolOptions {
>     name: "web_search".into(),
>     parent,
>     end_time: Some(finished_at),
>     duration: Some(Duration::from_millis(150)),
>     ..Default::default()
> });
> ```
>
> For functional wrapping, the SDK exposes `with_tool` and `with_tool_async` free helpers that run a closure inside a tool span and JSON-serialize the result onto `traceloop.entity.output`:
>
> ```rust theme={null}
> let result = raindrop::with_tool::<_, _, std::io::Error>(
>     &interaction,
>     "park_check",
>     ToolOptions {
>         input: Some(json!({ "location": "Dolores Park" })),
>         ..Default::default()
>     },
>     || Ok(json!({ "recommendation": "yes" })),
> )?;
> ```
>
> For LLM spans and the rest of tracing, see [Tracing](https://raindrop.ai/docs/sdk/tracing.md#model-calls).

> [tab] Java
>
> Record a tool call after the fact with `trackTool`, giving its start and either an end time or a
> duration (the end time wins when both are set). To place it under a span recorded earlier, possibly
> by another process, save that span's IDs with `traceIdHex()` and `spanIdHex()` and pass them to
> `parent(traceId, spanId)`. `parent()` also accepts the base64 values returned by `traceId()` and
> `spanId()`. Malformed IDs are ignored and the span stays under the interaction's root.
>
> | Builder method | Description |
> | - | - |
> | `.input(Object)` | Input passed to the tool |
> | `.output(Object)` | Output returned by the tool |
> | `.error(String)` | Error message; marks the span as failed |
> | `.startTime(Instant)` | When the tool started (defaults to its duration before the end) |
> | `.durationMs(long)` | Duration in milliseconds |
> | `.endTime(Instant)` | When the tool ended (defaults to start plus duration, or now) |
> | `.parent(String, String)` | Saved parent span: trace ID and span ID, from `traceIdHex()` / `spanIdHex()` (hex) or `traceId()` / `spanId()` (base64) |
>
> ```java theme={null}
> // When the parent span runs, save its IDs (for example with the job).
> String savedTraceId = span.traceIdHex();
> String savedSpanId = span.spanIdHex();
>
> // Later, possibly in another process:
> it.trackTool(TrackToolOptions.builder("web_search")
>         .input(Map.of("query", "weather in NYC"))
>         .output(Map.of("forecast", "sunny"))
>         .startTime(startedAt).endTime(endedAt)
>         .parent(savedTraceId, savedSpanId)
>         .build());
> ```
>
> For tool spans you start and end yourself (`startTool`), see [Tracing](https://raindrop.ai/docs/sdk/tracing.md#tool-calls).

> [tab] Swift
>
> `trackTool(_:input:output:error:duration:startTime:endTime:parent:)` records a call that already happened: give a start time plus a `duration` or an `endTime` (the end wins when both are given). `error` is a message string and marks the span as failed.
>
> To nest it under a span recorded earlier, even in another process, save that span's `span.ref` (a `SpanRef` of hex trace and span ids) and pass `parent: .ref(savedRef)`.
>
> For tool spans you start and end yourself (`startToolSpan`, `withTool`), see [Tracing](https://raindrop.ai/docs/sdk/tracing.md#tool-calls).

## Track feedback (signals)

Attach feedback, such as a thumbs-down with a comment, to an event by its id.

```typescript TypeScript icon="https://mintcdn.com/dawn-a6c57108/72ImDkk3v1Xlc68q/images/icons/typescript.svg?fit=max&auto=format&n=72ImDkk3v1Xlc68q&q=85&s=d8ece0bb7b221c02bae30c5db9c578b3" theme={null}
await raindrop.trackSignal({
  eventId: savedEventId,
  name: "thumbs_down",
  type: "feedback",
  sentiment: "NEGATIVE",
  comment: "Answer was off-topic",
});
```

```python Python icon="python" theme={null}
raindrop.track_signal(
    event_id=saved_event_id,
    name="thumbs_down",
    signal_type="feedback",
    sentiment="NEGATIVE",
    comment="Answer was off-topic",
)
```

```go Go icon="golang" theme={null}
_ = client.TrackSignal(ctx, raindrop.Signal{
	EventID:   savedEventID,
	Name:      "thumbs_down",
	Type:      "feedback",
	Sentiment: "NEGATIVE",
	Comment:   "Answer was off-topic",
})
```

```rust Rust icon="rust" theme={null}
client
    .track_signal(Signal {
        event_id: SAVED_EVENT_ID.into(),
        name: "thumbs_down".into(),
        kind: SignalKind::FEEDBACK.into(),
        sentiment: "NEGATIVE".into(),
        comment: "Answer was off-topic".into(),
        ..Default::default()
    })
    .await?;
```

```java Java icon="java" theme={null}
raindrop.trackSignal(Signal.builder()
        .eventId(savedEventId)
        .name("thumbs_down")
        .type(Signal.Type.FEEDBACK)
        .sentiment(Sentiment.NEGATIVE)
        .comment("Answer was off-topic")
        .build());
```

```swift Swift icon="swift" theme={null}
raindrop.trackSignal(Signal(
  eventId: savedEventId,
  name: "thumbs_down",
  type: .feedback,
  sentiment: .negative,
  comment: "Answer was off-topic"
))
```

> [tab] TypeScript
>
> Signals capture quality ratings on AI events. Use `trackSignal()` with the same `eventId` from `begin()` or `trackAi()`.
>
> | Parameter | Type | Description |
> | - | - | - |
> | `eventId` | `string` | The ID of the AI event you're evaluating |
> | `name` | `"thumbs_up"`, `"thumbs_down"`, `string` | Signal name |
> | `type` | `"default"`, `"standard"`, `"feedback"`, `"edit"`, `"agent"`, `"agent_internal"` | Optional, defaults to `"default"` |
> | `comment` | `string` | User comment (for `feedback` signals) |
> | `after` | `string` | User's edited content (for `edit` signals) |
> | `sentiment` | `"POSITIVE"`, `"NEGATIVE"` | Signal sentiment (defaults to `"NEGATIVE"`) |

> [tab] Python
>
> Signals are used to attach user feedback (such as thumbs down or thumbs up) or other labels to existing events. Use the `track_signal` function.
>
> * `event_id` (str): The ID of the event to attach the signal to.
> * `name` (str): Name of the signal (e.g., "thumbs\_up", "copied\_code").
> * `signal_type` (Literal\["default", "feedback", "edit"]): Type of signal. Defaults to `"default"`.
>   * For `"feedback"` signals, a `"comment"` string must be included in the `properties`.
>   * For `"edit"` signals, an `"after"` string (representing the content after edit) must be included in the `properties`.
> * `timestamp` (Optional\[str]): ISO 8601 formatted timestamp. Defaults to current UTC time.
> * `properties` (Optional\[Dict\[str, Any]]): Additional properties for the signal.
> * `attachment_id` (Optional\[str]): ID of a specific attachment within the original event to associate this signal with.
> * `sentiment` (Optional\[Literal\["POSITIVE", "NEGATIVE"]]): Optional sentiment indicating whether the signal is positive or negative.
> * `comment` (Optional\[str]): Convenience parameter for feedback signals. If provided, it's added to `properties` as `{"comment": "your comment"}`.
> * `after` (Optional\[str]): Convenience parameter for edit signals. If provided, it's added to `properties` as `{"after": "new content"}`.
>
> ```python theme={null}
> raindrop.track_signal(event_id="evt_abc123", name="thumbs_up", sentiment="POSITIVE")
> ```

> [tab] Go
>
> Signals capture quality ratings on AI events. Use `TrackSignal()` with the same event ID from `Begin()` or `TrackAI()`.
>
> | Parameter | Type | Description |
> | - | - | - |
> | `EventID` | `string` | The ID of the AI event you're evaluating |
> | `Name` | `string` | Signal name (e.g. `"thumbs_up"`, `"thumbs_down"`) |
> | `Type` | `string` | `"default"`, `"feedback"`, `"edit"`, `"agent"`, etc. Defaults to `"default"` |
> | `Sentiment` | `string` | `"POSITIVE"` or `"NEGATIVE"` |
> | `Comment` | `string` | Free-text feedback |
> | `After` | `string` | The corrected or preferred output, for edit signals |
> | `Properties` | `map[string]any` | Additional metadata |
> | `AttachmentID` | `string` | Optional attachment ID to associate the signal with |

> [tab] Rust
>
> Signals capture quality ratings on AI events. Use `track_signal()` with the same event ID from `begin()` or `track_ai()`.
>
> | Field | Type | Description |
> | - | - | - |
> | `event_id` | `String` | The ID of the AI event you're evaluating |
> | `name` | `String` | Signal name (e.g. `"thumbs_up"`, `"thumbs_down"`) |
> | `kind` | `String` | One of `SignalKind::{DEFAULT, STANDARD, FEEDBACK, EDIT, AGENT, AGENT_INTERNAL}`. Defaults to `"default"`. |
> | `sentiment` | `String` | `"POSITIVE"` or `"NEGATIVE"` |
> | `comment` | `String` | Merged into `properties.comment` for `feedback` signals |
> | `after` | `String` | Merged into `properties.after` for `edit` signals |
> | `attachment_id` | `String` | Optional attachment ID to associate the signal with |
> | `properties` | `BTreeMap<String, _>` | Additional metadata |

> [tab] Java
>
> Signals capture quality ratings on AI events. Use `trackSignal()` with the same event id returned
> by `begin()` or `trackAi()`.
>
> | Field | Type | Description |
> | - | - | - |
> | `eventId` | `String` | The id of the AI event you're evaluating |
> | `name` | `String` | Signal name (e.g. `"thumbs_up"`, `"thumbs_down"`) |
> | `type` | `Signal.Type` | One of `DEFAULT`, `STANDARD`, `FEEDBACK`, `EDIT`, `AGENT`, `AGENT_INTERNAL` |
> | `sentiment` | `Sentiment` | `POSITIVE` or `NEGATIVE` |
> | `comment` | `String` | Merged into `properties.comment` (for `FEEDBACK` signals) |
> | `after` | `String` | Merged into `properties.after` (for `EDIT` signals) |
> | `attachmentId` | `String` | Optional attachment id to associate the signal with |
> | `properties` | `Map<String,Object>` | Additional metadata |

> [tab] Swift
>
> | Field | Description |
> | - | - |
> | `eventId` | The AI event you're rating |
> | `name` | Signal name, e.g. `thumbs_up` |
> | `type` | `.default`, `.feedback`, `.edit`, `.standard`, `.agent`, `.agentInternal` |
> | `sentiment` | `.positive` or `.negative` |
> | `comment` | Sent as `properties.comment` |
> | `after` | Sent as `properties.after` (edit signals) |
> | `attachmentId`, `properties`, `timestamp` | Optional |

## Identify users

Attach traits to a user id; they show on that user's events. For `plan`, we recommend `free`, `paid` or `trial`.

```typescript TypeScript icon="https://mintcdn.com/dawn-a6c57108/72ImDkk3v1Xlc68q/images/icons/typescript.svg?fit=max&auto=format&n=72ImDkk3v1Xlc68q&q=85&s=d8ece0bb7b221c02bae30c5db9c578b3" theme={null}
raindrop.setUserDetails({
  userId: "user-123",
  traits: { name: "Jane", plan: "paid" },
});
```

```python Python icon="python" theme={null}
raindrop.identify(
    user_id="user-123",
    traits={"name": "Jane", "plan": "paid"},
)
```

```go Go icon="golang" theme={null}
_ = client.Identify(ctx, raindrop.User{
	UserID: "user-123",
	Traits: map[string]any{"name": "Jane", "plan": "paid"},
})
```

```rust Rust icon="rust" theme={null}
client
    .identify(User {
        user_id: "user-123".into(),
        traits: BTreeMap::from([
            ("name".into(), json!("Jane")),
            ("plan".into(), json!("paid")),
        ]),
    })
    .await?;
```

```java Java icon="java" theme={null}
raindrop.identify(IdentifyEvent.builder()
        .userId("user-123")
        .trait("name", "Jane")
        .trait("plan", "paid")
        .build());
```

```swift Swift icon="swift" theme={null}
raindrop.identify(userId: "user-123", traits: ["name": "Jane", "plan": "paid"])
```

> [tab] TypeScript
>
> The method is `setUserDetails()`.

> [tab] Python
>
> To associate traits with users, you can use the `identify` function. It takes:
>
> * `user_id` (str): The unique identifier of the user.
> * `traits` (Dict\[str, Union\[str, int, bool, float]]): The traits associated with the user.

> [tab] Go
>
> `Identify()` queues the traits and returns immediately.

> [tab] Rust
>
> `identify()` queues the traits and returns immediately.

> [tab] Java
>
> Add traits one at a time with `.trait(key, value)`.

> [tab] Swift
>
> `identify(userId:traits:)` queues the traits and returns immediately.

## Configuration

Every SDK takes the same core options: the write key, a [project](https://raindrop.ai/docs/platform/projects.md), debug logging and the deadline for flushing. The full list for each language is below.

```typescript TypeScript icon="https://mintcdn.com/dawn-a6c57108/72ImDkk3v1Xlc68q/images/icons/typescript.svg?fit=max&auto=format&n=72ImDkk3v1Xlc68q&q=85&s=d8ece0bb7b221c02bae30c5db9c578b3" theme={null}
const configured = new Raindrop({
  writeKey: process.env.RAINDROP_WRITE_KEY,
  projectId: "support-prod", // omit for the default Production project
  debugLogs: process.env.NODE_ENV !== "production",
  flushTimeoutMs: 5_000, // deadline for forceFlush() and close()
});
```

```python Python icon="python" theme={null}
raindrop.init(
    os.getenv("RAINDROP_WRITE_KEY"),
    project_id="support-prod",  # omit for the default Production project
    flush_timeout=5,  # deadline in seconds for flush() and shutdown()
    tracing_enabled=True,  # needed for spans, including track_tool()
)
raindrop.set_debug_logs(os.getenv("ENV") != "production")
```

```go Go icon="golang" theme={null}
client, err := raindrop.New(
	raindrop.WithWriteKey(os.Getenv("RAINDROP_WRITE_KEY")),
	raindrop.WithProjectID("support-prod"), // omit for the default Production project
	raindrop.WithDebug(os.Getenv("ENV") != "production"),
	raindrop.WithFlushTimeout(5*time.Second), // deadline for Flush() and Close()
)
```

```rust Rust icon="rust" theme={null}
use std::time::Duration;

let client = Client::builder()
    .write_key(std::env::var("RAINDROP_WRITE_KEY").unwrap_or_default())
    .project_id("support-prod") // omit for the default Production project
    .debug(std::env::var("ENV").as_deref() != Ok("production"))
    .flush_timeout(Duration::from_secs(5)) // deadline for flush() and close()
    .build()?;
```

```java Java icon="java" theme={null}
Raindrop raindrop = new Raindrop(RaindropConfig.builder()
        .writeKey(System.getenv("RAINDROP_WRITE_KEY"))
        .projectId("support-prod") // omit for the default Production project
        .debugLogs(!"production".equals(System.getenv("ENV")))
        .flushTimeoutMs(5_000) // deadline for flush() and close()
        .build());
```

```swift Swift icon="swift" theme={null}
var config = RaindropConfiguration(
  writeKey: ProcessInfo.processInfo.environment["RAINDROP_WRITE_KEY"],
  projectId: "support-prod"  // omit for the default Production project
)
config.debug = ProcessInfo.processInfo.environment["ENV"] != "production"
config.flushDeadline = .seconds(5)  // deadline for finish(), flush() and close()
let raindrop = RaindropClient(configuration: config)
```

> [tab] TypeScript
>
> | Option | Type | Default | Description |
> | - | - | - | - |
> | `writeKey` | `string` | — | Your Raindrop write key. Without one, the SDK sends nothing to Raindrop and prints how to get a key |
> | `projectId` | `string` | `"default"` | Send events to a specific [project](https://raindrop.ai/docs/platform/projects.md); omit for the default **Production** project |
> | `debugLogs` | `boolean` | `false` | Print queued events and tracing info to console |
> | `disabled` | `boolean` | `false` | Disable all tracking (useful for test environments) |
> | `redactPii` | `boolean` | `false` | Enable client-side PII redaction |
> | `redactPiiFields` | `{ event?, signal?, identify? }` | — | More fields to redact when `redactPii` is on; see [PII redaction](https://raindrop.ai/docs/security/pii-redaction.md#typescript) |
> | `flushTimeoutMs` | `number` | `10000` | Deadline for `forceFlush()` and `close()`, retries included |
> | `maxTextFieldChars` | `number` | `1000000` | Per-field character cap on AI input/output and tool/span content; longer values are truncated |
> | `useExternalOtel` | `boolean` | `false` | Use your own OpenTelemetry setup instead of Raindrop's built-in one |
> | `bypassOtelForTools` | `boolean` | `false` | Ship `trackTool()` / `withTool()` / `startToolSpan()` spans directly via HTTP, bypassing the OTEL exporter |
> | `disableBatching` | `boolean` | `true` in dev | Disable span batching for faster feedback in development |
> | `appGit` | `AppGitOptions \| false` | automatic | Configure application Git provenance, or disable all client enrichment with `false` |
>
> Call `await raindrop.close()` before your process exits to flush any buffered events.

> [tab] Python
>
> You'll see your write key when you log into [app.raindrop.ai](https://app.raindrop.ai). Besides the write key, `raindrop.init(...)` takes, among others:
>
> * `tracing_enabled=True`: enable tracing for the task and tool decorators and auto-instrumentation (see [Model calls](https://raindrop.ai/docs/sdk/tracing.md#model-calls))
> * `bypass_otel_for_tools=True`: bypass the OTEL exporters for `interaction.track_tool()` spans only
> * `flush_timeout`: deadline in seconds for `flush()` and `shutdown()` (default 10)
> * `project_id`, `app_git` and `max_text_field_chars`: see Projects and Application Git provenance below, and [Attachments](#attachments)
>
> The SDK has several configurable parameters:
>
> * `max_queue_size`: Maximum number of events to store in the buffer (default: 10\_000)
> * `upload_size`: Number of events to send in a single API request (default: 10)
> * `upload_interval`: Time interval in seconds between automatic flushes (default: 1.0). You can modify these parameters if needed:
>
> ```python theme={null}
> raindrop.max_queue_size = 20_000
> raindrop.upload_size = 200
> raindrop.upload_interval = 2.0
> ```
>
> If you want to enable debug logs to see the events being added to the buffer, you can use the `set_debug_logs` function:
>
> ```python theme={null}
> raindrop.set_debug_logs(True)
> ```

> [tab] Go
>
> | Option | Description | Default |
> | - | - | - |
> | `WithWriteKey(string)` | Your Raindrop API key (required) | — |
> | `WithProjectID(string)` | Send events to a specific [project](https://raindrop.ai/docs/platform/projects.md); omit for the default **Production** project | `"default"` |
> | `WithDebug(bool)` | Print debug logs to the configured logger | `false` |
> | `WithEndpoint(string)` | Override the API endpoint | Raindrop API |
> | `WithHTTPClient(*http.Client)` | Use a custom HTTP client | 15s timeout |
> | `WithLogger(*slog.Logger)` | Use a custom `slog.Logger` | `slog.Default()` |
> | `WithServiceName(string)` | Override the service name in traces | `"raindrop.go-sdk"` |
> | `WithServiceVersion(string)` | Override the service version in traces | SDK version |
> | `WithPartialFlushInterval(time.Duration)` | How often to flush buffered events | `1s` |
> | `WithTraceFlushInterval(time.Duration)` | How often to flush buffered spans | `1s` |
> | `WithTraceBatchSize(int)` | Max spans per trace export batch | `50` |
> | `WithTraceQueueSize(int)` | Max spans held in the queue | `5000` |
> | `WithAppGit(AppGitOptions)` | Configure application Git values and discovery | automatic |
> | `WithAppGitDisabled()` | Disable all client-level Git enrichment | — |
> | `WithFlushTimeout(time.Duration)` | The one deadline for `Flush()` and `Close()`; each request's timeout is clamped to what is left of it | `10s` |
> | `WithCloseTimeout(time.Duration)` | Former name of `WithFlushTimeout`; sets the same deadline | `10s` |
>
> If `writeKey` is omitted, the client becomes a no-op instead of failing.

> [tab] Rust
>
> | Builder method | Description | Default |
> | - | - | - |
> | `.write_key(&str)` | Your Raindrop API key. Empty/missing key → SDK becomes a no-op. | — |
> | `.project_id(&str)` | Send events to a specific [project](https://raindrop.ai/docs/platform/projects.md); omit for the default **Production** project | `"default"` |
> | `.endpoint(&str)` | Override the API endpoint | `https://api.raindrop.ai/v1/` |
> | `.debug(bool)` | Verbose debug logging via `tracing` | `false` |
> | `.partial_flush_interval(Duration)` | Periodic event flush. `Duration::ZERO` disables periodic flush | `1s` |
> | `.trace_flush_interval(Duration)` | Periodic span flush. `Duration::ZERO` disables periodic flush | `1s` |
> | `.trace_max_batch_size(usize)` | Max spans per trace export request | `50` |
> | `.trace_max_queue_size(usize)` | Max spans buffered before back-pressuring | `5000` |
> | `.max_attempts(u32)` | HTTP attempts per request. `1` disables retries. | `3` |
> | `.base_delay(Duration)` | Backoff base (exponential, ±20% jitter) | `1s` |
> | `.jitter_fraction(f64)` | Backoff jitter fraction (0.0–1.0) | `0.2` |
> | `.event_max_queue_size(usize)` | Max buffered events and queued sends; past it, new items are dropped with a warning | `5000` |
> | `.request_timeout(Duration)` | Per-attempt timeout on every request to Raindrop | `30s` |
> | `.flush_timeout(Duration)` | Deadline for `flush()` and `close()`. `Duration::ZERO` disables it | `10s` |
> | `.close_timeout(Duration)` | Overrides the deadline for `close()` alone | same as `flush_timeout` |
> | `.max_text_field_chars(usize)` | Per-field character cap on AI input/output and tool/LLM span content; longer values are truncated | `1_000_000` |
> | `.service_name(&str)` | OTLP `resource.service.name` | `"raindrop.rust-sdk"` |
> | `.library_name(&str)` | `$context.library.name` reported with each event | `"raindrop-rust"` |
> | `.library_version(&str)` | `$context.library.version` | crate version |
> | `.http_client(reqwest::Client)` | Inject a custom `reqwest::Client` (`request_timeout` still applies) | new client w/ 30s timeout |
> | `.local_workshop_url(&str)` | Mirror cloud-bound posts to a local Workshop daemon | auto-detected localhost |
> | `.disable_local_workshop()` | Disable env/probe-based local Workshop mirroring | — |
> | `.app_git(AppGitConfig)` | Configure application Git values and discovery | automatic |
> | `.disable_app_git()` | Disable all client-level Git enrichment | — |
>
> Call `client.close().await` before your process exits to flush buffered events and spans. If `write_key` is empty and no local Workshop is configured, the client becomes a no-op (zero HTTP calls) instead of failing.
>
> Local Workshop mirroring is enabled when `RAINDROP_LOCAL_DEBUGGER` is set to a URL, when `RAINDROP_WORKSHOP` is a truthy value or URL, or when the SDK can connect to the default Workshop daemon at `http://localhost:5899/v1/`.

> [tab] Java
>
> | Builder method | Description | Default |
> | - | - | - |
> | `.writeKey(String)` | Your Raindrop write key. Empty/missing → SDK becomes a no-op. | — |
> | `.projectId(String)` | Send events to a specific [project](https://raindrop.ai/docs/platform/projects.md); omit for the default **Production** project | `"default"` |
> | `.endpoint(String)` | Override the ingest endpoint. | `https://api.raindrop.ai/v1/` |
> | `.bufferSize(int)` | Flush once this many events are buffered. | `50` |
> | `.bufferTimeoutMs(long)` | Flush after this delay even if the buffer isn't full. | `1000` |
> | `.redactPii(boolean)` | Redact emails, phone numbers, credit cards, SSNs, and secrets from AI input/output and span input, output and errors. | `false` |
> | `.maxTextFieldChars(int)` | Per-field character cap; oversized fields are truncated. | `1_000_000` |
> | `.flushTimeoutMs(long)` | Deadline for `flush()` and `close()`, retries included. | `10000` |
> | `.debugLogs(boolean)` | Verbose logging to stderr. | `false` |
> | `.localWorkshopUrl(String)` | Mirror cloud-bound posts to a local Workshop daemon. | auto-detected localhost |
> | `.disableLocalWorkshop()` | Disable env/probe-based local Workshop mirroring. | — |
> | `.appGit(AppGit)` | Configure application Git values and discovery. | automatic |
> | `.appGit(false)` | Disable all client-level Git enrichment. | — |
> | `.disabled(boolean)` | When true, the SDK sends nothing. | `false` |
>
> Call `raindrop.close()` (or `shutdown()`) before your process exits to flush buffered events and
> spans within `flushTimeoutMs`. If `writeKey` is empty and no local Workshop is configured, the
> client is a no-op (zero HTTP calls) rather than an error.
>
> Local Workshop mirroring is enabled when `RAINDROP_LOCAL_DEBUGGER` is set to a URL, when
> `RAINDROP_WORKSHOP` is a truthy value or URL, or when the SDK can reach the default Workshop daemon
> at `http://localhost:5899/v1/`.

> [tab] Swift
>
> | Option | Default | Description |
> | - | - | - |
> | `writeKey` | — | Write key. Empty → no-op client |
> | `projectId` | org default | Project slug, sent as `X-Raindrop-Project-Id`. Omitted, empty or `"default"` uses the default (Production) project. An invalid slug is ignored with a warning and the default project is used |
> | `endpoint` | `https://api.raindrop.ai/v1/` | Ingestion base URL |
> | `flushInterval` | 1 s | Background send interval |
> | `flushDeadline` | 10 s | Deadline for `finish`, `flush` and `close`, retries included |
> | `maxRetryAfter` | 30 s | Cap on an honoured `Retry-After` |
> | `requestTimeout` | 2 s | Idle timeout per request; timed-out payloads are retried later |
> | `maxTextFieldChars` | 1,000,000 | `input` / `output` are truncated beyond this, ending in `...[truncated by raindrop]` |
> | `redactPii` | `false` | Redact emails, phone numbers, card numbers, SSNs, addresses and passwords (same rules as the TypeScript SDK) from event `input` / `output` and from span content: prompts, responses, tool input/output and error messages. User ids, properties and other span attributes are not changed |
> | `beforeSend` | — | Edit or drop each payload before it's sent |
> | `appGit` | from environment | Build identity (see below) |
> | `tracingEnabled` | `true` | Export spans |
> | `storageDirectory` | Application Support on iOS/tvOS/watchOS/visionOS; none elsewhere | Where unsent payloads are kept between launches |
> | `storageProtection` | `.completeUntilFirstUserAuthentication` | Data-protection class for the offline storage file. Use `.complete` if your policy requires it to be unreadable while the device is locked |
> | `handleAppLifecycle` | `true` | Flush when the app goes to the background and when the network returns |
> | `localWorkshopURL` | from environment | Mirror requests to a local Raindrop Workshop |
> | `transport` | `URLSession` | Custom HTTP transport |
> | `logHandler`, `debug` | stderr, `false` | Logging. Warnings are always logged (rate-limited) |

## Projects

Pass a project id when you create the client to scope every event from it to a specific [project](https://raindrop.ai/docs/platform/projects.md). 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](https://raindrop.ai/docs/platform/projects.md) for isolation, archival, and the full behavior table.

> [tab] TypeScript
>
> Pass `projectId` to scope every event from a client to a specific [project](https://raindrop.ai/docs/platform/projects.md). 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](https://raindrop.ai/docs/platform/projects.md) for isolation, archival, and the full behavior table.
>
> **Multiple projects in one process**
>
> When 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.
>
> ```typescript theme={null}
> import { Raindrop } from "raindrop-ai";
>
> // One long-lived client per project, created at startup and reused.
> const rdSupport = new Raindrop({ writeKey: RAINDROP_API_KEY, projectId: "support-prod" });
> const rdBilling = new Raindrop({ writeKey: RAINDROP_API_KEY, projectId: "billing-prod" });
>
> // Each client owns its delivery pipeline — events ship to its own project.
> const interaction = rdBilling.begin({ eventId: crypto.randomUUID(), userId: "u1", event: "billing-chat", input: "..." });
> interaction.trackTool({ name: "invoice_lookup", input: {}, output: {} });
> await interaction.finish({ output: "..." });      // -> billing-prod
>
> rdSupport.trackAi({ userId: "u1", event: "support-chat", input: "q", output: "a" });  // -> support-prod
> ```
>
> **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()`:
>
> ```typescript theme={null}
> await rdBilling.asCurrent(async () => {
>   await openai.chat.completions.create({ ... });  // spans -> billing-prod
> });
> ```
>
> > [!WARNING]
> >
> > 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.
>
> > [!NOTE]
> >
> > 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.

> [tab] Python
>
> Pass `project_id` to `raindrop.init(...)` to scope events to a specific [project](https://raindrop.ai/docs/platform/projects.md). This sets the `X-Raindrop-Project-Id` header on each request.
>
> Omitting `project_id` (or passing `"default"`) sends to the default **Production** project, which is the existing behavior. See [Projects](https://raindrop.ai/docs/platform/projects.md) for isolation, archival, and the full behavior table.
>
> **Multiple projects in one process**
>
> When one service runs several agents that
> should report to **different** projects — e.g. a FastAPI app serving a support
> agent and a billing agent — create one `Raindrop` client per project instead
> of using the module-level `init()`. This is the same instance-based shape as
> the other SDKs:
>
> ```python theme={null}
> from raindrop import Raindrop
>
> # One long-lived client per project, created at startup and reused.
> rd_support = Raindrop(api_key=KEY, project_id="support-prod", tracing_enabled=True)
> rd_billing = Raindrop(api_key=KEY, project_id="billing-prod", tracing_enabled=True)
>
> # Same method names as the module API — the instance is the routing decision.
> interaction = rd_billing.begin(user_id="u1", event="billing-chat", input="...")
> interaction.track_tool(name="invoice_lookup", input={...}, output={...})
> interaction.finish(output="...")            # -> billing-prod
>
> rd_support.track_ai(user_id="u1", event="support-chat", input="q", output="a")
> rd_support.track_signal(event_id=eid, name="thumbs_up")  # -> support-prod
> ```
>
> Each client owns its configuration and delivery pipeline, so concurrent
> requests handled by different agents route independently — there is no shared
> mutable project state to race on. The module-level API keeps working
> unchanged; it is simply the default, process-wide client.
>
> Per-instance configuration and lifecycle: `rd.flush()` / `rd.shutdown()`
> drain that client only, within its own `Raindrop(flush_timeout=...)` (an `atexit` hook drains every client at process
> exit under one shared deadline); `Raindrop(max_text_field_chars=...)` caps
> that client's payload fields without affecting other clients (module-level
> `init(max_text_field_chars=...)` still sets the process-wide default,
> 1,000,000 characters per field); and `bypass_otel_for_tools=True` ships
> `interaction.track_tool()` spans on the client's own connection, so they
> route per instance with no shared-pipeline caveats.
>
> **Tracing across multiple projects.** OpenTelemetry auto-instrumentation is
> shared per process: the first client constructed with `tracing_enabled=True`
> initializes it, and later clients share the pipeline. `begin()` scopes spans
> 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 `as_current()`:
>
> ```python theme={null}
> with rd_billing.as_current():
>     openai_client.chat.completions.create(...)  # spans -> billing-prod
> ```
>
> > [!WARNING]
> >
> > One tracing API key per process: all auto-instrumented spans export under the
> > first tracing-enabled client's key. Clients created with a different
> > `api_key` (a different workspace) get a warning, and from then on only spans
> > scoped to a client via `begin()`/`as_current()` 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. Manual
> > events (`track_ai`, `begin`/`finish`, signals, `identify`) always ship with
> > each client's own key and are unaffected.

> [tab] Go
>
> Pass `WithProjectID` to scope every event from a client to a specific [project](https://raindrop.ai/docs/platform/projects.md). Under the hood this sets the `X-Raindrop-Project-Id` header on each request.
>
> A project slug is up to 63 lowercase letters, digits, and hyphens, and it must start and end with a letter or digit (`^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$`).
>
> Omitting `WithProjectID` (or passing `"default"`) sends to the default **Production** project, which is the existing behavior, so single-project orgs need nothing here. See [Projects](https://raindrop.ai/docs/platform/projects.md) for isolation, archival, and the full behavior table.

> [tab] Rust
>
> Pass `.project_id(...)` on the builder to scope every event from a client to a specific [project](https://raindrop.ai/docs/platform/projects.md). Under the hood this sets the `X-Raindrop-Project-Id` header on each request.
>
> A project slug is up to 63 lowercase letters, digits, and hyphens, and it must start and end with a letter or digit (`^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$`).
>
> Omitting `.project_id(...)` (or passing `"default"`) sends to the default **Production** project, which is the existing behavior, so single-project orgs need nothing here. See [Projects](https://raindrop.ai/docs/platform/projects.md) for isolation, archival, and the full behavior table.

> [tab] Java
>
> Pass `.projectId(...)` on the builder to scope every event from a client to a specific [project](https://raindrop.ai/docs/platform/projects.md). Under the hood this sets the `X-Raindrop-Project-Id` header on each request.
>
> A project slug is up to 63 lowercase letters, digits, and hyphens, and it must start and end with a letter or digit (`^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$`). An invalid slug is ignored with a warning and no header is sent, so a typo never breaks ingestion.
>
> Omitting `.projectId(...)` (or passing `"default"`) sends to the default **Production** project, which is the existing behavior, so single-project orgs need nothing here. See [Projects](https://raindrop.ai/docs/platform/projects.md) for isolation, archival, and the full behavior table.

> [tab] Swift
>
> Set `projectId` on `RaindropConfiguration` (see [Configuration](#configuration)). It's sent as `X-Raindrop-Project-Id`; omitted, empty or `"default"` uses the default (Production) project, and an invalid slug is ignored with a warning.

## 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.

```typescript TypeScript icon="https://mintcdn.com/dawn-a6c57108/72ImDkk3v1Xlc68q/images/icons/typescript.svg?fit=max&auto=format&n=72ImDkk3v1Xlc68q&q=85&s=d8ece0bb7b221c02bae30c5db9c578b3" theme={null}
const withGit = new Raindrop({
  writeKey: process.env.RAINDROP_WRITE_KEY,
  appGit: {
    commitSha: "0123456789abcdef0123456789abcdef01234567",
    branch: "main",
  },
});
```

```python Python icon="python" theme={null}
raindrop.init(
    os.getenv("RAINDROP_WRITE_KEY"),
    app_git={
        "commit_sha": "0123456789abcdef0123456789abcdef01234567",
        "branch": "main",
    },
)
```

```go Go icon="golang" theme={null}
client, err := raindrop.New(
	raindrop.WithWriteKey(os.Getenv("RAINDROP_WRITE_KEY")),
	raindrop.WithAppGit(raindrop.AppGitOptions{
		CommitSHA: "0123456789abcdef0123456789abcdef01234567",
		Branch:    "main",
	}),
)
```

```rust Rust icon="rust" theme={null}
use raindrop::AppGitConfig;

let client = Client::builder()
    .write_key(std::env::var("RAINDROP_WRITE_KEY").unwrap_or_default())
    .app_git(
        AppGitConfig::new()
            .commit_sha("0123456789abcdef0123456789abcdef01234567")
            .branch("main"),
    )
    .build()?;
```

```java Java icon="java" theme={null}
Raindrop raindrop = new Raindrop(RaindropConfig.builder()
        .writeKey(System.getenv("RAINDROP_WRITE_KEY"))
        .appGit(AppGit.builder()
                .commitSha("0123456789abcdef0123456789abcdef01234567")
                .branch("main")
                .build())
        .build());
```

```swift Swift icon="swift" theme={null}
var config = RaindropConfiguration(writeKey: ProcessInfo.processInfo.environment["RAINDROP_WRITE_KEY"])
config.appGit = AppGitConfig(
  commitSha: "0123456789abcdef0123456789abcdef01234567",
  branch: "main"
)
let raindrop = RaindropClient(configuration: config)
```

> [tab] TypeScript
>
> 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 identity**
>
> Configure 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.

> [tab] Python
>
> The SDK reports the instrumented application's revision as `raindrop.app.commit_sha`, with optional
> `raindrop.app.commit_dirty` and `raindrop.app.branch`. Local Git discovery is enabled by default for
> ordinary in-process applications, runs once in a bounded background thread, and never blocks event
> calls, `flush()`, or `shutdown()`. An operation started while discovery is pending omits the metadata.
> Automatic branch discovery is opt-in.
>
> Local Git lookup requires nonblocking pipe support and is skipped on Windows with Python
> earlier than 3.12. Explicit Git configuration and supported deployment/CI environment sources
> remain available on those runtimes.
>
> The same `app_git` option is available on `Raindrop(...)`. Use `app_git=False` to disable all
> client-level enrichment without removing canonical properties explicitly supplied on an operation.
> `app_git={"auto_detect": False}` or `RAINDROP_GIT_AUTO_DETECT=false` disables automatic build,
> deployment, Git, and CI sources but retains explicit config, canonical operation properties, and
> `RAINDROP_COMMIT_SHA`, `RAINDROP_COMMIT_DIRTY`, and `RAINDROP_BRANCH`. Set
> `RAINDROP_GIT_SOURCE_DIRECTORY` to the application-under-test source and
> `RAINDROP_GIT_DETECT_BRANCH=true` to enable branch discovery.
> `RAINDROP_COMMIT_SHA` accepts a full 40- or 64-character hexadecimal SHA,
> `RAINDROP_COMMIT_DIRTY` accepts `true` or `false`, and `RAINDROP_BRANCH` accepts an ordinary string.
> Client config wins over those explicit environment values, which win over reliable build/deploy
> metadata, local application Git, and provider-marked CI metadata.
>
> A non-empty `source_directory` 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 the selected repository is unavailable, metadata stays absent;
> ambient build, deployment, and CI identity is not substituted. Discovery remains background work,
> so an operation started while it is pending keeps the absent snapshot.
>
> Existing canonical event or owning-span properties always win, even when null, empty, or invalid;
> unrelated properties are preserved. A per-operation SHA override does not inherit automatically
> inferred branch or dirty state from a different source. Remote and coding integrations must receive
> the AUT revision or an explicit AUT `source_directory` and must not inspect an observer or edited
> workspace by default. This metadata is for reporting and filtering only; it does not select a
> simulation checkout or rewrite release and SDK fields.

> [tab] Go
>
> The SDK reports `raindrop.app.commit_sha`, plus `raindrop.app.commit_dirty` and
> `raindrop.app.branch` when known. Without a selected source directory, Go prefers main-binary VCS
> build information, never dependency metadata, then reliable deployment metadata, bounded background
> application Git discovery, and provider-marked CI metadata. The first operations may omit metadata
> while discovery is pending; event calls, `Close()`, and flushing do not wait for it. Branch discovery
> is opt-in.
>
> Use `WithAppGitDisabled()` to disable all client enrichment. Setting `AutoDetect` to a pointer to
> `false`, or setting `RAINDROP_GIT_AUTO_DETECT=false`, disables automatic sources while retaining
> explicit config, canonical operation properties, and `RAINDROP_COMMIT_SHA`,
> `RAINDROP_COMMIT_DIRTY`, and `RAINDROP_BRANCH`. `WithAppGitDisabled()` does not remove canonical
> properties explicitly supplied on an operation. `RAINDROP_GIT_DETECT_BRANCH=true` enables branch
> discovery. The explicit values are a full 40- or 64-character hexadecimal SHA, `true` or
> `false` dirty state, and an ordinary branch string, respectively. Existing canonical operation
> properties always win, including invalid values; an overridden SHA does not acquire inferred dirty
> or branch values from another source. The properties
> are for reporting and filtering only, not simulation checkout selection, release, or SDK identity.
> Client config wins over the explicit Raindrop environment values and all automatic sources.
>
> A non-empty `SourceDirectory` or `RAINDROP_GIT_SOURCE_DIRECTORY` is made absolute and captured when
> the client is initialized. It exclusively selects the automatic AUT checkout after explicit config
> and `RAINDROP_*` identity values. If the repository is unavailable, metadata stays absent; ambient
> main-binary, build, deployment, and CI identity is not substituted. Discovery remains background
> work, so pending operations omit it without blocking.

> [tab] Rust
>
> The SDK reports `raindrop.app.commit_sha`, with optional `raindrop.app.commit_dirty` and
> `raindrop.app.branch`. Automatic application Git discovery runs once in a bounded background task;
> telemetry and `close()` do not wait, so early operations may omit metadata. Branch discovery is
> opt-in. Rust uses the application source, explicit environment, or injected artifact identity and
> never treats crate dependency metadata as the application revision.
>
> Use `.disable_app_git()` to disable client enrichment without removing canonical properties
> explicitly supplied on an operation. `.auto_detect(false)` or `RAINDROP_GIT_AUTO_DETECT=false`
> disables automatic sources but retains explicit config, canonical operation properties, and `RAINDROP_COMMIT_SHA`,
> `RAINDROP_COMMIT_DIRTY`, and `RAINDROP_BRANCH`. `RAINDROP_GIT_SOURCE_DIRECTORY` selects the AUT source;
> `RAINDROP_GIT_DETECT_BRANCH=true` enables branch discovery. The explicit values are a full 40- or
> 64-character hexadecimal SHA, `true` or `false` dirty state, and an ordinary branch string,
> respectively. Existing canonical properties always win,
> including invalid values, and a SHA override does not acquire inferred branch or dirty values from
> another source. The metadata is for reporting/filtering, not simulation checkout selection or
> release/SDK identity.
> Client config wins over explicit Raindrop environment values, which win over reliable deployment,
> application Git, and provider-marked CI sources.
>
> A non-empty `.source_directory(...)` or `RAINDROP_GIT_SOURCE_DIRECTORY` is resolved and captured at
> client initialization. It exclusively selects the automatic AUT checkout after explicit config and
> `RAINDROP_*` identity values. If that repository is unavailable, metadata stays absent; ambient
> build, deployment, and CI identity is not substituted. Discovery remains background work, so pending
> operations omit it without blocking.

> [tab] Java
>
> The SDK reports `raindrop.app.commit_sha`, with optional `raindrop.app.commit_dirty` and
> `raindrop.app.branch`. Application Git discovery runs once on a bounded daemon thread; telemetry,
> flush, and shutdown do not wait, so early operations may omit metadata. Branch discovery is opt-in.
> Java uses application injection, explicit environment, or the configured application source and
> never dependency/JAR metadata as the application revision.
>
> Use `.appGit(false)` to disable client enrichment without removing canonical properties explicitly
> supplied on an operation. `.autoDetect(false)` or `RAINDROP_GIT_AUTO_DETECT=false` disables automatic
> sources but retains explicit config, canonical operation properties, and `RAINDROP_COMMIT_SHA`,
> `RAINDROP_COMMIT_DIRTY`, and `RAINDROP_BRANCH`. `RAINDROP_GIT_SOURCE_DIRECTORY` selects the AUT source;
> `RAINDROP_GIT_DETECT_BRANCH=true` enables branch discovery. The explicit values are a full 40- or
> 64-character hexadecimal SHA, `true` or `false` dirty state, and an ordinary branch string,
> respectively. Existing canonical properties always win,
> including invalid values, and an overridden SHA does not acquire inferred branch or dirty state from
> another source. The metadata is for reporting/filtering, not simulation checkout selection or
> release/SDK identity.
> Client config wins over explicit Raindrop environment values, which win over reliable deployment,
> application Git, and provider-marked CI sources.
>
> A non-empty `.sourceDirectory(...)` or `RAINDROP_GIT_SOURCE_DIRECTORY` is resolved and captured at
> client initialization. It exclusively selects the automatic AUT checkout after explicit config and
> `RAINDROP_*` identity values. If that repository is unavailable, metadata stays absent; ambient
> build, deployment, and CI identity is not substituted. Discovery remains background work, so pending
> operations omit it without blocking.

> [tab] Swift
>
> Events and spans get `raindrop.app.commit_sha`, `raindrop.app.commit_dirty` and `raindrop.app.branch`. Values you set take precedence over the `RAINDROP_COMMIT_SHA`, `RAINDROP_COMMIT_DIRTY` and `RAINDROP_BRANCH` environment variables. Properties you send yourself always win. Only full 40- or 64-character SHAs are used. Set `appGit = nil` to turn this off. In apps there's no process environment: set `appGit` in code or from your build configuration. The SDK never inspects a Git checkout itself. The SDK doesn't read Git or CI metadata automatically; pass build identity explicitly.

## 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 TypeScript icon="https://mintcdn.com/dawn-a6c57108/72ImDkk3v1Xlc68q/images/icons/typescript.svg?fit=max&auto=format&n=72ImDkk3v1Xlc68q&q=85&s=d8ece0bb7b221c02bae30c5db9c578b3" theme={null}
const result = await raindrop.forceFlush();
if (!result.delivered) {
  console.log(`${result.queued} payload(s) still queued`);
}

await raindrop.close(); // before the process exits
```

```python Python icon="python" theme={null}
result = raindrop.flush()
if not result:
    print(f"{result.pending} payload(s) still pending")

raindrop.shutdown()  # also runs automatically at exit
```

```go Go icon="golang" theme={null}
if err := client.Flush(ctx); errors.Is(err, raindrop.ErrUndelivered) {
	log.Printf("some telemetry was not delivered: %v", err)
}
// client.Close() (deferred above) drains the rest before exit.
```

```rust Rust icon="rust" theme={null}
if let Err(err) = client.flush().await {
    eprintln!("raindrop: {err}"); // Error::Undelivered: still queued
}

client.close().await?; // before the process exits
```

```java Java icon="java" theme={null}
FlushResult result = raindrop.flushWithResult();
if (!result.delivered()) {
    System.out.println("Raindrop: " + result);
}

raindrop.close(); // before the process exits
```

```swift Swift icon="swift" theme={null}
let result = await raindrop.flush()
if !result.isComplete {
  print("Raindrop: \(result)")
}

await raindrop.close() // before the process exits
```

> [tab] TypeScript
>
> 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.
>
> > [!NOTE]
> >
> > 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.

> [tab] Python
>
> The Raindrop SDK uses a buffering mechanism to efficiently send events in batches. The events are automatically flushed when the buffer reaches a certain size or after a specified timeout.
>
> You can manually flush the events by calling the `flush` function, for example at the end of a serverless handler. Make sure this happens before the process exits or you will lose events.
>
> `flush()` returns once everything queued before it, including sends already in progress, was handed to Raindrop, or when `flush_timeout` (default 10 seconds) passes. If the deadline passes first, payloads that weren't sent yet, or were waiting on a retry, stay queued for the background sender. A request cut off mid-flight is dropped rather than sent twice.
>
> `flush()` returns a `FlushResult` with `delivered`, `pending`, `dropped` and `timed_out`. It is truthy when everything was delivered, and a warning is logged when it wasn't. `flush()` never raises, and ignoring the result is safe.
>
> To ensure all events are processed before your application exits, call `raindrop.shutdown()`. This will also flush any pending partial events from interactions. `shutdown()` uses the same `flush_timeout` deadline and returns a `FlushResult` too. Whatever is still unsent at the deadline is dropped with a warning. The SDK also calls `shutdown()` automatically when the process exits normally.

> [tab] Go
>
> Call `client.Close()` before your process exits to send any buffered events and spans. Call `client.Flush(ctx)` when you need everything sent so far delivered before continuing, for example at the end of a serverless handler.
>
> * `Flush()` returns once everything queued before it, including sends already in progress, was handed to Raindrop, or when the deadline passes: the context's deadline if it has one, otherwise `WithFlushTimeout` (default 10s).
> * 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.
> * `Close()` works the same way under the same deadline, then stops the background goroutines. Whatever is still unsent at the deadline is dropped.
> * When either call returns with something undelivered, it logs a warning and returns an error wrapping `raindrop.ErrUndelivered`. The error is safe to ignore; it never means your own work failed.

> [tab] Rust
>
> Call `client.flush().await` when you need everything sent so far delivered before continuing, for example at the end of a serverless handler, and `client.close().await` before your process exits.
>
> * `flush()` returns once everything queued before it, including sends already in progress, was handed to Raindrop, or when `flush_timeout` (default 10s) passes. Each request's timeout is shortened to fit what's left of the deadline.
>
> * 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.
>
> * In that case `flush()` logs a warning and returns `Err(Error::Undelivered { count, dropped })`. The error is safe to ignore; it never means your own work failed, so log it rather than propagating it with `?`.
>
> * `close()` works the same way under the same deadline, then stops the background tasks. Whatever is still unsent at the deadline is dropped with a warning, and `close()` returns `Ok(())`.
>
> * `Interaction::finish()` returns `Ok(())` even when its final update couldn't be delivered; only `flush()` reports undelivered data.
>
> > [!NOTE]
> >
> > `raindrop::Error` is `#[non_exhaustive]`, so a `match` on it needs a wildcard arm.

> [tab] Java
>
> Raindrop is designed never to slow down or crash your application. The SDK is a strict no-op when
> Raindrop is unreachable, slow, rate-limited, or misconfigured:
>
> * **All network I/O runs on a background daemon thread.** `trackAi`, `track`, `trackSignal`,
>   `identify`, `begin` / `finish`, and span start/end only enqueue work and return immediately on
>   the caller's thread. Only `flush()` and `close()` wait, and never longer than `flushTimeoutMs`.
> * **Oversized payloads cost the cap, not the payload.** Text fields and structured span/property
>   values are bounded *before* serialization, so a multi-MB input is `O(cap)` on the caller.
> * **Bounded waits everywhere.** Connect/read timeouts, a capped retry count with clamped backoff,
>   and one deadline for `flush()` and `close()` — they return promptly even against a black-hole
>   endpoint.
> * **Exceptions never escape.** Serialization or transport failures are swallowed and
>   rate-limited-logged; your code path is unaffected.
>
> `flush()` blocks until everything queued before the call, retries included, was handed to Raindrop,
> or until `flushTimeoutMs` (default 10 seconds) passes. Use it when you need telemetry delivered
> before continuing, for example at the end of a serverless handler.
>
> * If the deadline passes first, payloads that weren't sent yet, or were waiting on a retry, stay
>   queued and keep sending in the background. A request cut off mid-flight is dropped rather than
>   sent twice. A warning is logged.
> * `flushWithResult()` does the same and returns a `FlushResult` with `delivered()`, `timedOut()`
>   and `undeliveredSends()`. It never throws, and ignoring the result is safe.
> * `close()` waits under the same deadline, then stops the client. Whatever is still unsent at the
>   deadline is dropped with a warning.

> [tab] Swift
>
> * `trackAi`, `trackEvent`, `begin`, interaction updates, `identify` and `trackSignal` only add to an in-memory queue and return immediately. They never wait on the network and never throw.
> * Queued data is sent in the background about once a second.
> * `finish()`, `flush()` and `close()` send what's queued and return once it's delivered, or after 10 seconds (`flushDeadline`). A request still in flight at the deadline is dropped rather than resent, since it may already have reached Raindrop.
> * They return a `FlushResult` (`delivered`, `dropped`, `pending`, `timedOut`, `undelivered`, and `isComplete`). It's `@discardableResult` and safe to ignore; a warning is logged when anything wasn't delivered. `dropped` counts payloads Raindrop rejected permanently or `beforeSend` dropped; `undelivered` counts payloads given up on.
> * After `flush()`, anything not yet sent, or waiting on a retry, stays queued and is sent later.
> * After `close()`, leftovers are saved to offline storage for the next launch when `storageDirectory` is set (the default in apps), and dropped otherwise. Either way a warning says which.
> * Call `await raindrop.close()` before your process exits (for example at the end of a serverless handler).
>
> **Apps (iOS, tvOS, watchOS, visionOS)**
>
> * When the app goes to the background, the SDK sends what's queued within the time the system allows, then saves anything left to disk. Saved payloads are sent on the next launch, unchanged.
> * Offline storage is bounded (5 MB, 7 days) and excluded from backups. By default it's readable once the device has been unlocked after a restart, so background flushing works while it's locked again. With `storageProtection = .complete` it's unreadable whenever the device is locked: unsent data stays in memory until the next unlock, and saved data from an earlier run is sent once the device is unlocked. Data queued while locked is lost if the app is killed before then.
> * The package includes a privacy manifest (`PrivacyInfo.xcprivacy`). It declares no tracking, and the user ID, user content and product interaction data the SDK sends. Reflect these in your app's privacy details.
> * In apps there's no process environment: set `appGit` and the write key in code or from your build configuration.
> * A write key shipped inside an app can be extracted from it. Treat it accordingly.

## 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.

> [tab] TypeScript
>
> 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](#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).

> [tab] Python
>
> Tracking calls never raise because of delivery problems; failures are logged (rate-limited), regardless of whether debug logs are on.
>
> * A request is tried up to 3 times. 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 warning and never retried. It still counts as delivered in `FlushResult`.
> * When Raindrop accepts a request but reports items it rejected or changed, they are logged as warnings.
> * Other warnings cover an interaction 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).

> [tab] Go
>
> * 408, 429 and 5xx responses are retried, waiting at least the `Retry-After` the server sends (capped at 30s). 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 warning and never retried.
> * There is no client-side limit on event size; Raindrop decides what it accepts. Individual text fields are capped by `WithMaxTextFieldChars` (default 1,000,000).
> * When Raindrop accepts a request but reports items it rejected or changed, they are logged as warnings.
> * Warnings are logged at `Warn` through the configured logger, rate-limited, whether or not `WithDebug` 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), and an event finished twice in the same process (still sent).

> [tab] Rust
>
> * 408, 429 and 5xx responses are retried, waiting at least the `Retry-After` the server sends (capped at 30s). 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 warning and never retried. It isn't reported as a `flush()` error.
> * There is no client-side limit on event size; Raindrop decides what it accepts. Individual text fields are capped by `max_text_field_chars`.
> * When Raindrop accepts a request but reports items it rejected or changed, they are logged as warnings.
> * Warnings go through `tracing::warn!`, rate-limited, whether or not `debug` 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).

> [tab] Java
>
> * A request is tried up to 3 times. 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 warning and never retried.
>   It isn't counted as undelivered in `FlushResult`.
> * 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 go to stderr, rate-limited, whether or not `debugLogs` is on.
>
> The SDK is manual-only — it does not monkey-patch or auto-instrument your LLM client libraries — so
> it cannot interfere with your application's runtime.

> [tab] Swift
>
> Retries: timeouts (408), rate limits (429) and server errors are retried with backoff, waiting at least the `Retry-After` the server sends (capped at `maxRetryAfter`). Other 4xx responses are not retried; the payload is dropped with a warning. There is no client-side limit on payload size; Raindrop decides what it accepts. When Raindrop accepts a request but reports items it rejected or changed, they are logged as warnings.

## More

> [card] [Tracing](https://raindrop.ai/docs/sdk/tracing.md)
>
> Spans, tool calls, model calls and sub-agents, in every SDK.

> [card] [Tool catalog capture](https://raindrop.ai/docs/sdk/tool-catalog.md)
>
> How the tools a model was given are recorded.

> [card] [Browser SDK](https://raindrop.ai/docs/sdk/browser.md)
>
> Track AI events from web apps.

> [card] [HTTP API](https://raindrop.ai/docs/sdk/http-api.md)
>
> Send events without an SDK.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.