Automatic, once you start it
You write no telemetry code for any of these.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.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.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
OrdinaryCapture.track names, with attributes the console can build on without configuration. Nothing breaks without them, and they are suggestions rather than a schema.
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. AnNSException’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.
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)andsha256(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=abcbecomesmyapp://product/{id}, plusutm_campaignwhen 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
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.
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.
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.