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

> The Raindrop SDK allows you to track user events and AI interactions in your app. This documentation provides a brief overview of how to use the TypeScript SDK.

# TypeScript

## Installation

Install with your package manager of choice:

<CodeGroup>
  ```bash npm theme={null}
  npm install raindrop-ai
  ```

  ```bash yarn theme={null}
  yarn add raindrop-ai
  ```

  ```bash pnpm theme={null}
  pnpm add raindrop-ai
  ```

  ```bash bun theme={null}
  bun add raindrop-ai
  ```
</CodeGroup>

```typescript theme={null}
import { Raindrop } from "raindrop-ai";

// Replace with the key from your Raindrop dashboard
const raindrop = new Raindrop({ writeKey: RAINDROP_API_KEY });
```

***

## Quick Start: Interaction API

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. **`finish()`** – Record the AI's final output and close the interaction

<Info>
  **Using Vercel AI SDK?** Check out our [automatic integration](/docs/integrations/vercel-ai-sdk) to track AI events and traces with zero configuration.
</Info>

### Example: Chat Completion

```typescript theme={null}
import { openai } from '@ai-sdk/openai';
import { generateText } from 'ai';
import { randomUUID } from "crypto";
import { Raindrop } from "raindrop-ai";

const raindrop = new Raindrop({ writeKey: RAINDROP_API_KEY });

const message = "What is love?";
const eventId = randomUUID(); // Generate your own ID for log correlation

// 1. Start the interaction
const interaction = raindrop.begin({
  eventId,
  event: "chat_message",
  userId: "user_123",
  input: message,
  model: "gpt-4o",
  convoId: "convo_123",
  properties: {
    tool_call: "reasoning_engine",
    system_prompt: "you are a helpful...",
    experiment: "experiment_a",
  },
});

// 2. Make the LLM call
const { text } = await generateText({
  model: openai("gpt-4o"),
  prompt: message,
});

// 3. Finish the interaction
interaction.finish({
  output: text,
});
```

### Updating an Interaction

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",
  },
]);
```

### Resuming an Interaction

If you no longer have the interaction object returned from `begin()`, resume it with `resumeInteraction()`:

```typescript theme={null}
const interaction = raindrop.resumeInteraction(eventId);
```

<Warning>
  Interactions are subject to a 1 MB event limit. Oversized payloads will be truncated. [Contact us](mailto:founders@raindrop.ai) if you have custom requirements.
</Warning>

***

## Single-Shot Tracking (`trackAi`)

For simple request-response interactions, you can use `trackAi()` directly:

```typescript theme={null}
raindrop.trackAi({
  event: "user_message",
  userId: "user123",
  model: "gpt-4o-mini",
  input: "Who won the 2023 AFL Grand Final?",
  output: "Collingwood by four points!",
  properties: {
    tool_call: "reasoning_engine",
    system_prompt: "you are a helpful...",
    experiment: "experiment_a",
  },
});
```

> We recommend using `begin()` → `finish()` for new code to take advantage of partial-event buffering, tracing, and upcoming features like automatic token counts.

***

## Tracking Signals (Feedback)

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"`) |

```typescript theme={null}
await raindrop.trackSignal({
  eventId: "my_event_id",
  name: "thumbs_down",
  comment: "Answer was off-topic",
});
```

***

## Self Diagnostics

Self Diagnostics lets your agent **proactively report its own issues** — capability gaps, missing context, persistent tool failures — back to your team. Signals appear in Raindrop's [Self Diagnostics](/docs/platform/self-diagnostics) dashboard.

The easiest way to enable Self Diagnostics is by using `wrap()` with the Vercel AI SDK:

```typescript theme={null}
import * as ai from "ai";

const { generateText, streamText } = raindrop.wrap(ai, {
  context: { userId: "user_123", eventName: "support-agent" },
  selfDiagnostics: {
    enabled: true,
  },
});
```

