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