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

# Swift (Beta)

> Track AI interactions, events, signals and traces from Swift on iOS, macOS, tvOS, watchOS, visionOS and Linux.

## Installation

Add the package with Swift Package Manager (Xcode: **File ▸ Add Package Dependencies…**):

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

Requires Swift 6, iOS 16 / macOS 13 / tvOS 16 / watchOS 9 / visionOS 1, or Linux. No third-party dependencies.

```swift theme={null}
import Raindrop

let raindrop = RaindropClient(writeKey: "YOUR_WRITE_KEY")
```

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

```swift theme={null}
let interaction = raindrop.begin(BeginOptions(
  userId: "user-123",
  event: "chat_message",
  input: "Can you suggest a calm Saturday morning in San Francisco?",
  model: "gpt-4o",
  convoId: "conv-123",
  properties: ["surface": "chat"]
))

let output = try await callLLM()

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

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

### Updating an interaction

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

```swift theme={null}
let interaction = raindrop.resumeInteraction(eventId: "evt_123", beganAt: savedBeginDate)
await interaction.finish(output: "Here is a shorter version of that itinerary.")
```

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

```swift theme={null}
let eventId = 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"
))
```

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

## Tracking signals (feedback)

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

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

## Identifying users

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

## Feature flags

```swift theme={null}
raindrop.trackAi(AiEvent(userId: "user-123", event: "chat_message", input: q, output: a,
                         featureFlags: ["new_prompt": "variant_b"]))
```

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

```swift theme={null}
let run = interaction.startSpan("agent.run", operationId: "ai.workflow")

let llm = interaction.startLlmSpan(
  "anthropic.messages",
  provider: "anthropic",
  model: "claude-sonnet-4-5",
  messages: [.system("You are concise."), .user("What's the weather in Paris?")],
  tools: [ToolDeclaration(name: "get_weather", inputSchema: ["type": "object"])],
  parent: .span(run)
)
llm.setOutput("I'll check the weather.")
llm.setUsage(TokenUsage(inputTokens: 47, outputTokens: 11))
llm.end()

let forecast = try await interaction.withTool("get_weather", input: ["city": "Paris"], parent: .span(run)) {
  try await weather(for: "Paris")
}

run.end()
await interaction.finish(output: "It's sunny in Paris.")
```

* `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:

```swift theme={null}
let answer = try await interaction.withSpan("agent.run", operationId: "ai.workflow") { _ in
  let llm = interaction.startLlmSpan("anthropic.messages", model: "claude-sonnet-4-5", input: question)  // child of agent.run
  llm.setOutput(draft)
  llm.end()
  return try await interaction.withTool("get_weather", input: ["city": "Paris"]) {  // child of agent.run
    try await weather(for: "Paris")
  }
}
```

* 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 | Detached |
| - | - | - |
| Events | Part of the parent's event | Its own event, linked to the parent |
| Spans | Nested under one subagent span in the parent's trace | Its own trace |
| In Raindrop | A subagent within the turn | A subagent pill that opens the child event |
| API | `inlineSubagent(_:input:) { }` | `subagent(name:input:)` + `beginSubagent(_:)` |

### Inline subagents

```swift theme={null}
let summary = try await turn.inlineSubagent("order-worker", input: .string(question)) {
  // Model calls, tools and spans started here nest under the subagent automatically.
  try await worker.run(question, interaction: turn)
}
```

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

```swift theme={null}
// Parent turn: record the launch and get the ids the child needs.
let handoff = turn.subagent(name: "researcher", input: ["topic": "swift"])

Task.detached {
  let run = raindrop.beginSubagent(handoff, input: "Research Swift concurrency")
  do {
    let notes = try await research()
    await run.finish(output: notes)
  } catch {
    await run.fail(error)   // a CancellationError is recorded as a cancellation
  }
}

await turn.finish(output: "I've asked the researcher to look into it.")
```

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

```swift theme={null}
var config = RaindropConfiguration(writeKey: "YOUR_WRITE_KEY", projectId: "support-prod")
config.redactPii = true
config.beforeSend = { payload in
  var p = payload
  p.body["properties"] = .object([:])   // edit, or return nil to drop
  return p
}
let raindrop = RaindropClient(configuration: config)
```

| 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 | Upper bound for `finish`, `flush` and `close` |
| `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) |

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

```swift theme={null}
config.appGit = AppGitConfig(commitSha: BuildInfo.commitSha, commitDirty: false, branch: BuildInfo.branch)
```

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

```swift theme={null}
try await raindrop.validate()   // checks the write key and project; sends no data
```

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


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