breadcrumb

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.

or read the quickstart

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
});
  1. Integrate in your stack. One instance, pointed at the database you already run, mounted in your app behind your own auth.
  2. Instrument. Pass bc.telemetry() to the AI SDK, and wrap everything around it in bc.trace().
  3. 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);
});
kind span dur 0 4.81s
agent support-reply 4.81s ────────────────────────────────────────
retrieval load-context 210ms ██
span generate 3.74s ───────────────────────────────
llm streamText.doStream 1.64s ██████████████
tool lookup-order 240ms ██
llm streamText.doStream 1.82s ███████████████
tool persist-reply 782ms ███████
█ self time ─ waiting on children ✕ failed

Storage and querying

your app ──▶ breadcrumb ──▶ your database ──▶ your dashboard bc.trace() normalize breadcrumb_spans <Dashboard />

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:

┌ review queue ──────────────┬ trace 8f21c4 ─────────────────┐ │ ● refund #4192 $0.004 │ user where is my order? │ │ ○ refund #4193 $0.002 │ tool lookup-order 240ms │ │ ○ signup #877 $0.001 │ model gpt-5 1.64s │ │ │ reply it shipped friday │ │ 3 flagged today │ [ approve ] [ reject ] │ └────────────────────────────┴───────────────────────────────┘

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