Skip to main content
Beta. The Java SDK is at 0.0.1. 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.

Installation

The SDK requires Java 17+. The artifact coordinates are ai.raindrop:raindrop-java. Gradle:
dependencies {
    implementation 'ai.raindrop:raindrop-java:0.0.1'
}
Maven:
<dependency>
    <groupId>ai.raindrop</groupId>
    <artifactId>raindrop-java</artifactId>
    <version>0.0.1</version>
</dependency>
Source code and releases live in raindrop-ai/java-sdk. Smallest possible program:
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:
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):
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());
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.
Use track() for non-AI product events:
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:
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:
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:
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():
FieldTypeDescription
eventIdStringThe id of the AI event you’re evaluating
nameStringSignal name (e.g. "thumbs_up", "thumbs_down")
typeSignal.TypeOne of DEFAULT, STANDARD, FEEDBACK, EDIT, AGENT, AGENT_INTERNAL
sentimentSentimentPOSITIVE or NEGATIVE
commentStringMerged into properties.comment (for FEEDBACK signals)
afterStringMerged into properties.after (for EDIT signals)
attachmentIdStringOptional attachment id to associate the signal with
propertiesMap<String,Object>Additional metadata
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:
raindrop.selfDiagnose(SelfDiagnoseOptions.builder()
        .eventId("evt_123")
        .category("retrieval")
        .description("No documents matched the query after 3 attempts")
        .build());

Identifying Users

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:
FieldTypeDescription
typeAttachment.TypeCODE, TEXT, IMAGE, or IFRAME (serialized as type on the wire)
roleAttachment.RoleINPUT or OUTPUT
nameStringOptional display name
valueStringContent or URL
languageStringProgramming language (only meaningful for CODE attachments)
attachmentIdStringOptional UUID. The backend auto-assigns one if empty; set it explicitly to round-trip with a signal’s attachmentId.
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());
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.

Configuration

Raindrop raindrop = new Raindrop(RaindropConfig.builder()
        .writeKey(System.getenv("RAINDROP_WRITE_KEY"))
        .redactPii(true)
        .debugLogs(!"production".equals(System.getenv("ENV")))
        .build());
Builder methodDescriptionDefault
.writeKey(String)Your Raindrop write key. Empty/missing → SDK becomes a no-op.
.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/.

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 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:
Raindrop raindrop = new Raindrop(RaindropConfig.builder()
        .localWorkshopUrl("http://localhost:5899/v1/")
        .build());