See the [Vercel AI SDK docs](/docs/integrations/vercel-ai-sdk#self-diagnostics-optional) for the full `wrap()` reference. For integrations that don't use the Vercel AI SDK, or can't use `wrap()`, the sections below cover standalone alternatives.

### Self Diagnostics Tool

`createSelfDiagnosticsTool()` creates a tool your agent can call to report issues. It gives you:

* A reusable `execute()` handler that tracks an `agent` signal
* Adapter-specific tool definitions for Vercel AI SDK, OpenAI SDK, and Anthropic SDK

The four default categories are `missing_context`, `repeatedly_broken_tool`, `capability_gap`, and `complete_task_failure`. You can replace these with your own.

<Warning>
  Self diagnostics requires an `eventId` to correlate the signal with an interaction. Pass `eventId`, `interaction`, or `getEventId` when creating the tool.
</Warning>

```typescript theme={null}
const interaction = raindrop.begin({
  eventId: "evt_123",
  event: "agent_run",
  userId: "user_123",
  input: "Fix my deployment",
});

const diagnostics = raindrop.createSelfDiagnosticsTool({
  interaction, // pulls eventId from interaction.getEventId()
  toolName: "__raindrop_report", // optional
  guidance: "Optional extra guidance for your domain", // optional
  // Default signals (used when `signals` is omitted):
  signals: {
    missing_context: {
      description:
        "You cannot complete the task because critical information, credentials, or access is missing and the user cannot provide it. " +
        "Do NOT report this for normal clarifying questions — only when you are blocked.",
      sentiment: "NEGATIVE",
    },
    repeatedly_broken_tool: {
      description:
        "A tool has failed on multiple distinct attempts in this conversation, preventing task completion. You are sure the tool exists, and you have tried to use it, but it has failed. " +
        "A single tool error is NOT enough — the tool must be persistently broken across retries.",
      sentiment: "NEGATIVE",
    },
    capability_gap: {
      description:
        "The task requires a tool, permission, or capability that you do not have. " +
        "For example, the user asks you to perform an action but no suitable tool exists, or you lack the necessary access. " +
        "Do NOT report this if you simply need more information from the user — only when the gap is in your own capabilities.",
      sentiment: "NEGATIVE",
    },
    complete_task_failure: {
      description:
        "You were unable to accomplish what the user asked despite making genuine attempts. This might be things like, you genuinely do not have the capabilities the user is asking for. You have tried but run into a persistent bug in the environment etc. " +
        "This is NOT a refusal or policy block — you tried and failed to deliver the result.",
      sentiment: "NEGATIVE",
    },
  },
});

// Later, when tool is executed:
await diagnostics.execute({
  category: "missing_context",
  detail: "Missing deployment credentials and logs access.",
});
```

You don't need to mention self diagnostics in your system prompt — the tool description generated by the SDK already tells the model when and how to call it. Just wire the tool into your LLM call using one of the adapters below.

When the agent calls the tool, a signal is tracked on the same `eventId`:

```json theme={null}
{
  "event_id": "evt_...",
  "signal_name": "self diagnostics - missing_context",
  "signal_type": "agent",
  "properties": {
    "source": "agent_reporting_tool",
    "category": "missing_context",
    "signal_description": "You cannot complete the task because critical information...",
    "detail": "User asked to fix deployment but no access to logs or SSH credentials."
  }
}
```

#### Supported adapters

`createSelfDiagnosticsTool()` supports these adapters:

| Adapter                      | Use with                        | Returns                                             |
| ---------------------------- | ------------------------------- | --------------------------------------------------- |
| `diagnostics.forVercelAI()`  | `ai` package / Vercel AI SDK    | `{ description, parameters, inputSchema, execute }` |
| `diagnostics.forOpenAI()`    | `openai` package function tools | OpenAI `tools` entry (`type: "function"`)           |
| `diagnostics.forAnthropic()` | `@anthropic-ai/sdk` tool config | Anthropic `tools` entry (`input_schema`)            |

Vercel AI SDK:

```typescript theme={null}
const tools = {
  [diagnostics.name]: diagnostics.forVercelAI(),
};
```

OpenAI SDK:

```typescript theme={null}
const response = await openai.chat.completions.create({
  model: "gpt-4o-mini",
  messages,
  tools: [diagnostics.forOpenAI()],
  tool_choice: { type: "function", function: { name: diagnostics.name } },
});

const toolCall = response.choices[0]?.message.tool_calls?.[0];
if (toolCall) {
  await diagnostics.execute(JSON.parse(toolCall.function.arguments || "{}"));
}
```

Anthropic SDK:

```typescript theme={null}
const response = await anthropic.messages.create({
  model: "claude-sonnet-4-6",
  max_tokens: 512,
  messages,
  tools: [diagnostics.forAnthropic()],
  tool_choice: { type: "tool", name: diagnostics.name },
});

const block = response.content.find((b) => b.type === "tool_use");
if (block && block.type === "tool_use") {
  await diagnostics.execute(block.input);
}
```

#### UI guidance

`__raindrop_report` is an internal tool. If your chat UI renders tool calls, hide this tool call from end users to avoid confusing them with internal diagnostics details.

```typescript theme={null}
const visibleToolCalls = toolCalls.filter((call) => call.toolName !== diagnostics.name);
```

### Manual Reporting

If your agent already has its own way of detecting issues, use `selfDiagnose()` to report them directly — no LLM tool call needed.

```typescript theme={null}
raindrop.selfDiagnose({
  eventId: "evt_123",
  description: "User asked for production DB access but no credentials were provided.",
  category: "missing_context",   // optional — defaults to "general"
});
```

| Parameter     | Type                         | Required | Description                                                       |
| ------------- | ---------------------------- | -------- | ----------------------------------------------------------------- |
| `eventId`     | `string`                     | Yes      | Interaction/event ID (from `begin()` or `trackAi()`)              |
| `description` | `string`                     | Yes      | Human-readable description of the issue                           |
| `category`    | `string`                     | No       | Category name (e.g. `"missing_context"`). Defaults to `"general"` |
| `source`      | `string`                     | No       | Origin identifier. Defaults to `"selfDiagnose"`                   |
| `sentiment`   | `"POSITIVE"` \| `"NEGATIVE"` | No       | Defaults to `"NEGATIVE"`                                          |
| `properties`  | `Record<string, unknown>`    | No       | Additional custom properties                                      |

***

## Attachments

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) |

