OpenTelemetry
Breadcrumb is an OpenTelemetry consumer, so it fits an app that already has tracing: register its span processor on your provider, or send spans over HTTP from a service running somewhere else.
Your own tracer provider
If you already set up OpenTelemetry — @vercel/otel, a NodeSDK, Sentry —
register bc.spanProcessor and model spans reach Breadcrumb from the tracer you
already have:
// instrumentation.ts
import { registerOTel } from "@vercel/otel";
import { bc } from "./breadcrumb";
export function register() {
registerOTel({ serviceName: "app", spanProcessors: [bc.spanProcessor] });
}
Calls then need no Breadcrumb-specific wiring — the AI SDK’s own telemetry flag
is enough, and functionId still names the call and carries its cost:
const { text } = await generateText({
model: openai("gpt-5"),
prompt,
experimental_telemetry: { isEnabled: true, functionId: "generate-answer" },
});
bc.telemetry() keeps working alongside it and pins Breadcrumb’s own tracer, so
a span goes to one provider or the other, never both — no duplicate rows. Context
propagation is global either way, so a call made inside a bc.trace() callback
still nests into that trace.
What gets stored
On a shared provider the processor sees every span in the app, including HTTP,
database, and filesystem instrumentation. By default it keeps only the spans
Breadcrumb can read — those carrying ai.*, gen_ai.*, or breadcrumb.*
attributes — so registering it doesn’t turn your trace table into a general span
dump. shouldExport replaces that rule when you want your own spans stored too:
breadcrumb({
// …
shouldExport: (span) => span.instrumentationScope.name !== "@opentelemetry/instrumentation-fs",
});
Whatever you filter, the spans that survive still read correctly: a call keeps
its functionId name and cost attribution wherever it sits, and a trace missing
the span above it renders as a partial trace rather than falling apart.
Ingesting over HTTP
For services outside your main process, Breadcrumb accepts traces over HTTP. Enable the ingest endpoints with an API key, then point any OTLP/HTTP exporter at your mount path.
breadcrumb({
// …
ingest: { apiKey: process.env.BREADCRUMB_KEY },
});
Point the exporter at basePath/api/ingest/otel and send the key as a header:
OTEL_EXPORTER_OTLP_ENDPOINT=https://yourapp.com/api/breadcrumb/api/ingest/otel \
OTEL_EXPORTER_OTLP_HEADERS="x-breadcrumb-key=$BREADCRUMB_KEY"
Sending spans as JSON
There’s also a plain JSON endpoint at basePath/api/ingest/spans for sending
span records directly, without an OpenTelemetry SDK:
curl -X POST https://yourapp.com/api/breadcrumb/api/ingest/spans \
-H "x-breadcrumb-key: $BREADCRUMB_KEY" \
-H "content-type: application/json" \
-d '{ "spans": [{ "traceId": "abc", "name": "external-call", "kind": "llm" }] }'
NOTE
Ingest routes authenticate with the API key and sit outside your dashboard
authorize guard. They stay closed until you set ingest.apiKey.
Partial traces
A trace can reach Breadcrumb without the span at its top: another tracer owns
that one, it was sampled away, or the service holding it never exports here.
Those runs still read correctly — the earliest surviving span names the run, and
every disconnected subtree is rendered rather than the first one only. Naming and
cost attribution follow the call’s functionId, not its position in the tree, so
a call keeps its identity under someone else’s root span.
Next steps
- Production: secure your keys and set retention.
- Cost & tokens: price the traces you ingest.