start call. The rest lives in Settings, then Project, then Telemetry (/settings/project/telemetry).
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.
The eight surfaces
Each has a switch and a share of sessions.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.
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.
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.
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.
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.
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 atstart(), 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 ashttp.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.