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

# Java (Beta)

> The Raindrop Java SDK lets you track user events and AI interactions from JVM applications. This page covers installation, event tracking, and manual tracing.

<Info>
  **Beta.** The Java SDK is at `0.0.3`. 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.
</Info>

## Installation

The SDK requires **Java 17+**. The artifact coordinates are `ai.raindrop:raindrop-java`.

Gradle:

```gradle theme={null}
dependencies {
    implementation 'ai.raindrop:raindrop-java:0.0.3'
}
```

Maven:

```xml theme={null}
<dependency>
    <groupId>ai.raindrop</groupId>
    <artifactId>raindrop-java</artifactId>
    <version>0.0.3</version>
</dependency>
```

Smallest possible program:

```java theme={null}
import ai.raindrop.*;

public class Main {
    public static void main(String[] args) {
        // Empty / missing key → SDK becomes a no-op (zero HTTP), so you can integrate
        // the SDK code first and add the key later without affecting your app.
        Raindrop raindrop = new Raindrop(RaindropConfig.builder()
                .writeKey(System.getenv("RAINDROP_WRITE_KEY"))
                .build());

        // ... use raindrop ...

        raindrop.close();
    }
}
```

`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 ...
}
```

***

## Single-Shot Tracking (`trackAi`)

For simple request-response interactions, call `trackAi()` directly. 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")
        .model("gpt-4o")
        .input("Who won the 2023 AFL Grand Final?")
        .output("Collingwood by four points!")
        .convoId("conv-123")
        .properties(Map.of(
                "ai.usage.prompt_tokens", 10,
                "ai.usage.completion_tokens", 5))
        .build());
```

<Info>
  For new code we recommend the `begin()` → `finish()` interaction API below: it buffers a pending
  event immediately and links any spans you create into the same trace.
</Info>

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());
```

***

## Interactions

`begin()` opens an interaction (a trace) and immediately ships a pending event so it appears in the
Events tab right away. Update it as work progresses, then `finish()` records the final output:

```java theme={null}
Interaction it = raindrop.begin(BeginOptions.builder()
        .event("chat_message")
        .userId("user-123")
        .convoId("conv-123")
        .input("Can you suggest a calm Saturday morning in San Francisco?")
        .build());

String output = callLlm();

it.setModel("gpt-4o")
  .setProperty("final_status", "completed")
  .setOutput(output)
  .finish();
```

`setInput`, `setOutput`, `setModel`, `setProperty`, and `setError` return the interaction, so they
chain. Call `finish()` exactly once; it finalizes the event and closes the root span.

***

## Tracing

Tracing captures detailed execution information — multi-step pipelines, tool calls, and subagents —
so you can visualize the full execution flow, debug prompt chains, and understand the intermediate
steps behind a response.

### Tool and task spans

Spans created from an interaction inherit its `userId`, `convoId`, and `event`, so the dashboard
groups them under the same user, conversation, and event. Use `startTool` for tool calls and
`startTask` for other units of work:

```java theme={null}
Span search = it.startTool("web_search");
search.setInput("flights to tokyo");
try {
    search.setOutput(runSearch());
} catch (Exception e) {
    search.setError(e);
} finally {
    search.end();
}
```

`setInput`, `setOutput`, `setError` (which accepts a `Throwable` or a `String`), and `setAttribute`
all return the span for chaining. Always `end()` a span — a `try/finally` guarantees it even when
the work throws.

### Nested spans & subagents

Spans are nestable: a span started from another span is parented to it, so subagent and tool
trajectories form the correct tree in the trace view. Use `startSpan(SpanOptions)` for a generic
span (for example a subagent with a custom `operationId`), and `startTool` / `startTask` for the
common cases:

```java theme={null}
Span root = it.startSpan(SpanOptions.builder("agent.root")
        .kind("workflow").operationId("ai.workflow").build());

Span researcher = root.startSpan(SpanOptions.builder("subagent.researcher")
        .kind("task").operationId("ai.subagent")
        .attribute("subagent.name", "researcher").build());

Span lookup = researcher.startTool("customer_profile_lookup");
lookup.setInput(Map.of("customer_id", "cust_123"));
lookup.setOutput(Map.of("plan", "enterprise"));
lookup.end();

researcher.end();
root.end();
```

You can also start a free-standing tool span that is not tied to an interaction with
`raindrop.startToolSpan("name")`.

***

## Signals

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                                                         |

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

### Self-diagnostics

`selfDiagnose()` reports a signal that surfaces in the dashboard's Self Diagnostics tab — useful
when your agent detects that it is stuck or degraded:

```java theme={null}
raindrop.selfDiagnose(SelfDiagnoseOptions.builder()
        .eventId("evt_123")
        .category("retrieval")
        .description("No documents matched the query after 3 attempts")
        .build());
```

***

## Identifying Users

```java theme={null}
raindrop.identify(IdentifyEvent.builder()
        .userId("user-123")
        .trait("name", "Jane")
        .trait("email", "jane@example.com")
        .trait("plan", "paid") // we recommend "free", "paid", or "trial"
        .build());
```

***

## Attachments

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

```java theme={null}
raindrop.trackAi(AiTrackEvent.builder()
        .event("chat_message")
        .userId("user-123")
        .input("review this snippet")
        .output("looks good")
        .addAttachment(Attachment.builder()
                .type(Attachment.Type.CODE)
                .role(Attachment.Role.INPUT)
                .language("java")
                .name("Example.java")
                .value("System.out.println(\"hello\");")
                .build())
        .addAttachment(Attachment.builder()
                .type(Attachment.Type.IMAGE)
                .role(Attachment.Role.OUTPUT)
                .value("https://example.com/image.png")
                .build())
        .build());
```

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

***

## Configuration

```java theme={null}
Raindrop raindrop = new Raindrop(RaindropConfig.builder()
        .writeKey(System.getenv("RAINDROP_WRITE_KEY"))
        .redactPii(true)
        .debugLogs(!"production".equals(System.getenv("ENV")))
        .build());
```

| 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](/docs/platform/projects); 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 text.                          | `false`                       |
| `.maxTextFieldChars(int)`   | Per-field character cap; oversized fields are truncated.                                             | `1_000_000`                   |
| `.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.                                                    | —                             |
| `.disabled(boolean)`        | When true, the SDK sends nothing.                                                                    | `false`                       |

Call `raindrop.close()` (or `shutdown()`) before your process exits to flush buffered events and
spans under a bounded deadline. 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/`.

***

## Projects

Pass `.projectId(...)` on the builder 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.

```java theme={null}
Raindrop raindrop = new Raindrop(RaindropConfig.builder()
        .writeKey(System.getenv("RAINDROP_WRITE_KEY"))
        .projectId("support-prod")
        .build());
```

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](/docs/platform/projects) for isolation, archival, and the full behavior table.

***

## Reliability

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. `flush()` is non-blocking too.
* **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 a bounded flush-on-shutdown deadline — `close()` returns 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.

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.

***

## Local development with Workshop

[Workshop](https://raindrop.sh) is the local-first trace debugger. Point the SDK at it (or just run
on `localhost` with `RAINDROP_WORKSHOP=1`) and events + spans stream into the UI with no write key:

```java theme={null}
Raindrop raindrop = new Raindrop(RaindropConfig.builder()
        .localWorkshopUrl("http://localhost:5899/v1/")
        .build());
```
