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

# Capture Policy

> One document per project deciding what the SDK on a device may collect

Everything the SDK collects is decided by a document the server owns and the **device** evaluates. One poll carries the whole truth, so a device that has been offline for an hour still applies the policy it has.

The whole app-side integration is the `start` call. The rest lives in **Settings**, then **Project**, then **Telemetry** (`/settings/project/telemetry`).

<Note>
  Nothing here is pushed. A device applies a change on its **next poll**, so turning a surface on and watching a page for ten seconds proves nothing.
</Note>

## The eight surfaces

Each has a switch and a share of sessions.

| Surface   | What it covers                                                                   | Also needs                                    |
| --------- | -------------------------------------------------------------------------------- | --------------------------------------------- |
| `vitals`  | MetricKit metrics and diagnostics: launch time, hangs, CPU, disk writes, crashes | `OrbitVitals` started                         |
| `events`  | `Capture.track` events and breadcrumbs                                           |                                               |
| `screens` | The `screen` breadcrumb, which the session timeline and the screen map read      | Your app calls `Capture.screen`               |
| `taps`    | `tap`, `scroll`, `keyboard`, `orientation` interaction events                    |                                               |
| `network` | Outgoing request spans, plus bodies and the header allowlist                     | `networkEnabled`, and an instrumented session |
| `logs`    | The app's own log lines                                                          | `logsEnabled`                                 |
| `replay`  | Wireframe layout frames                                                          | `replayEnabled` in the app                    |
| `revenue` | StoreKit 2 purchases, renewals, refunds, trials and grace periods                | `revenueEnabled` in the app                   |

`network` and `logs` are off by default. `replay` and `revenue` are gated twice: your app opts in locally and the policy allows it, and neither side can turn one on alone.

<Info>
  A project that never saved a policy is running the defaults, and the card says so rather than presenting them as a decision someone made: vitals, events, screens and taps on at full rate, network, logs, replay and revenue off.
</Info>

## Sampling is per session, not per event

`sampleRate` is the share of **sessions** that collect a surface. The decision is made once per session on the device, from a stable hash of the session id, so it holds for the whole launch and needs no stored state.

Fifty percent means half of the sessions with **every** tap, not half of the taps. Each surface salts the hash with its own name, so `screens` at 0.5 and `taps` at 0.5 are not the same half of the sessions: otherwise every surface at fifty percent would watch one half of your users and never the other.

<Warning>
  **Revenue is never sampled.** It is the one surface with no share to set, and the column says "never sampled" instead of offering one. Half of somebody's money is not a smaller revenue chart, it is a wrong one, and Orbit cannot scale it back up because it does not know which purchases the device kept quiet about.
</Warning>

## Targets

A target raises or lowers surfaces for the devices it matches: "full capture for the store build on iPhone 15", or "this tenth of users".

Matchers: an app version range (`>=2.1.0 <3.0.0`, no carets), an OS version range, device model identifiers, locale prefixes, a stable bucket of the user id your app set, the user attributes your app set, and a share of sessions.

Exactly one target applies, the **first from the top** whose matchers all hold, which is why the rows move up and down: there are no priorities to reason about. A target that names no matcher matches every device and is only allowed as the last row. Merging is per surface and per field, so a target that raises `taps` leaves the other seven as the project has them.

The matching runs on the device, and it is the same code as Orbit's: the test fixture is generated by running Orbit's own evaluators, and the Swift port has to reproduce every answer in it.

## Workflows

Rules the device runs against its own ring buffer. When an event matches, or the app crashes, or a screen is visited five times in two minutes, the device then flushes now, raises some surfaces for a few minutes, or sends the buffer.

They never reach a server: the device decides, at the moment the evidence is worth having. The cooldown is what stops a crash loop uploading on every launch.

## The kill switch

Stops everything at once: every surface off, the ring dropped, the disk queue deleted, nothing sent, not even what was already queued, while the device keeps polling, which is how it hears that it may resume.

It is a field of its own rather than "turn every surface off", so turning it back off restores the surfaces **exactly as they were**. It is not sampling at zero: a sampled-out device still buffers and still uploads on a workflow, a killed device does nothing but poll. Enabling it asks for a reason, and the reason is what the change log shows.

## The ring buffer and the window

