> ## Documentation Index
> Fetch the complete documentation index at: https://learn.kubit.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Send traces from Node

> Send OpenTelemetry spans from a Node or TypeScript agent to Kubit with @kubit-ai/otel, on its own or next to another OpenTelemetry SDK.

`@kubit-ai/otel` sends OpenTelemetry spans from a Node or TypeScript app to Kubit. It has two entry points: `configure()` when Kubit is your only OpenTelemetry setup, and `KubitSpanProcessor` when your app already creates a tracer provider or a `NodeSDK`. This page covers both, the span filter, and the settings to check before production.

## Before you start

* [ ] Node.js 18 or later
* [ ] OpenTelemetry JS SDK v2. Version 1 isn't supported.
* [ ] A Kubit API key in `KUBIT_API_KEY`

Kubit authenticates spans with an ingestion API key, read from the `KUBIT_API_KEY` environment variable. Each key belongs to one workspace.

| Way to get a key | How |
| - | - |
| **Coding agent** | Run `/kubit-integrate` in Claude Code. It creates or selects a workspace, mints a key and writes `KUBIT_API_KEY` to `.env.local` or `.env`. See [Set up with your coding agent](https://learn.kubit.ai/agent-observability/get-started/coding-agent.md). |
| **Kubit app** | **TBD** |

> [!WARNING]
>
> Keep the key out of source control. Put it in `.env` or your secrets manager, and check that `.env` is in `.gitignore`.

## Prompt for your coding agent

```text theme={null}
Add Kubit tracing to this Node repo. Follow https://learn.kubit.ai/agent-observability/send-traces/node.md exactly.
If the repo already creates a NodeTracerProvider or NodeSDK, add KubitSpanProcessor to its spanProcessors. Otherwise use configure().
Read KUBIT_API_KEY from .env. Show me the diff before writing any file.
```

## Install

Install the package and its OpenTelemetry peer dependencies:

```bash theme={null}
npm install @kubit-ai/otel \
  @opentelemetry/api \
  @opentelemetry/exporter-trace-otlp-proto \
  @opentelemetry/resources@^2 \
  @opentelemetry/sdk-trace-base@^2 \
  @opentelemetry/sdk-trace-node@^2
```

| Peer dependency | Minimum version |
| - | - |
| `@opentelemetry/api` | `1.7.0` |
| `@opentelemetry/exporter-trace-otlp-proto` | `0.200.0` |
| `@opentelemetry/resources` | `2.0.0` |
| `@opentelemetry/sdk-trace-base` | `2.0.0` |
| `@opentelemetry/sdk-trace-node` | `2.0.0` |

## Choose configure() or KubitSpanProcessor

| Your app today | Use | Why |
| - | - | - |
| No OpenTelemetry setup | `configure()` | Creates and registers a tracer provider with Kubit's processor in one call |
| Creates its own `NodeTracerProvider` or `NodeSDK` | `KubitSpanProcessor` | Adds Kubit next to your existing processors on the one provider |
| Sends traces to Langfuse, Braintrust or another OpenTelemetry-based SDK | `KubitSpanProcessor` | Same as above. [Set up with your coding agent](https://learn.kubit.ai/agent-observability/get-started/coding-agent.md) handles the sink-specific wiring. |

`@kubit-ai/otel` has no `attach()`, unlike the Python SDK. OpenTelemetry JS SDK v2 takes span processors only when the provider is created, so there's no way to add one to a provider that's already running.

> [!WARNING]
>
> Don't call `configure()` if your app already sets up a tracer provider or `NodeSDK`. `configure()` registers a second global provider and replaces the first, so your existing traces stop reaching their current destination.

## You have no OpenTelemetry setup

Create `kubit-instrumentation.ts` and import it first in your entrypoint, before any LLM client is created:

```typescript kubit-instrumentation.ts theme={null}
import { configure } from "@kubit-ai/otel";

const apiKey = process.env.KUBIT_API_KEY;
if (!apiKey) throw new Error("KUBIT_API_KEY is not set");

export const tracerProvider = configure({
  apiKey,
  serviceName: "support-agent",
  serviceVersion: "1.4.0",
  resourceAttributes: { "deployment.environment": "production" },
});
```

```typescript index.ts theme={null}
import "./kubit-instrumentation"; // first import, before any LLM client is created
```

Change `serviceName`, `serviceVersion` and `deployment.environment` to match your app. Kubit uses them to tell agents and releases apart.

### configure() options

| Option | Required | Default | Notes |
| - | - | - | - |
| **apiKey** | **Yes** | | Your Kubit API key. The SDK doesn't read it from the environment, so pass it in. |
| **serviceName** | Optional | `default` | Becomes the OpenTelemetry `service.name`. Set it. Kubit uses it to tell agents apart. |
| **serviceVersion** | Optional | Not set | Becomes `service.version` |
| **resourceAttributes** | Optional | `{}` | Extra resource attributes, such as `deployment.environment` |
| **endpoint** | Optional | `https://otel.kubit.ai/v1/traces` | Overrides the trace endpoint. The `KUBIT_OTEL_ENDPOINT` environment variable does the same. |
| **shouldExportSpan** | Optional | LLM spans only | Decides which spans go to Kubit. See [Span filtering](#span-filtering). |

`configure()` returns the tracer provider it registered.

## You already have a tracer provider

Pass `KubitSpanProcessor` in `spanProcessors` next to your other processors, when you create the provider.

### NodeTracerProvider

```typescript tracing.ts theme={null}
import { KubitSpanProcessor } from "@kubit-ai/otel";
import { resourceFromAttributes } from "@opentelemetry/resources";
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";

const apiKey = process.env.KUBIT_API_KEY;
if (!apiKey) throw new Error("KUBIT_API_KEY is not set");

const provider = new NodeTracerProvider({
  resource: resourceFromAttributes({ "service.name": "support-agent" }),
  spanProcessors: [
    // your existing processors stay here
    new KubitSpanProcessor({ apiKey }),
  ],
});
provider.register();
```

### NodeSDK

```typescript instrumentation.ts theme={null}
import { KubitSpanProcessor } from "@kubit-ai/otel";
import { NodeSDK } from "@opentelemetry/sdk-node";

const apiKey = process.env.KUBIT_API_KEY;
if (!apiKey) throw new Error("KUBIT_API_KEY is not set");

export const sdk = new NodeSDK({
  serviceName: "support-agent",
  spanProcessors: [
    // your existing processors stay here
    new KubitSpanProcessor({ apiKey }),
  ],
});
sdk.start();
```

### KubitSpanProcessor options

`KubitSpanProcessor` is an OpenTelemetry `BatchSpanProcessor` that drops non-LLM spans before they're queued.

| Option | Required | Default | Notes |
| - | - | - | - |
| **apiKey** | **Yes** | | Your Kubit API key, sent in the `x-api-key` header |
| **endpoint** | Optional | `https://otel.kubit.ai/v1/traces` | Same resolution as `configure()` |
| **shouldExportSpan** | Optional | LLM spans only | See [Span filtering](#span-filtering) |
| **maxQueueSize** | Optional | `2048` | Spans held before new ones are dropped |
| **scheduledDelayMillis** | Optional | `5000` | Time between export batches, in milliseconds |
| **maxExportBatchSize** | Optional | `512` | Spans per export request |
| **exportTimeoutMillis** | Optional | `30000` | Timeout for each export, in milliseconds |

For full control, `KubitExporter` is the plain exporter without filtering or batching. Wrap it in your own processor.

## Span filtering

By default, `KubitSpanProcessor` sends only LLM-related spans to Kubit. HTTP, database and framework spans from other auto-instrumentation stay out of your workspace.

A span is sent when any of these is true:

| Rule | Example |
| - | - |
| It was created by the Kubit SDK tracer, `kubit-sdk` | Spans from Kubit's own helpers |
| It has a `gen_ai.*` attribute | `gen_ai.request.model`, `gen_ai.tool.name` |
| Its instrumentation scope starts with a known LLM prefix | `ai` (Vercel AI SDK), `@arizeai/openinference`, `langfuse-sdk`, `langsmith`, `braintrust` |

`KNOWN_LLM_INSTRUMENTATION_SCOPE_PREFIXES` exports the full prefix list.

### Keep spans from your own framework

```typescript tracing.ts theme={null}
import { KubitSpanProcessor, getInstrumentationScopeName, isDefaultExportSpan } from "@kubit-ai/otel";

const apiKey = process.env.KUBIT_API_KEY;
if (!apiKey) throw new Error("KUBIT_API_KEY is not set");

export const kubitProcessor = new KubitSpanProcessor({
  apiKey,
  shouldExportSpan: ({ otelSpan }) =>
    isDefaultExportSpan(otelSpan) ||
    (getInstrumentationScopeName(otelSpan)?.startsWith("my-framework") ?? false),
});
```

### Send every span

```typescript tracing.ts theme={null}
import { KubitSpanProcessor } from "@kubit-ai/otel";

const apiKey = process.env.KUBIT_API_KEY;
if (!apiKey) throw new Error("KUBIT_API_KEY is not set");

export const kubitProcessor = new KubitSpanProcessor({ apiKey, shouldExportSpan: () => true });
```

If `shouldExportSpan` throws, the span is dropped.

## Trace LLM and tool calls

Kubit receives whatever spans your app produces. Spans from an LLM instrumentation library pass the default filter. With the Vercel AI SDK, turn on telemetry per call:

```typescript chat.ts theme={null}
import "./kubit-instrumentation";
import { openai } from "@ai-sdk/openai";
import { generateText } from "ai";

const { text } = await generateText({
  model: openai("gpt-4o"),
  prompt: "Where is my order?",
  experimental_telemetry: { isEnabled: true },
});
console.log(text);
```

For tool calls, wrap each tool in its own span so it shows up as a step in the execution path:

```typescript tools.ts theme={null}
import { trace } from "@opentelemetry/api";

const tracer = trace.getTracer("support-agent");

export async function searchOrders(customerId: string, query: string): Promise<unknown[]> {
  return tracer.startActiveSpan("execute_tool search_orders", async (span) => {
    span.setAttribute("gen_ai.operation.name", "execute_tool");
    span.setAttribute("gen_ai.tool.name", "search_orders");
    const results = await runOrderSearch(customerId, query); // your existing tool code
    span.setAttribute("search.result_count", results.length);
    span.end();
    return results;
  });
}
```

[Span attributes](https://learn.kubit.ai/agent-observability/send-traces/span-attributes.md) lists the keys Kubit reads, including user and session IDs.

## Before production

| Check | Why |
| - | - |
| `serviceVersion` set to your release | Triggers compare metrics across versions |
| `deployment.environment` set | Keeps dev and production traces apart |
| Short-lived scripts and serverless handlers flush before exit | Spans wait up to 5 seconds in the batch. They're lost if the process exits first. Call `forceFlush()` on the provider, or `shutdown()` on `NodeSDK`. |
| `KUBIT_OTEL_ENDPOINT` set only on purpose | It redirects every Kubit export from the process |
| User and session IDs on the root span | Without them Kubit can't group traces into sessions or join them to users |
| Sensitive fields masked before export | Message content goes to Kubit for labeling |

`@kubit-ai/otel` doesn't read the standard `OTEL_EXPORTER_OTLP_ENDPOINT` or `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` variables. They apply to the whole process and would redirect Kubit's traces if another OpenTelemetry SDK set them. Use `KUBIT_OTEL_ENDPOINT` instead.

## Verify

Send a test span with debug logging on:

```typescript send-test-span.ts theme={null}
import { configure } from "@kubit-ai/otel";
import { trace } from "@opentelemetry/api";
import type { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";

const apiKey = process.env.KUBIT_API_KEY;
if (!apiKey) throw new Error("KUBIT_API_KEY is not set");

const provider = configure({ apiKey, serviceName: "support-agent" }) as NodeTracerProvider;

trace.getTracer("support-agent").startActiveSpan("chat.completion", (span) => {
  span.setAttribute("gen_ai.request.model", "gpt-4o");
  span.setAttribute("gen_ai.usage.input_tokens", 10);
  span.setAttribute("gen_ai.usage.output_tokens", 5);
  span.end();
});

await provider.forceFlush();
```

```bash theme={null}
KUBIT_OTEL_LOG_LEVEL=debug npx tsx send-test-span.ts
```

The output includes these lines, among other debug lines:

```text Output theme={null}
[kubit-otel info] kubit_otel configured  service_name=support-agent service_version=- endpoint=<redacted>
[kubit-otel debug] KubitSpanProcessor forceFlush
```

No `Dropped span due to shouldExportSpan filter` line for `chat.completion` means the span passed the filter.

## If it fails

| Symptom | Cause | Fix |
| - | - | - |
| `Error: KUBIT_API_KEY is not set` | The env var isn't set in the shell that runs Node, or `.env` isn't loaded yet | Export it, or load `.env` (for example with `import "dotenv/config"`) before `kubit-instrumentation` |
| `[kubit-otel debug] Dropped span due to shouldExportSpan filter  span_name=...` | The span has no `gen_ai.*` attribute and isn't from a known LLM scope | Add `gen_ai.*` attributes, or extend `shouldExportSpan` as in [Span filtering](#span-filtering) |
| Your existing traces stop arriving after you add Kubit | `configure()` registered a second provider | Remove `configure()` and add `KubitSpanProcessor` to your existing provider |
| Spans from a short script never show up | The process exited before the 5-second batch was exported | Call `forceFlush()` on the provider before exit |
| The app is on OpenTelemetry JS SDK v1 | `configure()` uses v2-only APIs (`resourceFromAttributes()` and `spanProcessors` at construction) | Upgrade the OpenTelemetry packages to v2. |
| Bad or revoked API key | | |

## Next steps

* [Span attributes](https://learn.kubit.ai/agent-observability/send-traces/span-attributes.md): the attribute keys Kubit reads.
* [Set up with your coding agent](https://learn.kubit.ai/agent-observability/get-started/coding-agent.md): keep Langfuse or Braintrust as your sink and add Kubit next to it.
* [Is my data flowing?](https://learn.kubit.ai/agent-observability/get-started/data-flowing.md): step-by-step checks.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.