Roll your own LLM tracing in minutes
LLM tracing for TypeScript apps that installs like a library, not a service. Traces land in the database you already run, and the dashboard is a component you mount in your own app.
I'm adding Breadcrumb to my TypeScript app: embeddable LLM tracing that stores traces in a database I own and renders its dashboard as a React component inside my own app. Docs: - Introduction: https://breadcrumb.sh/docs/ - Quickstart: https://breadcrumb.sh/docs/quickstart/ - Mounting the handler: https://breadcrumb.sh/docs/frameworks/ - Mounting the dashboard: https://breadcrumb.sh/docs/dashboard/ - Database & adapters: https://breadcrumb.sh/docs/database/ - Instrumenting: https://breadcrumb.sh/docs/instrumenting/ - Custom UI (client, React hooks, kit): https://breadcrumb.sh/docs/react/ - MCP for coding agents: https://breadcrumb.sh/docs/mcp/ - Production: https://breadcrumb.sh/docs/production/ Read those, then set it up: 1. Work out my stack from the codebase: framework, database, and how I call models. 2. Install @breadcrumb-sh/core and @breadcrumb-sh/react plus the driver for my database. 3. Create one shared bc instance with the matching adapter and a basePath. 4. Mount bc.handler at that path, behind an authorize check. That serves the JSON API, not a UI. 5. Render <BreadcrumbDashboard /> on a page route I own, pointed at that basePath, and import "@breadcrumb-sh/react/styles.css" once above it. Guard that route with my app's existing auth. 6. Instrument my existing LLM calls and confirm a trace lands. 7. Open the dashboard's MCP tab, create a key, and connect yourself to it, so you can query these traces directly when I ask you to debug something. Good to know: - The API and the dashboard page are two separate routes. The handler serves no HTML. - The dashboard fills its container, so give it one with a height. - The schema is created automatically in development. For production, generate migration files with "npx breadcrumb generate", commit them, and set migrations: "manual". - On serverless, await bc.flush() before the response returns so no spans are lost.Setup
import { breadcrumb } from "@breadcrumb-sh/core";
import { postgres } from "@breadcrumb-sh/core/adapters";
export const bc = breadcrumb({
database: postgres(process.env.DATABASE_URL!),
basePath: "/api/breadcrumb",
mcp: { hidePayloads: true }, // your agent reads traces, not prompts
});- Integrate in your stack. One instance, pointed at the database you already run, mounted in your app behind your own auth.
- Instrument. Pass
bc.telemetry()to the AI SDK, and wrap everything around it inbc.trace(). - View and debug with MCP. Read the dashboard yourself, or hand your coding agent a key and let it query the traces.
Instrumenting a request
Model calls instrument themselves. Pass bc.telemetry() where the Vercel AI SDK takes experimental_telemetry and the model, provider, token counts, cost and payloads are read from the attributes the SDK already emits.
const { text } = await streamText({
model: openai("gpt-5"),
prompt,
experimental_telemetry: bc.telemetry({
functionId: "generate-answer",
userId,
sessionId,
}),
});Wrap the work around them in bc.trace() to get the retrieval, the tool calls and the database write on the same timeline.
await bc.trace("support-reply", async (t) => {
const docs = await loadContext(question);
const answer = await generate(docs);
await db.insert(replies).values(answer);
});Storage and querying
Spans land in one table, breadcrumb_spans, in the database your app already writes to. Not an export, not a webhook, not a read-only API over someone else's schema, so your own analytics are ordinary queries with the ORM you already use.
// after `prisma db pull`, the table is a model like any other
const spend = await prisma.breadcrumb_spans.groupBy({
by: ["model"],
where: { user_id: customer.id, status: "ok" },
_sum: { cost: true },
});Cost per customer on their billing page. Failures per tenant. Whether the accounts that churned were the ones hitting the slow path. Those need the traces and your users table in the same query, which is the one thing a hosted tool can never do.
Retention is a window you set per environment, swept as traces arrive. Nothing is metered per event, so you never have to sample or drop payloads to keep a bill down.
Build a custom UI for your tracing
A hosted dashboard has to stay generic, because it serves everyone's app. Yours only has to serve yours, and because the dashboard is a component, your panel ships as a page inside it:
Mount the built-in dashboard on day one, then add your own pages to it. When it stops fitting entirely, the same data is there as React hooks and typed queries, with a headless kit for span tree assembly, self time and payload parsing.
import { BreadcrumbDashboard, useSessions, useTrace } from "@breadcrumb-sh/react";
import { flowRows } from "@breadcrumb-sh/core/kit";
function ReviewQueue() {
const [selected, setSelected] = useState<string | null>(null);
const { data: queue } = useSessions({ status: "error" });
const { data: spans = [] } = useTrace(selected);
const rows = flowRows(spans);
// Your layout, your domain: the reply beside the docs it cited,
// the reviewer's verdict, the retry that finally worked.
}
// Ships as a page in the dashboard, beside the ones that came in the box.
<BreadcrumbDashboard
pages={[{ name: "review", label: "Review", element: <ReviewQueue /> }]}
/>;Documentation
- Quickstart: install, mount, capture a first trace.
- Frameworks: Next.js, Hono, or plain Node.
- Dashboard: mount the UI, add your own pages.
- Instrumenting: the AI SDK, OpenTelemetry, manual spans.
- React: hooks and the headless kit for your own panel.
- Querying: the typed API over your own traces.
- Production: auth, retention, cost tables, going live.