```typescript theme={null}
interaction.addAttachments([
  {
    type: "code",
    role: "input",
    language: "typescript",
    name: "example.ts",
    value: "console.log('hello');",
  },
  {
    type: "text",
    name: "Additional Info",
    value: "Some extra text",
    role: "input",
  },
  { type: "image", value: "https://example.com/image.png", role: "output" },
  { type: "iframe", value: "https://example.com/embed", role: "output" },
]);
```

***

## Feature Flags

Feature flags record which flags and experiment variants were active when an event happened, so you can compare AI quality across variants in Raindrop. Like `properties`, they attach to individual events — pass them wherever the event is created (`begin()` or `trackAi()`), or set them on a live interaction as your pipeline makes decisions.

Two input forms are accepted:

| Form                                                                         | Example                                                            | Sent as `feature_flags`                                              |
| ---------------------------------------------------------------------------- | ------------------------------------------------------------------ | -------------------------------------------------------------------- |
| Array of flag names (each implicitly `"true"`)                               | `["prompt-v2", "reasoning"]`                                       | `{"prompt-v2": "true", "reasoning": "true"}`                         |
| Object with explicit values (`string`, `number`, or `boolean` — stringified) | `{ "prompt-version": "v2", "max-retries": 3, "reasoning": false }` | `{"prompt-version": "v2", "max-retries": "3", "reasoning": "false"}` |

### With `begin()`

Pass the flags that shaped the request — typically the values you just read from your flag provider (LaunchDarkly, Statsig, PostHog, etc.):

