> ## Documentation Index
> Fetch the complete documentation index at: https://docs.parsaa.app/llms.txt
> Use this file to discover all available pages before exploring further.

# What the SDK Collects

> What is automatic, what your app calls, and what never leaves the device

Three lists. Everything the Orbit SDK can send is in the first two, and everything it deliberately keeps is in the third.

## Automatic, once you start it

You write no telemetry code for any of these.

| Signal                            | Module         | Notes                                                                                                                                                                                                    |
| --------------------------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Lifecycle breadcrumbs             | `OrbitCapture` | `app.foreground`, `app.background`, `app.terminate`, `app.memoryWarning`                                                                                                                                 |
| Crashes                           | `OrbitCapture` | An uncaught `NSException` or a `SIGABRT` writes one small file while the process is dying; the next launch turns it into an `app.crash` event. Both handlers chain to whatever was installed before them |
| MetricKit metrics and diagnostics | `OrbitVitals`  | Launch time, hangs, CPU, disk writes, crash diagnostics. Apple hands an app one payload per device per day                                                                                               |
| `os_log` error and fault entries  | `OrbitCapture` | With `logsEnabled`. Read back from this process's own log store, off the flush path at background priority, at most once a minute                                                                        |
| Network requests                  | `OrbitCapture` | With `networkEnabled`, for the sessions you instrument. Method, path template, host, status, the two body sizes, duration                                                                                |
| StoreKit 2 revenue                | `OrbitCapture` | With `revenueEnabled` and the `revenue` surface on. Purchases, renewals, refunds, expiries, trials and grace periods                                                                                     |
| Wireframe replay                  | `OrbitCapture` | With `replayEnabled`, the `replay` surface on, and `enableReplay(window:)` called                                                                                                                        |

<Note>
  There is no swizzling and no `URLProtocol` anywhere in the SDK. Network instrumentation is a `URLSessionTaskDelegate` you hand a session to, replay taps come from one gesture recognizer added to the window (or a local `NSEvent` monitor on macOS), and push observation is a proxy you install yourself.
</Note>

## What your app has to call

Screen names, business events and identity are things only your app knows. The SDK could infer screens only by reading the view hierarchy, which it will not do.

```swift theme={null}
Capture.screen("PaywallSheet")
Capture.track("checkout.started", attrs: ["plan": "pro", "items": 3])
Capture.breadcrumb("cart.opened")
Capture.setUser(idHash: alreadyHashedAccountId, attrs: ["tier": "pro"])
```

| Call                                                               | What it is for                                                                                                                           |
| ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `Capture.screen(_:)`                                               | The screen name. It is what the session timeline, the screen map and the tap heatmaps are cut on                                         |
| `Capture.track(_:attrs:)`                                          | A business event with scalar attributes                                                                                                  |
| `Capture.breadcrumb(_:)`                                           | A note in the trail, without the weight of an event                                                                                      |
| `Capture.setUser(idHash:attrs:)`                                   | Identity, already hashed by you                                                                                                          |
| `Capture.enableReplay(window:)`                                    | Turns on the wireframe recorder against a `UIWindow` or `NSWindow`                                                                       |
| `Capture.instrument(session:)`                                     | Rebuilds a `URLSession` around the SDK's delegate. `Capture.instrumentedShared` and `Capture.instrument(task:)` are the other two shapes |
| `Capture.observePushNotifications()`                               | Installs a forwarding proxy in front of your notification delegate                                                                       |
| `Capture.pushOpened(campaign:)`                                    | A push that was opened                                                                                                                   |
| `Capture.deepLinkOpened(_:)`                                       | From `onOpenURL` or `application(_:open:)`                                                                                               |
| `Capture.widgetTapped(kind:)`                                      | A widget tap                                                                                                                             |
| `Capture.appClipLaunched()` and `Capture.extensionLaunched(kind:)` | The other two re-engagement entries                                                                                                      |

### About `setUser`

It takes an identifier **your app has already hashed**. The SDK never hashes anything itself, and it refuses a value containing `@` or whitespace, so an email address passed by mistake is dropped on the device. `setUser(idHash: nil)` is signing out: it clears the hash and the attributes together.

Attributes are how a session becomes a user you can find on the Users tab: at most 20, keys up to 64 characters, values up to 256, redacted keys dropped, a long value clipped rather than failing the call. They persist across launches until you clear them or the app is deleted.

### Track names Orbit already understands

Ordinary `Capture.track` names, with attributes the console can build on without configuration. Nothing breaks without them, and they are suggestions rather than a schema.

