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

# Troubleshooting

> Diagnose collector failures, endpoint and CSP problems, tenant API responses, missing projections, coverage, and live evidence.

Start with the collector result and browser request sequence. Successful ordinary collection returns a request ID only after the tenant projection is committed.

## The collector returns `ok: false`

`heretic(config)` resolves normal failure paths instead of rejecting into the host application.

| Result                   | Meaning                                                                    | Check                                                                                                       |
| ------------------------ | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `fetch unavailable`      | The run path cannot find global `fetch`.                                   | Run in a browser with `globalThis.fetch`. `fetchImpl` applies to discovery helpers, not session collection. |
| `no edge resolved`       | Resolution found no endpoint and no fallback.                              | Call `resolveEndpoint(config)` and inspect `fallbackEdges`.                                                 |
| `session open failed`    | `GET /n` failed, returned a non-success status, invalid JSON, or no nonce. | Inspect reachability, TLS, CORS, CSP, status, and response body.                                            |
| `edge returned <status>` | The final `/c` or `/z` request returned a non-success status.              | Confirm the nonce belongs to the selected edge and inspect the response.                                    |
| `post timed out`         | The final POST exceeded `postTimeoutMs`, which defaults to 8,000 ms.       | Inspect payload POST timing and page lifecycle.                                                             |
| Other text               | The collector converted an exception to a string.                          | Record it for diagnosis, but do not parse it as an enum.                                                    |

The collector runs once per page. Later calls reuse the first run. Check the options passed to the first call.

## The collector does not appear to start

With `auto: true`, the collector waits for page load when necessary and then for an idle callback. Browsers without `requestIdleCallback` wait no more than 500 ms.

Use the browser network panel to confirm:

```text theme={null}
GET   <edge>/n
start GET <edge>/healthz?n=<nonce>
run   browser measurements
POST  <edge>/c?n=<nonce>
```

With `zdr: true`, the last path is `/z`.

`budgetMs` governs scheduling inside the core measured phase. Rendering and declared-surface collection run later. `postTimeoutMs` applies only to the final POST.

## Endpoint resolution picks the wrong edge

```js theme={null}
import { resolveEndpoint } from '@heretic-hq/collector';

const endpoint = await resolveEndpoint({
  siteKey: 'hrtc_live_0123456789abcdef01234567',
});
```

Resolution checks explicit `endpoint`, injected edge, session cache, optional DoH, optional control plane, and fallback in that order. A successful discovery can remain in `sessionStorage` until its TTL expires. Clear `heretic.edge.v1` when testing a changed discovery setup.

The browser must connect directly to the measurement edge. A CDN or reverse proxy in front of it causes the edge to observe that intermediary's TCP and TLS construction.

## A request is blocked

Check these boundaries:

* `connect-src` permits the paired `tcp.<pop>.probe.heretic.tech` and `quic.<pop>.probe.heretic.tech` origins plus configured discovery endpoints.
* The endpoint presents valid HTTPS and CORS responses.
* Content filters or network controls do not block the edge hostname.
* `/n` and the final POST use the paired TCP origin. Both `/healthz` requests use the QUIC origin for the same POP.
* The page remains active long enough to finish the POST.

The parallel probe uses `blob:` workers. If CSP blocks them, the payload records:

```text theme={null}
parallel.failure: "csp-blocked"
```

Other sections continue. Permit the worker with:

```text theme={null}
worker-src blob:
```

## The tenant API returns `404`

Check that the identifier is the raw 32-character lowercase hexadecimal value returned by ordinary collection. Do not add or remove a prefix.

The same `404` covers:

* a missing or mistyped ID
* ZDR
* a row attributed to another site
* an archived site or revoked ownership
* tenant deletion
* retention pruning

These states are deliberately indistinguishable.

## The tenant API returns `401`

`401` means the bearer key is missing, malformed, unknown, or revoked.

Confirm that:

* the header starts with `Bearer `
* the value is an `hrtc_sk_<32 lowercase hex>` key
* the value is complete and remains server-side
* the key has not been revoked
* the site has not been retired

A valid secret for the wrong site returns `404`, not `401`.

The route defines no application-level `429` contract. Treat other statuses as unexpected HTTP failures, not verdict values.

## Collection succeeds but no row appears

Confirm that the first collector call used an active public key for the same site as the server secret. The Worker rechecks active key and site ownership in the immutable write. A projection failure makes ordinary collection fail and returns no request ID.

## A ZDR request cannot be retrieved

This is expected. `/z` creates no assessment, identity, projection, NDJSON record, request ID, or retrievable session. The collector returns `{ ok: true }`.

## The tenant response lacks deep evidence

The tenant endpoint returns a compact projection. It omits the full session, raw IP, raw claims, detailed transports, geometry, the full concealment report, the sensor report, `contradicting_families`, and per-finding sensor admissions.

After ownership is verified, the dashboard maps the row's recorded POP through a fixed Warsaw, Singapore, and San Jose allowlist. It uses a separate server-only operator credential for that POP. If the full live record is gone or the POP cannot return it, the dashboard falls back to the stored projection. Browser and database values cannot select an arbitrary edge host.

Live sessions normally expire 900 seconds after creation. Memory pressure can remove them sooner. Failed local persistence can retain an unpersisted session through the retry window, up to about 20 additional minutes at the current 30-second sweep interval.

## A family has incomplete coverage

Read `coverage.state` before interpreting status:

| State           | Meaning                                                                                                                                                 |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `measured`      | At least one supported rule had usable input. Other rules may still have abstained.                                                                     |
| `unavailable`   | Required input did not arrive.                                                                                                                          |
| `not-yet-ruled` | Input arrived, but the current catalogue could not rule on it, or a relevant sensor admission impaired an otherwise measured, finding-free wire family. |

Common collector-side causes include:

* `budgetMs` elapsed before a measured section started
* one probe failed while later sections continued
* CSP blocked the parallel worker
* `surface: false` removed rule-bearing inputs
* the browser did not expose a required API

`render: false` does not disable identity. `rawSurface: false` does not disable the rest of the declared surface.

## The verdict is `insufficient`

Inspect the stable `insufficient_reason`:

| Value             | Meaning                                                                        |
| ----------------- | ------------------------------------------------------------------------------ |
| `sensor-gap`      | Required transport evidence did not join after the return grace.               |
| `awaiting-return` | Required transport or client evidence is absent within the grace window.       |
| `no-return`       | Transport evidence joined, the grace elapsed, and no rulable payload returned. |

An unreachable edge produces a collector failure, not an insufficient assessment.

## Identity IDs are absent or changed

Identity version `2` omits an ID when its anchors are absent:

* `machine` needs cores, display, and timezone.
* `instance` needs canvas, fixed WebGL raster, or audio.

Compare IDs only within one identity version and one public-site-key scope. Public-key rotation changes them. Changes to component inputs can also change them.