```typescript theme={null}
// Flag values you resolved for this user
const promptVersion = await flagProvider.getVariant("prompt-version", userId); // "v1" | "v2"
const reasoningEnabled = await flagProvider.isEnabled("reasoning", userId);    // boolean

const interaction = raindrop.begin({
  event: "chat_message",
  userId,
  input: message,
  model: "gpt-4o",
  // Array form also works: featureFlags: ["prompt-v2", "reasoning"]
  featureFlags: {
    "prompt-version": promptVersion,
    reasoning: reasoningEnabled,
  },
});

const { text } = await generateText({
  model: openai("gpt-4o"),
  system: promptVersion === "v2" ? SYSTEM_PROMPT_V2 : SYSTEM_PROMPT_V1,
  prompt: message,
});

interaction.finish({ output: text });
```

### Updating a live interaction

Use `setFeatureFlag` / `setFeatureFlags` when a flag decision happens mid-interaction — for example, a retrieval strategy chosen after `begin()`:

```typescript theme={null}
interaction.setFeatureFlag("retrieval-strategy", "hybrid"); // explicit value
interaction.setFeatureFlag("semantic-cache");               // value defaults to "true"
interaction.setFeatureFlags(["rerank", "query-expansion"]); // array form — both "true"
```

| Method            | Parameters                                                         | Behavior                                    |
| ----------------- | ------------------------------------------------------------------ | ------------------------------------------- |
| `setFeatureFlag`  | `(name: string, value?: string \| number \| boolean)`              | Sets one flag; `value` defaults to `"true"` |
| `setFeatureFlags` | `(flags: string[] \| Record<string, string \| number \| boolean>)` | Normalizes and merges into existing flags   |

Flags merge across calls; setting the same flag again overwrites its value.

### With `trackAi()`

```typescript theme={null}
raindrop.trackAi({
  event: "summarize_ticket",
  userId,
  model: "gpt-4o-mini",
  input: ticketText,
  output: summary,
  featureFlags: ["summary-prompt-v2"], // sent as {"summary-prompt-v2": "true"}
});
```

***

## Identifying Users

```typescript theme={null}
raindrop.setUserDetails({
  userId: "user123",
  traits: {
    name: "Jane",
    email: "jane@example.com",
    plan: "paid", // we recommend 'free', 'paid', 'trial'
    os: "macOS",
  },
});
```

***

## PII Redaction

Read more about how Raindrop handles privacy and PII redaction [here](/docs/security/pii-redaction). Enable client-side PII redaction when initializing the SDK:

```typescript theme={null}
new Raindrop({
  writeKey: RAINDROP_API_KEY,
  redactPii: true,
});
```

***

## Error Handling

Exceptions are raised when errors occur while sending events to Raindrop. Handle these appropriately in your application.

***

## Configuration

```typescript theme={null}
new Raindrop({
  writeKey: RAINDROP_API_KEY,
  debugLogs: process.env.NODE_ENV !== "production",  // Print queued events
  disabled: process.env.NODE_ENV === "test",         // Disable all tracking
});
```

| Option               | Type      | Default       | Description                                                                                                |
| -------------------- | --------- | ------------- | ---------------------------------------------------------------------------------------------------------- |
| `writeKey`           | `string`  | —             | Your Raindrop API key (required)                                                                           |
| `projectId`          | `string`  | `"default"`   | Send events to a specific [project](/docs/platform/projects); 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                                                                           |
| `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                                                   |

Call `await raindrop.close()` before your process exits to flush any buffered events.

***

## Projects

Pass `projectId` to scope every event from a client to a specific [project](/docs/platform/projects). Under the hood this sets the `X-Raindrop-Project-Id` header on each request.

```typescript theme={null}
const raindrop = new Raindrop({
  writeKey: RAINDROP_API_KEY,
  projectId: "support-prod",
});
```

Omitting `projectId` (or passing `"default"`) sends to the default **Production** project, which is the existing behavior. See [Projects](/docs/platform/projects) 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({ userId: "u1", event: "billing-chat", input: "..." });
interaction.trackTool({ name: "invoice_lookup", input: {}, output: {} });
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.
</Warning>

<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.
</Note>

***

## Tracing

Tracing captures detailed execution information from your AI pipelines—multi-model interactions, chained prompts, and tool calls. This helps you:

* Visualize the full execution flow of your AI application
* Debug and optimize prompt chains
* Understand the intermediate steps that led to a response

### Getting Started

Wrap your code with `withSpan` or `withTool` on an interaction, and LLM calls inside are automatically captured:

```typescript theme={null}
import { Raindrop } from "raindrop-ai";

