Troubleshooting
Nothing arrives, the wrong environment, missing bodies, missing cost and other common causes.
On this page
Work down the page. The order matches how often each cause turns out to be the real one.
Nothing arrives at all
Is the key reaching the process?
node -e "console.log(Boolean(process.env.TRACEHATCH_API_KEY))"Writing an .env file does not load it. Next.js loads .env.local itself;
other Node apps need their own loader or Node.js 22+ --env-file. The SDK does
fall back to .env.local or .env beside the running package when the process
environment has none — but only for its own TRACEHATCH_* settings.
Without a key the SDK stays disabled and sends nothing, by design.
flushWithResult() returns disabled, which is the fastest way to confirm it.
Does the connection work?
npx --yes @tracehatch/cli@0.3.0 doctorIf the diagnostic itself fails, the problem is the key, the API address or the
network — not your instrumentation. A key that was revoked answers 401 key_revoked. For a self-hosted installation, check TRACEHATCH_BASE_URL
points at the API origin, not the dashboard.
Did the server restart?
The SDK is loaded at startup. An already-running process will not pick it up.
The connection works, but no application calls
This is the most common outcome, and it has one dominant cause.
The SDK loaded after your provider client
Provider clients capture fetch when they are constructed. A client created
at module scope, before the SDK loaded, keeps the original fetch forever.
- Use the preload flag —
node --import @tracehatch/sdk/auto server.js— so the SDK runs before any of your imports. - Or make
import "@tracehatch/sdk/auto"the genuinely first import of the entry file, above every other import. - In Next.js, use
instrumentation.tswith theNEXT_RUNTIME === "nodejs"guard.
A custom production start script
init edits the start/dev script it found. A start:prod, a Procfile, a
Dockerfile CMD or a process manager that bypasses it records nothing. Add
--import @tracehatch/sdk/auto there too, or set
NODE_OPTIONS="--import @tracehatch/sdk/auto".
The client was given its own fetch
A client constructed with a custom fetch option is not captured. Remove the
option, or record the call with a
manual generation span.
The request shape is not one Tracehatch recognises
Only the documented request shapes become spans. Audio uploads and other non-JSON bodies are not captured, and a provider outside the supported set needs a manual span or the log endpoint.
The wrong runtime
The Node SDK does not run in the Edge runtime, in browsers, or in Deno. Use the gateway there.
Runs appear in the wrong place
The key selects the project and the environment. If runs are landing in
development when you expected production, the server has a th_test_ key.
Check which key that deployment actually has — not which one you meant to give
it.
Filtering also matters: the environment and time range in the header apply to the dashboard, Traces and every Analyze page, and your choice is saved per user and project.
Runs appear but something is missing
| Missing | Why |
|---|---|
| Prompts and outputs | TRACEHATCH_CAPTURE_BODIES=false, or the installer's default. Timings and usage remain. |
| Cost | The model has no price coverage. Usage is still counted. |
| Token usage on a stream | The provider did not report it. OpenAI chat streams get include_usage added unless you set it. |
| Time to first token | Only recorded for streams. |
| Tool spans | The tool result never came back in a later request, or you ran the tool outside the loop. Time it with tool(). |
| Span detail on some runs | Above the monthly allowance, non-failed traces are sampled: counters and metrics stay, detail does not. |
| Older runs entirely | Retention. Free keeps 7 days, Pro 90. |
| A value you expected masked | Redaction is heuristic. When prompts must be excluded, exclude them. |
A tool is recorded twice
You timed a provider-requested tool with tool() without linking it to the
provider's call. Pass the call id so the explicit span replaces the inferred
one:
await tool("search docs", () => search(args), {
attributes: { "tool.call_id": call.id },
})A stream never finishes
Inside an explicit trace(), keep the callback open until the stream is
consumed or cancelled. The run's end waits for the stream, bounded by two
minutes; past that an unread stream is recorded as cancelled.
Serverless: the last run is missing
The process can be frozen the moment the handler returns. await flush() before
returning on AWS Lambda, Netlify and similar; use after(() => flush()) in
Next.js route handlers. Vercel functions are handled for you through
waitUntil.
Do not call shutdown() per request — it removes the capture hooks, so
everything after it goes unrecorded.
Sessions are wrong or split
Inference is a fallback. If you already have a conversation id, send it:
x-tracehatch-session-id: conversation-42or call setSession().
Derived ids come from the system prompt and the first user message, so a prompt
that changes between turns will split a conversation.
Monorepos
Install and configure in the package that makes model calls, not the repo root.
tracehatch init --cwd apps/api picks it directly; --yes never guesses when
several candidates match.
Errors you may see
| Code | Meaning |
|---|---|
unauthenticated | Missing or malformed credential. |
key_revoked | The ingest key was revoked. Create a new one. |
feature_not_available | The workspace's plan does not include it — not a permission problem. |
plan_limit_reached | A plan limit, such as the Free project count. |
quota_exceeded | Three times the monthly allowance. Honour Retry-After; resets at month end. |
sdk_upgrade_required | The SDK is older than the API's minimum wire version. Upgrade the package. |
validation_failed | errors[] names the exact paths. |
Full list and the problem shape: HTTP API.
Still stuck
Turn on the SDK's own diagnostics and read what it decided:
import { init, flushWithResult } from "@tracehatch/sdk"
const client = init({ debug: true })
console.log(client.stats())
console.log(await flushWithResult())stats() reports queued and dropped spans; flushWithResult() distinguishes
disabled, empty, accepted, timeout and failed. Between them they
separate "misconfigured" from "nothing has happened yet" from "sent and
rejected". See Delivery and flushing.