Skip to main content

Installation

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.

Quick start: Interaction API

interaction.eventId is the event’s id (a UUID unless you pass eventId). Use it for signals.

Updating an interaction

Each send carries the interaction’s full state. Raindrop drops partial events that have neither input nor output, so the SDK holds updates until the interaction has one of them.

Resuming an interaction

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.

Single-shot tracking (trackAi)

trackAi needs input, output, or both. Use trackEvent for non-AI events:

Tracking signals (feedback)

Identifying users

Feature flags

Tracing

Spans are exported to Raindrop as OpenTelemetry (OTLP JSON). Start them from an interaction so they’re linked to its event, user and conversation:
  • startToolSpan(_:input:) gives you a ToolSpan to end yourself. trackTool(_:input:output:error:duration:) records a call that already happened.
  • withTool records the result, or the thrown error, and rethrows.
  • Token usage is sent as raw provider counts (gen_ai.usage.*); Raindrop computes cost.
  • RaindropClient.startSpan, startLlmSpan and startToolSpan create spans outside an interaction. Pass eventId: to link them to an event.
  • span.ref is a SpanRef (hex trace and span ids). Save it to parent spans recorded later, even in another process: parent: .ref(savedRef).
  • RaindropConfiguration.tracingEnabled = false turns all spans off.

Nesting spans automatically

A span started without parent: nests under the current span if that span belongs to the same event. withSpan, withTool and inlineSubagent make their span current while their closure runs, so you don’t have to pass parents around:
  • The current span is task-local: Task {} and async let inherit it, Task.detached doesn’t. Span.current reads it, and span.withCurrent { } sets it for any span.
  • Spans for a different event never nest under it, so a detached subagent (its own event) stays separate even when started inside the scope.
  • An explicit parent: always wins. parent: .root starts a new trace.

Subagents

A subagent can be recorded in two ways. Use inline when the parent waits for the subagent as part of the same turn, and detached when it runs on its own.

Inline subagents

inlineSubagent records a tool span tagged raindrop.agent.role=subagent and raindrop.subagent.name, makes it current while the closure runs, records the result (or the thrown error) and rethrows.

Detached subagents (experimental)

A detached subagent is work that reports as its own event, linked back to the turn that launched it. It can run in a Task.detached, on another thread, or in another process. The link is just ids, carried on span attributes (raindrop.handoff.*).
  • subagent(name:) records a launch_subagent tool span on the parent with the child’s event id. Later spans in that turn are opted in so a turn whose only content is launches still becomes an event.
  • beginSubagent(_:) opens the child’s interaction under handoff.childEventId. Every span started from it carries raindrop.handoff.parentEventId, parentSpanId, name and raindrop.agent.role=subagent. The child’s user and conversation default to the parent’s.
  • End the child with finish(output:), fail(_:) (records an errored ai.subagent.failed span if none of its spans errored, and outputs Aborted: …) or cancel(reason:) (sets raindrop.handoff.terminal=cancelled on the child event).
  • SubagentHandoff is plain Codable data. To run the child in another process, encode it and send it however you like. You can also build one from ids you stored yourself: SubagentHandoff(parentEventId:parentSpanId:childEventId:name:).
  • Only accept a handoff from code you trust: it decides which event the child’s data is attributed to.
  • Not included: the x-raindrop-handoff / traceparent header carrier used by the TypeScript and Python SDKs. To hand off to a service using those SDKs, pass the ids yourself.

Configuration

Retries: rate limits (429) and server errors are retried with backoff. Other 4xx responses are not retried; the payload is dropped with a warning.

How calls behave

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

Application Git provenance

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. The SDK never inspects a Git checkout itself.

Raindrop Workshop

Set RAINDROP_WORKSHOP=1 (or localWorkshopURL) to mirror every request to a local Workshop daemon at http://localhost:5899. With no write key, the client sends to Workshop only. On a physical device, point localWorkshopURL at your Mac’s address, and allow local networking in your app’s App Transport Security settings (NSAllowsLocalNetworking).

Validating your setup

Known limitations

  • No automatic instrumentation of LLM clients: record interactions and spans with the APIs above.
  • Detached subagents are experimental, and there’s no header carrier (toHeaders / fromHeaders) yet; pass the ids explicitly.
  • trackAi doesn’t create a span on its own; use the tracing APIs.
  • The SDK doesn’t read Git or CI metadata automatically; pass build identity explicitly.
  • resumeInteraction in a new process can’t recover fields set earlier in another process. Updates merge into the stored event server-side.