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

# zespan doctor

> Diagnose SDK setup issues — bad API key, unreachable API, no data arriving, or a provider client wrapped in the wrong order — before they turn into a churned trial.

`zespan doctor` runs a fixed set of checks against your project and prints a
pass/warn/fail/skip summary. It exists for one specific, common failure: a
customer installs the SDK, sees nothing in the dashboard, and has no way to
tell whether the problem is their API key, their network, their code, or
nothing at all (a project that just hasn't sent its first trace yet).

## Quick start

<CodeGroup>
  ```bash npx theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  npx @zespan/cli doctor --api-key $ZESPAN_API_KEY
  ```

  ```bash Installed theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
  zespan doctor
  ```
</CodeGroup>

With no flags, `doctor` reads `ZESPAN_API_KEY` from the environment and
`.zespan.yaml` from the current directory — see [CLI overview](/cli/overview)
for the full precedence rules. Self-hosted deployments should also pass
`--api-url` (or set `ZESPAN_API_URL`).

## What it checks

Checks run in a fixed order, grouped under four headings. Every `fail` or
`warn` result comes with at least one actionable fix line, printed indented
under it (`→`).

### Configuration

<AccordionGroup>
  <Accordion title="API key — calls GET /v1/sdk/whoami">
    Confirms the key authenticates and reports which project it belongs to.
    This is the first check, and the only one whose *failure* short-circuits
    every check below it (see [Short-circuit on a bad key](#short-circuit-on-a-bad-key)).

    * **pass** — `Valid — project "<name>" (<id>)`
    * **fail** — the key was flatly rejected (HTTP 401). Fix: double-check
      `ZESPAN_API_KEY` / `--api-key` against **Settings → API Keys**, or
      rotate if the key was revoked.
    * **skip** — a network failure, not a rejected key (DNS, TLS, timeout,
      connection refused). Fix: check connectivity and any corporate proxy.
  </Accordion>

  <Accordion title="API reachability — GET /health">
    Independent of the whoami call — this hits the API host's root
    `/health` route, so it still runs and gives a useful second data point
    even when the key check above failed on the network (not on the key).

    * **pass** — `<host> responded in <ms>ms`
    * **fail** — names the host it couldn't reach, and lists any
      `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY` (or lowercase) environment
      variables that were set at the time — or says none were set, since an
      unconfigured egress proxy on a corporate network is a common cause.
  </Accordion>

  <Accordion title="Environment">
    Reports the resolved `--env` / `ZESPAN_ENVIRONMENT` / `.zespan.yaml`
    `environment` value, or **skip** if none is configured. Purely
    informational — there's nothing to pass or fail here.
  </Accordion>
</AccordionGroup>

### SDK

<AccordionGroup>
  <Accordion title="SDK version">
    Compares the `@zespan/sdk` version against the latest version the API
    reports. When `node_modules/@zespan/sdk/package.json` is readable,
    compares the ACTUALLY RESOLVED installed version; otherwise falls back to
    the range declared in your `package.json` (`dependencies` or
    `devDependencies`, e.g. `^1.0.0`) and labels it "declared range" rather
    than "installed", since a range's floor isn't necessarily what's really
    installed.

    * **skip** — `@zespan/sdk` isn't a dependency at all (a Python-only or
      OTel-native project is expected to skip this), or the API didn't
      report a latest version to compare against.
    * **warn** — the compared version is behind. Fix: `pnpm add @zespan/sdk@latest`.
    * **pass** — up to date.
  </Accordion>

  <Accordion title="Wrapper ordering — the most common cause of &#x22;I see nothing&#x22;">
    See [Wrapper ordering: a heuristic, not a guarantee](#wrapper-ordering-a-heuristic-not-a-guarantee) below.
  </Accordion>
</AccordionGroup>

### Data flow

<Accordion title="Data flow — calls GET /v1/projects/:id/ingest-health">
  Checks whether spans have actually arrived recently, using the project id
  you configured (or the one your API key resolves to). Reports a **fail**
  once your integration has gone more than 15 minutes without a span, with
  the last-seen timestamp and 24h span count.

  A `lastSpanAt` of `null` — no span in the last 90 days — is reported as
  **skip**, not fail: the API bounds this query to a 90-day lookback for
  ClickHouse partition pruning, so a brand-new project that hasn't sent its
  first trace yet and a project whose integration went quietly dead over 90
  days ago are indistinguishable from this response alone. `doctor` says so
  explicitly rather than guessing, and gives a fix path for both readings.

  If `--project`/`ZESPAN_PROJECT_ID` names a project that doesn't match the
  one your API key actually belongs to, this check **fails** with a plain
  message naming both project ids — one of the five causes `doctor` exists to
  diagnose — instead of the raw `HTTP 403: {"error":"API key does not match
      the requested project"}` the API returns.

  Any ingest rejections recorded for the project are folded into the detail
  and fix lines by reason and count.
</Accordion>

### Policy & redaction

<Accordion title="PII redaction">
  Reads the project's resolved redaction policy from the same whoami call.

  * **pass** — the server reports redaction as on (names the preset, if one
    is set).
  * **skip** — the server didn't report a redaction policy at all, OR
    reported it as off. The server-side policy has no way to be changed
    today, so `doctor` never `warn`s about it — a warning a user has no way
    to clear is worse than no check at all. The skip detail still names the
    real, working client-side fix (`zespan.init({ redactPii: true })`,
    optionally scoped with a preset: `gdpr`, `hipaa`, `ccpa`, `pci-dss`,
    `soc2`, `finance`, `education`, or `transportation`) in case your SDK is
    already configured that way — client-side redaction happens before data
    reaches the API, which this check can't see.
</Accordion>

## Wrapper ordering: a heuristic, not a guarantee

A provider client (`OpenAI`, `Anthropic`, `Mistral`, `Groq`, `Cohere`,
`GoogleGenAI`) constructed **before** `zespan.init()` runs never gets patched
by the SDK — every call it makes is invisible to Zespan. This is the single
most common cause of "I installed the SDK and see nothing," so `doctor`
scans your project's source files (TypeScript, JavaScript, and Python; up to
500 files, 8 directories deep, skipping `node_modules`/`dist`/`.venv` and
similar) looking for it.

The rule it applies is deliberately narrow: it only flags a provider
constructor at **module scope** (zero leading indentation — not inside a
function, method, or class body) in a file where `zespan.init(` does not
appear on an earlier line of that same file.

<Warning>
  **This is a heuristic and it can produce false positives.** The check reads
  one file at a time — it has no way to know that `zespan.init()` already ran
  in a *different* module that got imported first. If your actual entry point
  calls `zespan.init()` before it imports the file `doctor` flagged, the
  provider client in that file genuinely runs after init and the warning is a
  false positive.

  **How to tell:** trace your own import graph. If the module that calls
  `zespan.init()` is imported (directly or transitively) before the flagged
  file anywhere in your app, you're safe — the flagged construction really
  does run after init, `doctor` just can't see across files to confirm it.
  If you can't establish that ordering, treat the warning as real.
</Warning>

```
✓ No module-scope provider clients found before zespan.init()   ← pass
⚠ 1 provider client constructed at module scope before           ← warn
  zespan.init() in your project.
  → src/openai.ts:3 — construct the OpenAI client after
    zespan.init(), or lazily: move zespan.init() into a module
    imported before src/openai.ts, or construct the OpenAI client
    lazily inside the handler that uses it instead of at module scope.
    (no zespan.init( call was found on an earlier line in this file.)
```

## Sample output

This is real output from `zespan doctor` run with no configured API key
(`ZESPAN_API_KEY` unset), showing the short-circuit behavior described below:

```
Configuration
  ✗ API key                  Key rejected: Request to api.zespan.com failed with HTTP 401: {"error":"Not authenticated"}
    → Double-check ZESPAN_API_KEY (or --api-key) against the key shown in Settings -> API Keys for this project.
    → If the key was recently rotated or revoked, generate a new one and update your environment.
  − Reachable                Skipped: "API key" failed above.
  − Environment              Skipped: "API key" failed above.

SDK
  − Sdk version               Skipped: "API key" failed above.
  − Wrapper order              Skipped: "API key" failed above.

Data flow
  − Data flow                 Skipped: "API key" failed above.

Policy & redaction
  − Redaction                 Skipped: "API key" failed above.

1 error, 0 warnings.
```

## Behaviors worth knowing

<Steps>
  <Step title="Short-circuit on a bad key, not a cascade">
    If the API key check itself reports `fail` (the key was flatly rejected —
    a 401, not a network problem), every later check is marked `skip` without
    ever running. Six confusing failures caused by one bad key is a worse
    diagnostic experience than one clear failure, so `doctor` stops there.
    This only triggers on `apiKeyValid` specifically failing — a network
    failure (`skip`, not `fail`) does not short-circuit; `API reachability`
    still runs independently and gives you a second data point.
  </Step>

  <Step title="A check never crashes doctor">
    If a check throws for any reason, `doctor` catches it and reports that
    check as `fail` carrying the thrown message, instead of the whole command
    crashing. `doctor` is the tool people reach for when something is already
    broken — it has to stay usable when things go wrong in unexpected ways.
  </Step>

  <Step title="Warnings never affect the exit code">
    Only a `fail` anywhere in the results makes `doctor` exit `1`. A `warn` —
    a stale SDK version, redaction disabled — is real, actionable advice, but
    it is not a broken setup, and a CI job gating on `doctor`'s exit code must
    not be blocked by advice it can't act on immediately.
  </Step>
</Steps>

| Exit code | Meaning                                                 |
| --------- | ------------------------------------------------------- |
| `0`       | No check reported `fail` (there may still be warnings). |
| `1`       | At least one check reported `fail`.                     |
| `2`       | Usage error — an unrecognized command.                  |

## `--json` output

`zespan doctor --json` prints exactly one JSON line — `{ results, summary }`
— and suppresses the grouped-text renderer, for CI consumption.

```bash theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
zespan doctor --json
```

Real output from the same unauthenticated run shown above:

```json theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
{"results":[{"id":"apiKeyValid","group":"Configuration","status":"fail","title":"API key","detail":"Key rejected: Request to api.zespan.com failed with HTTP 401: {\"error\":\"Not authenticated\"}","fixes":["Double-check ZESPAN_API_KEY (or --api-key) against the key shown in Settings -> API Keys for this project.","If the key was recently rotated or revoked, generate a new one and update your environment."]},{"id":"reachable","group":"Configuration","status":"skip","title":"Reachable","detail":"Skipped: \"API key\" failed above."},{"id":"environment","group":"Configuration","status":"skip","title":"Environment","detail":"Skipped: \"API key\" failed above."},{"id":"sdk-version","group":"SDK","status":"skip","title":"Sdk version","detail":"Skipped: \"API key\" failed above."},{"id":"data-flow","group":"Data flow","status":"skip","title":"Data flow","detail":"Skipped: \"API key\" failed above."},{"id":"redaction","group":"Policy & redaction","status":"skip","title":"Redaction","detail":"Skipped: \"API key\" failed above."},{"id":"wrapper-order","group":"SDK","status":"skip","title":"Wrapper order","detail":"Skipped: \"API key\" failed above."}],"summary":{"errors":1,"warnings":0,"passed":0,"skipped":6}}
```

| Field              | Type       | Notes                                                            |
| ------------------ | ---------- | ---------------------------------------------------------------- |
| `results`          | array      | One entry per check, in the fixed run order.                     |
| `results[].id`     | string     | Stable check id, e.g. `apiKeyValid`, `data-flow`.                |
| `results[].group`  | string     | `Configuration` \| `SDK` \| `Data flow` \| `Policy & redaction`. |
| `results[].status` | string     | `pass` \| `warn` \| `fail` \| `skip`.                            |
| `results[].title`  | string     | Human-readable check name.                                       |
| `results[].detail` | string?    | Human-readable explanation.                                      |
| `results[].fixes`  | string\[]? | Actionable next steps. Always present on `fail`/`warn`.          |
| `summary.errors`   | number     | Count of `fail` results.                                         |
| `summary.warnings` | number     | Count of `warn` results.                                         |
| `summary.passed`   | number     | Count of `pass` results.                                         |
| `summary.skipped`  | number     | Count of `skip` results.                                         |

## Flags

<ParamField query="--api-key" type="string">
  Your Zespan API key. Falls back to `ZESPAN_API_KEY`. Never read from `.zespan.yaml`.
</ParamField>

<ParamField query="--project" type="string">
  Project id, if it differs from the one your API key resolves to. Falls back to `ZESPAN_PROJECT_ID` or `.zespan.yaml`'s `project`.
</ParamField>

<ParamField query="--env" type="string">
  Environment name for the Environment check. Falls back to `ZESPAN_ENVIRONMENT` or `.zespan.yaml`'s `environment`.
</ParamField>

<ParamField query="--api-url" type="string" default="https://api.zespan.com/v1">
  Override the API base URL. Falls back to `ZESPAN_API_URL`. Never read from `.zespan.yaml` (same treatment as `--api-key` — see [CLI overview](/cli/overview)). Needed for a self-hosted deployment.
</ParamField>

<ParamField query="--json" type="boolean">
  Print `{ results, summary }` as a single JSON line instead of grouped text, and disable colour output.
</ParamField>

<ParamField query="--help" type="boolean">
  Print this command's usage and exit — does not run any checks or make any network calls.
</ParamField>

See [CLI overview](/cli/overview) for the full flag/env/file precedence rules.

## Next steps

<CardGroup cols={2}>
  <Card title="CLI overview" icon="terminal" href="/cli/overview">
    Installation, `.zespan.yaml`, and environment variables.
  </Card>

  <Card title="Troubleshooting" icon="life-buoy" href="/troubleshooting">
    Symptom-first fixes for missing traces, broken span trees, and \$0.00 cost.
  </Card>

  <Card title="PII redaction" icon="shield" href="/sdk/pii-redaction">
    Configure `redactPii` and choose a preset.
  </Card>

  <Card title="API keys" icon="key" href="/account/api-keys">
    Create and rotate the key `doctor` authenticates with.
  </Card>

  <Card title="zespan auth" icon="log-in" href="/cli/auth">
    A separate credential, for the commands that act as a person rather than a project.
  </Card>
</CardGroup>