const raindrop = new Raindrop({ writeKey: RAINDROP_API_KEY });

const interaction = raindrop.begin({ ... });
await interaction.withSpan({ name: "my_task" }, async () => {
  // LLM calls here are automatically traced
});
```

**Next.js users:** Add `raindrop-ai` to [`serverExternalPackages`](https://nextjs.org/docs/app/api-reference/config/next-config-js/serverExternalPackages) in your config:

```typescript theme={null}
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  serverExternalPackages: ['raindrop-ai'],
};

module.exports = nextConfig;
```

### Using `withSpan`

Use `withSpan` to trace tasks or operations. Any LLM calls within the span are automatically captured:

```typescript theme={null}
// Basic span
const result = await interaction.withSpan(
  { name: "generate_response" },
  async () => {
    return "Generated response";
  }
);

// Span with metadata
const result = await interaction.withSpan(
  {
    name: "embedding_generation",
    properties: { model: "text-embedding-3-large" },
    inputParameters: ["What is the weather today?"],
  },
  async () => {
    return [0.1, 0.2, 0.3, 0.4];
  }
);
```

| Parameter         | Type                     | Description                       |
| ----------------- | ------------------------ | --------------------------------- |
| `name`            | `string`                 | Name for identification in traces |
| `properties`      | `Record<string, string>` | Additional metadata               |
| `inputParameters` | `unknown[]`              | Input parameters for the task     |

### Using `withTool`

Use `withTool` to trace agent actions—memory operations, web searches, API calls, and more:

```typescript theme={null}
// Basic tool call
const result = await interaction.withTool(
  { name: "search_tool" },
  async () => {
    return "Search results";
  }
);

// Tool with metadata
const result = await interaction.withTool(
  {
    name: "calculator",
    properties: { operation: "multiply" },
    inputParameters: { a: 5, b: 10 },
  },
  async () => {
    return "Result: 50";
  }
);
```

| Parameter         | Type                     | Description                          |
| ----------------- | ------------------------ | ------------------------------------ |
| `name`            | `string`                 | Name for identification in traces    |
| `version`         | `number`                 | Version number of the tool           |
| `properties`      | `Record<string, string>` | Additional metadata                  |
| `inputParameters` | `Record<string, any>`    | Input parameters for the tool        |
| `traceContent`    | `boolean`                | Whether to trace content             |
| `suppressTracing` | `boolean`                | Suppress tracing for this invocation |

### Manual Tool Tracking

For more control over tool span tracking, use `trackTool` or `startToolSpan`.

#### `trackTool` – Retroactive Logging

Use `trackTool` to log a tool call after it has completed:

```typescript theme={null}
const interaction = raindrop.begin({
  eventId: "my-event",
  event: "agent_run",
  userId: "user_123",
  input: "Search for weather data",
});

// Log a completed tool call
interaction.trackTool({
  name: "web_search",
  input: { query: "weather in NYC" },
  output: { results: ["Sunny, 72°F", "Clear skies"] },
  durationMs: 150,
  properties: { engine: "google" },
});

// Log a failed tool call
interaction.trackTool({
  name: "database_query",
  input: { query: "SELECT * FROM users" },
  durationMs: 50,
  error: new Error("Connection timeout"),
});

interaction.finish({ output: "Weather search complete" });
```

| 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 `now - durationMs`) |
| `error`        | `Error \| string`        | Error if the tool failed                               |
| `properties`   | `Record<string, string>` | Additional metadata                                    |
| `traceId`      | `string`                 | W3C trace ID override (requires `parentSpanId`)        |
| `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);
} finally {
  toolSpan.end();
}

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) |

### Module Instrumentation

In some environments, automatic instrumentation may not work due to module loading order or bundler behavior. Use `instrumentModules` to explicitly specify which modules to instrument:

<Warning>
  **Anthropic users:** You must use a module namespace import (`import * as ...`), not the default export.