The buffer holds the last window of events **at full fidelity regardless of sampling**, bounded in RAM and on disk. Nothing in it is sent by the ordinary flush.

|              | Default    | Range                               |
| ------------ | ---------- | ----------------------------------- |
| Window       | 15 minutes | 1 minute to 7 days (10,080 minutes) |
| Disk ceiling | 8 MiB      | 0 (RAM only) to 256 MiB             |
| RAM ceiling  | 2 MiB      | 64 KiB to 8 MiB                     |

It is kept as append-only segments, at most 1 MiB or 5 minutes each, compressed on close and deleted oldest first, never rewritten. It survives relaunches. The editor draws the worst case per device as you type it ("256 MB holding up to 7 days of history") and warns above 64 MiB, which is a quarter of a gigabyte of somebody's phone held for a crash nobody has reported yet.

The buffer leaves the device four ways: a workflow's `uploadBuffer`, a crash on the next launch, an upload request from the console, or as a live stream's opening frames. It always arrives labelled as what it is.

## Upload requests

On the Fleet tab, or on a session's "This device" card, the console asks one installation for what it is holding.

Nothing is pushed: the ask rides that device's **next policy poll**, and it expires unanswered after 24 hours. The buffer arrives as an ordinary session labelled "Buffer, asked for". A window longer than a few minutes arrives as numbered parts, so the row counts them as they land ("7 of 42 parts") with the bytes received so far, and an app killed mid-upload resumes from the last part the server took. Cancelling also lets that device go back to its ordinary poll interval.

A cohort no device has polled from cannot be asked, because the ask travels on the poll.

## Watch a device live

The other half of the same question: instead of the last few minutes, the next few. Press **Watch live** on a session, a session's device card, or a Fleet row's device list, and the tail opens filtered to that install.

* **No socket, no push channel and nothing kept alive between polls.** The device learns it is being watched on its next poll, so the first frame is up to a minute away, and stopping is as slow as starting.
* While a stream is open the SDK sends what it records within two seconds of recording it, over the ingest route it already uses. The timer coalesces: a burst is one request, at most one request every two seconds, at most 200 events per request.
* The first frame is prefaced with the last 60 seconds of the ring buffer, so the console opens with context instead of an empty timeline.
* **A stream raises nothing.** It is a cadence, not a permission: a surface this session does not collect is not streamed either, no sample rate moves, the preface is gated and redacted like every other batch, and the kill switch still stops the device dead.
* Ten minutes by default, thirty at most, five devices at a time per project. Admin only, like every control that reaches a phone.

## Poll cadence

`GET /v1/ingest/policy`, request driven. The interval is a floor on how often the SDK will ask, never a timer that wakes a sleeping app.

| Situation                             | Cadence                                              |
| ------------------------------------- | ---------------------------------------------------- |
| App in the background                 | `poll.intervalSeconds`, 900 seconds by default       |
| **App in the foreground**             | `poll.pendingIntervalSeconds`, 60 seconds by default |
| An open upload request or live stream | `poll.pendingIntervalSeconds`, whichever is lower    |

The foreground drop is why "Watch live" promises a minute rather than fifteen: a device somebody is holding cannot hear about a stream, an upload request or the kill switch until it asks. A transient inactive state (a call, Control Centre, macOS losing frontmost) is not backgrounding and does not lift the drop. A backgrounded app returns to the slower cadence, which is where the battery argument lives.

## Flush cadence

There is no timer. A flush happens at `start()`, when the app backgrounds or terminates, when the ring fills, when the flush interval has elapsed **and** the app records an event, and when you call `Capture.flush()`. The defaults are a 900 second interval and 200 events per flush.

## Redacted keys and buffer-only settings

Beside the surfaces: the redacted keys, one per line, where a property with one of those names is dropped on the device before anything is written to disk; the network bodies sub-card (size, content types, and the fact that a body never rides an ordinary flush); the request header allowlist, which **does** ride the flush as `http.request.header.<name>` and refuses `Authorization`, `Cookie` and `Set-Cookie` whatever you list; and the buffer log level, which costs disk on the device and nothing on the wire.

## The change log

One row per save: when, who, what changed, and the version it produced. A kill switch save is marked as one, so it is findable without reading a diff. The card header says which version the project is on and how many devices read it in the last hour.