| Event             | Attributes             |
| ----------------- | ---------------------- |
| `paywall.shown`   | `paywall`, `placement` |
| `search`          | `query`, `results`     |
| `onboarding.step` | `step`, `index`        |
| `feature.used`    | `feature`              |

```swift theme={null}
Capture.track("paywall.shown", attrs: ["paywall": "pro", "placement": "onboarding"])
Capture.track("search", attrs: ["query": term, "results": hits.count])
```

<Tip>
  `paywall.shown` is the one with teeth. Paired with the revenue surface it is what the paywall conversion metric is computed from: the share of people who saw a paywall and bought within 24 hours.
</Tip>

## What never leaves the device

### Never collected at all

No pixels, no screenshots, no view snapshot of any kind. No text: not the accessibility label, not the accessibility value, not what is typed, not a view's rendered content. No IDFA, no App Tracking Transparency prompt, no IDFV, no persistent device identifier, and no raw user identifier.

Replay is held to the same rule. A wireframe frame is geometry, a coarse view type, a coarse accessibility role and four booleans (visible, selected, disabled, focused). The walked type has no field text could travel through, and the test suite proves it twice: by the type's field list, and by walking a fixture tree whose text, titles and accessibility labels are all filled with secrets, then checking the frame does not move by a byte when they change.

An `NSException`'s `reason` is never sent either: that string is formatted by your app and routinely carries the values it was about to show someone.

### Kept locally, sent only on request

Two things the policy can turn on that are recorded at full fidelity into the on-device ring buffer and are **never** part of the ordinary flush.

| Policy field                | What it keeps locally                            |
| --------------------------- | ------------------------------------------------ |
| `surfaces.network.bodies`   | Request and response bodies, per request         |
| `surfaces.logs.bufferLevel` | `os_log` lines from that level up, below `error` |

They leave the device only four ways: a workflow's `uploadBuffer`, an upload request someone made from the console, a crash, or the opening 60 seconds of a live stream. When they do, the console badges the row "captured on request", because a body is on Orbit because somebody asked, never because of a flush.

What bounds a captured body, all at once: the content type must be on the project's list, the bytes must parse as JSON (a body the SDK cannot redact is a body it does not keep), every value under a redacted key at any depth is replaced with `[redacted]`, and what is left is truncated. `Authorization`, `Cookie` and `Set-Cookie` are never captured, from either side, however they are spelled.

### Hashes only

* **User identity** rides the wire as `orbit.user.id_hash`, which your app computed. Orbit never receives an identifier it could resolve to a person, and mapping a hash back is yours.
* **Revenue** sends `sha256(transactionId)` and `sha256(originalTransactionId)`. Never the JWS, never Apple's raw transaction ids. The hashes answer "is this the same purchase" without carrying one.
* **A deep link is recorded as its shape**: `myapp://product/42?token=abc` becomes `myapp://product/{id}`, plus `utm_campaign` when there is one. A network path becomes a template the same way, and a segment that is numeric, a UUID, longer than 64 characters or contains an `@` becomes `{id}`.

### Anonymous mode

A project setting, off by default, in Settings, Analytics. With it on, every batch carries a hash of the installation id and a random salt that rotates at 00:00 UTC on the device, in place of the installation id itself. Two days of one install cannot be joined, retention past D1 stops being answerable, and the console says so out loud rather than drawing a line it cannot support.

Nothing is added to your app for it: the SDK reads the mode from the policy.

## The identifiers that do exist

| Identifier          | What it is                                                                                                                                           | Deleted by   |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ |
| Session id          | A UUID regenerated on every launch                                                                                                                   | Every launch |
| Previous session id | The one id kept across launches, so two consecutive sessions can be put in order. Always overwritten, never an install identifier                    | `purge()`    |
| Installation id     | A random UUID minted on first launch, derived from nothing about the device or the person. It is what lets someone ask *this* install for its buffer | `purge()`    |

Beside them the SDK sends your bundle identifier, version and build, the hardware model string from `uname` (`iPhone17,1`), the OS name and version, the locale and the time zone.

<Info>
  MetricKit call stacks arrive raw and unsymbolicated by design: Apple does not symbolicate on device. Symbolication happens in Orbit, against dSYMs you upload under Settings, Symbols.
</Info>

## On the device

`OrbitCapture` holds a ring of 500 events in memory, dropping the oldest when full and counting what it dropped, plus at most 20 batches or 2 MB on disk. `OrbitVitals` holds at most 50 payloads or 5 MB. Both are written atomically, oldest evicted first, and a batch is deleted when Orbit accepts it, when Orbit rejects it permanently, when a cap evicts it, or when the policy says capture is off.

Vitals are kept in Orbit for 30 days and then deleted. Your end users' data belongs to you, not to Parsaa.
