Skip to content

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?

Prove it from the same process that runs your serverTerminal
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?

Terminal
npx --yes @tracehatch/cli@0.3.0 doctor

If 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.ts with the NEXT_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

MissingWhy
Prompts and outputsTRACEHATCH_CAPTURE_BODIES=false, or the installer's default. Timings and usage remain.
CostThe model has no price coverage. Usage is still counted.
Token usage on a streamThe provider did not report it. OpenAI chat streams get include_usage added unless you set it.
Time to first tokenOnly recorded for streams.
Tool spansThe 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 runsAbove the monthly allowance, non-failed traces are sampled: counters and metrics stay, detail does not.
Older runs entirelyRetention. Free keeps 7 days, Pro 90.
A value you expected maskedRedaction 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:

One execution, one spanTypeScript
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:

Let Tracehatch use the id you already haveHTTP
x-tracehatch-session-id: conversation-42

or 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

CodeMeaning
unauthenticatedMissing or malformed credential.
key_revokedThe ingest key was revoked. Create a new one.
feature_not_availableThe workspace's plan does not include it — not a permission problem.
plan_limit_reachedA plan limit, such as the Free project count.
quota_exceededThree times the monthly allowance. Honour Retry-After; resets at month end.
sdk_upgrade_requiredThe SDK is older than the API's minimum wire version. Upgrade the package.
validation_failederrors[] 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:

Ask the SDK what it thinks is happeningTypeScript
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.