</Warning>

```typescript theme={null}
import OpenAI from "openai";
import Anthropic from "@anthropic-ai/sdk";
import * as AnthropicModule from "@anthropic-ai/sdk";  // Required for instrumentation
import { Raindrop } from "raindrop-ai";

const raindrop = new Raindrop({
  writeKey: RAINDROP_API_KEY,
  instrumentModules: {
    openAI: OpenAI,
    anthropic: AnthropicModule,  // Pass the module namespace, not the default export
  },
});
```

Supported modules: `openAI`, `anthropic`, `cohere`, `bedrock`, `google_vertexai`, `google_aiplatform`, `pinecone`, `together`, `langchain`, `llamaIndex`, `chromadb`, `qdrant`, `mcp`.

For Bedrock, use a namespace import — same pattern as Anthropic above. Cross-region inference profile model IDs (e.g. `us.anthropic.claude-*`) are supported.

```typescript theme={null}
import * as BedrockModule from "@aws-sdk/client-bedrock-runtime";

const raindrop = new Raindrop({
  writeKey: RAINDROP_API_KEY,
  instrumentModules: { bedrock: BedrockModule },
});
```

### OpenTelemetry Integration

If you already have an OpenTelemetry setup (Sentry, Datadog, Honeycomb, etc.), integrate Raindrop alongside it using `useExternalOtel`:

```typescript theme={null}
import { NodeSDK } from "@opentelemetry/sdk-node";
import * as AnthropicModule from "@anthropic-ai/sdk";
import Anthropic from "@anthropic-ai/sdk";
import { Raindrop } from "raindrop-ai";

// 1. Create Raindrop with useExternalOtel
const raindrop = new Raindrop({
  writeKey: RAINDROP_API_KEY,
  useExternalOtel: true,
  instrumentModules: { anthropic: AnthropicModule },
});

// 2. Add Raindrop's processor and instrumentations to your NodeSDK
const sdk = new NodeSDK({
  spanProcessors: [
    raindrop.createSpanProcessor(),  // Sends traces to Raindrop
    datadogProcessor,                  // Your existing processor
  ],
  instrumentations: raindrop.getInstrumentations(),
});
sdk.start();

// If using Sentry, skip NodeSDK and pass the processor directly:
// Sentry.init({
//   dsn: process.env.SENTRY_DSN,
//   openTelemetrySpanProcessors: [raindrop.createSpanProcessor()],
// });

// 3. Create AI clients AFTER SDK starts
const anthropic = new Anthropic({ apiKey: "..." });

// 4. Use Raindrop normally
const interaction = raindrop.begin({
  eventId: "my-event",
  event: "chat_request",
  userId: "user_123",
  input: "Hello!",
});

await interaction.withSpan({ name: "generate_response" }, async () => {
  const response = await anthropic.messages.create({
    model: "claude-3-haiku-20240307",
    max_tokens: 100,
    messages: [{ role: "user", content: "Hello!" }],
  });
  return response;
});

interaction.finish({ output: "Response from Claude" });
```

| Method                  | Description                                             |
| ----------------------- | ------------------------------------------------------- |
| `createSpanProcessor()` | Returns a span processor that sends traces to Raindrop  |
| `getInstrumentations()` | Returns OpenTelemetry instrumentations for AI libraries |

<Info>
  Without `instrumentModules`, `getInstrumentations()` returns instrumentations for all supported AI libraries. Specify `instrumentModules` to instrument only specific libraries.
</Info>

### 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 W3C hexadecimal IDs directly. Both values must be supplied together:

```typescript theme={null}
raindrop.resumeInteraction(eventId).trackTool({
  name,
  input,
  output,
  durationMs,
  traceId,       // 32 hexadecimal characters
  parentSpanId,  // 16 hexadecimal characters
});
```

Invalid or incomplete overrides are ignored, and the SDK falls back to the active OpenTelemetry context.

***

That's it! You're ready to explore your events in the Raindrop dashboard. Ping us on Slack or [email us](mailto:founders@raindrop.ai) if you get stuck!
