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

# Orbit Troubleshooting

> Why nothing is arriving, and the seven things it usually is

## Why is nothing arriving

Work down the list. They are ordered by how often each one is the answer, and only some of them are a problem.

<AccordionGroup>
  <Accordion title="1. The SDK key is missing, wrong or revoked" icon="key">
    Nothing reaches Orbit until an app sends a key the project owns. Check **Settings**, then **Project**, then **SDK**: if the panel says no key exists yet, create one. Keys are shown **once**, so a lost key is replaced rather than recovered, and a revoked key is refused for any app still sending it.

    The key goes in the app's build configuration or a secret store, never the repository, and it is sent as the `X-Orbit-Key` header. It identifies the project rather than a person, and it grants no read access.
  </Accordion>

  <Accordion title="2. The app never called the SDK, or called it with enabled: false" icon="code">
    Collection is off until you turn it on. `enabled` defaults to `false` and is the privacy opt in, so a build that starts the SDK with a literal `false`, or with a consent value that has not been granted yet, sends nothing by design.

    Check that `Orbit.Vitals.start` or `Orbit.Capture.start` really runs on the path the build takes, once, early, on the main thread.

    Two things are your app's to call and will never appear on their own: `Capture.screen(_:)`, which the session timeline, the screen map and the tap heatmaps are cut on, and `Capture.setUser(idHash:)`, without which the Users tab and the users unit in Analytics stay empty.
  </Accordion>

  <Accordion title="3. The surface is off in the capture policy" icon="toggle-off">
    `enabled` says the SDK **may** send. Whether it does is the project's call, in **Settings**, then **Telemetry**.

    Projects created before 8 September 2026 kept whatever answer they already had, which for most of them is off. `network`, `logs`, `replay` and `revenue` are off by default on every project, so a session can be arriving with no requests, no log lines, no wireframe and no purchases in it and nothing be broken.

    `replay` and `revenue` need **both** sides: the app's `replayEnabled` or `revenueEnabled`, and the policy switch. Neither turns one on alone.

    Also check the kill switch. A killed device does nothing but poll, and the change log says who enabled it and why.
  </Accordion>

  <Accordion title="4. The device has not polled yet" icon="clock">
    The capture policy is never pushed. A device applies a change on its **next poll**, so turning a surface on and watching the page for ten seconds proves nothing.

    | Situation                             | Cadence                |
    | ------------------------------------- | ---------------------- |
    | App in the background                 | 900 seconds by default |
    | App in the foreground                 | 60 seconds by default  |
    | An open upload request or live stream | 60 seconds by default  |

    A **live stream's first frame is about a minute away**, not instant, and stopping one is as slow as starting it. An **upload request** rides the next poll too, and expires unanswered after 24 hours.

    Vitals are slower still, and that is Apple: MetricKit hands an app one payload per device per day, so the first vitals card appears within a day of the SDK starting, not within minutes.
  </Accordion>

  <Accordion title="5. The workspace is on Free, which refuses ingest" icon="ban">
    Continuous SDK ingest is a plan feature. On **Free**, every `POST /v1/ingest/*` an app makes with the project's key is answered `402` and nothing it sends is stored. The key still exists and the policy poll still answers, so a device can still be told to stop.

    This is not a page waiting for a device, it is a page refusing what one sends, and no other reason on this list would ever explain it. Telemetry says so in a sentence above the ordinary empty states. Evidence on Free is uploaded by hand instead; Pro and above ingest continuously. See [Overview](/orbit/overview) for the tier table.
  </Accordion>

  <Accordion title="6. A browser upload fails before it starts (R2 CORS)" icon="cloud-arrow-up">
    Browser uploads (the Crashes dropzone, the Performance dropzone) `PUT` their bytes straight to a presigned storage URL, cross-origin from the console and with a signed `Content-Type` header, which makes it a preflighted request. An object storage bucket with **no CORS rules** fails every one of those before it starts, and the upload row is left `pending` forever with no visible error.

    The symptom is exactly that: a row that never leaves `pending`, from the browser only. The CLI is unaffected, because it is not a browser and there is no preflight, so `orbit upload` is both the workaround and the way to tell the two apart.

    On the hosted console this is already configured. On a self-managed bucket, add a CORS policy allowing your console origin, the `PUT` method and the `Content-Type` header.
  </Accordion>

  <Accordion title="7. The build is pointed at the wrong environment" icon="arrows-split-up-and-left">
    A TestFlight build and an App Store build are the same binary with different surroundings, and two things routinely differ between them.

    **The ingest URL and key.** If the values come from `Info.plist` or a build configuration, check which configuration the archive was built with. A TestFlight build carrying a staging ingest URL reports perfectly, to somewhere else.

    **The revenue environment.** StoreKit reports `production`, `sandbox` or `xcode`, and **only `production` reaches your numbers**. Sandbox and Xcode transactions are stored so a developer testing StoreKit sees rows arrive, and they deliberately do not move the numbers anybody reads. A TestFlight tester's purchase is a sandbox purchase.

    Crash reports differ too: TestFlight crash feedback is pulled every 30 minutes through your App Store Connect key, while App Store crashes are not exposed by Apple's API at all and arrive as `.ips` reports somebody uploads or as MetricKit diagnostics from the SDK.
  </Accordion>
</AccordionGroup>

## Other things that read as empty and are not

* **A session that sent replay frames and nothing else.** Layout frames ride the same pipeline as events but never appear in the live tail and never become issues. A session that looks empty in the tail can still have a full wireframe behind it.
* **A vitals-only session.** `OrbitVitals` alone carries MetricKit payloads and no events, so its timeline is empty by construction.
* **A sampled-out session.** `sampleRate` is per session: half the sessions with every tap, not half the taps. A session outside the share collects that surface at all.
* **`--` is not zero.** It means no value has arrived. A crash free rate reads `--` when the window holds no session, which the caption tells apart from a failed read.

## App Store Connect says 403 on analytics but Release works

The role the key needs for analytics reports is not the role Release needs. Requesting a report type for the first time needs an **Admin** key; downloading the reports needs Admin, Account Holder, Finance, or Sales and Reports. A key with only Developer or App Manager reads Release perfectly and gets a 403 on the reports sync, so the two record their outcomes separately and the card shows Apple's own message. See [Analytics](/orbit/analytics).

## Refused notification signatures above zero

On **Settings**, then **Analytics**, the notification card counts bodies that arrived with a signature Orbit refused. A number above zero there is a misconfiguration, almost always your notification URL pasted into another app's App Store Connect entry.

## Getting help

<Card title="Contact support" icon="envelope" href="mailto:hello@parsaa.app" horizontal>
  Include the project name, the SDK version, and what the empty state on the page actually says. The empty states are written to name which of the reasons above applies.
</Card>
