# SDKs and MCP (/docs/sdks)



obs-unified ships first-party SDKs for **three backend languages** plus a browser SDK and an MCP server for AI agents, all sharing one identity-propagation contract.

For a compact method-by-method cheat sheet, see [SDK API reference](/docs/sdk-reference).

For the agent-facing investigation surface, see [MCP server](/docs/mcp-server).

## Backend SDKs [#backend-sdks]

| Language                               | Install                                                          | Registry                                                                          | Where it runs                                                                                               |
| -------------------------------------- | ---------------------------------------------------------------- | --------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| **TypeScript (Fetch / Edge / Node)**   | `pnpm add @obsunified/telemetry-sdk`                             | [npm](https://www.npmjs.com/package/@obsunified/telemetry-sdk)                    | Cloudflare Workers · Hono · Next.js · Bun · Deno · Node.js                                                  |
| **TypeScript (Node OTel SDK wrapper)** | `pnpm add @obsunified/sdk`                                       | [npm](https://www.npmjs.com/package/@obsunified/sdk)                              | Node.js 20+ with `@opentelemetry/sdk-node` auto-instrumentation (Express, Fastify, NestJS, `pg`)            |
| **Go**                                 | `go get github.com/dev-sawan-ruparel/obs-unified/sdks/go@latest` | [pkg.go.dev](https://pkg.go.dev/github.com/dev-sawan-ruparel/obs-unified/sdks/go) | Any Go 1.22+ binary (no `vcs.commit.id` / `obs.config.digest` or feature-flag cohorts yet)                  |
| **Rust**                               | `cargo add obs-unified`                                          | [crates.io](https://crates.io/crates/obs-unified)                                 | Any Rust 1.75+ binary (Tokio runtime; no `vcs.commit.id` / `obs.config.digest` or feature-flag cohorts yet) |

**Which TypeScript backend package should I choose?**

* Choose &#x2A;*`@obsunified/telemetry-sdk`** for **Cloudflare Workers, Hono, Next.js, Bun, Deno, or lightweight Node.js** services where you want zero-dependency fetch-based OTLP export, structured logging, AI call tracking, and runtime lineage in one package.
* Choose &#x2A;*`@obsunified/sdk`*&#x2A; when your Node.js service already uses **`@opentelemetry/sdk-node` auto-instrumentation** (Express, Fastify, NestJS, `pg`, `ioredis`) and you want a 1-line wrapper that wires the standard Node OpenTelemetry SDK to the obs-unified collector.

All backend SDKs expose the **same core surface**, so muscle memory ports across runtimes:

| Concept    | TypeScript                               | Go                                   | Rust                                        |
| ---------- | ---------------------------------------- | ------------------------------------ | ------------------------------------------- |
| Init       | `initObservability(config)`              | `obs.Init(ctx, cfg)`                 | `obs_unified::init(cfg)`                    |
| HTTP stamp | `stampInteractionFromRequest(span, req)` | `obs.StampInteraction(ctx, r)`       | `obs_unified::stamp_interaction(span, req)` |
| LLM span   | `startLLMSpan(name, opts)`               | `obs.StartLLMSpan(ctx, name, opts)`  | `start_llm_span(name, opts)`                |
| Tool span  | `startToolSpan(name, opts)`              | `obs.StartToolSpan(ctx, name, opts)` | `start_tool_span(name, opts)`               |
| Project ID | `extraHeaders: { "x-project-id": id }`   | `obs.SetProjectID(ctx, id)`          | `set_project_id(id)`                        |

> **Go and Rust lineage gap.** The Go and Rust SDKs do not yet emit `vcs.commit.id`, `obs.config.digest`, or feature-flag cohort attributes, so change correlation by commit and by flag cohort works for TypeScript services only today. They still send `service.version` and the environment.

These are deliberately **thin wrappers around OpenTelemetry**: one-line collector init, OpenInference-shaped helpers so LLM/tool spans render in the dashboard's AI tab, runtime lineage (`service.version`, `vcs.commit.id`, `deployment.environment.name`) for AutoSRE change correlation, project-id propagation for multi-tenant, and the loop-guard header for services that ingest their own telemetry.

If your backend is in **Python, Java/JVM, .NET, Ruby, PHP, or Elixir**, standard OpenTelemetry OTLP/HTTP exporters work natively with **100% feature parity**—simply copy the inbound `x-obs-interaction` request header onto the active span as `obs.interaction.id` (see the [Python Flask walkthrough](/docs/instrument-python-flask) for a 4-line request hook).

## Browser SDK [#browser-sdk]

| Package                                                                                | Where it runs              | What it does                                                                                                                              |
| -------------------------------------------------------------------------------------- | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| [`@obsunified/analytics-sdk`](https://www.npmjs.com/package/@obsunified/analytics-sdk) | Browser (React or vanilla) | Tracks usage events, identifies users, mints **interaction\_id** on every click, auto-injects `x-obs-interaction` on outbound `fetch`/XHR |

This one is browser-only by definition — there's no parallel Go/Rust analytics SDK because that surface area is the web.

All four packages live in the obs-unified monorepo. The TypeScript packages publish to public npm under the [`@obsunified` organization](https://www.npmjs.com/org/obsunified). The Go SDK is consumed from the public `sdks/go` module path through Go module tags like `sdks/go/v0.1.1` and is indexed on [pkg.go.dev](https://pkg.go.dev/github.com/dev-sawan-ruparel/obs-unified/sdks/go). The Rust SDK publishes to crates.io as [`obs-unified`](https://crates.io/crates/obs-unified). The full list of published artifacts, including the all-in-one container image on GHCR, is in [Installation](/docs/installation#packages-and-registries).

## MCP server [#mcp-server]

| Package                                                                          | Where it runs             | What it does                                                                                                                                                                                                                      |
| -------------------------------------------------------------------------------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`@obsunified/mcp-server`](https://www.npmjs.com/package/@obsunified/mcp-server) | Local MCP host over stdio | Exposes read-only investigation tools for status, recent traces, trace detail, service maps, logs, AI sessions, users, replays, profiles, evals, compact evidence bundles, connected signals, agent runs, actions, and tool calls |

The MCP server is intentionally a **read boundary**. Agents can inspect the same graph a human sees in the dashboard, but they do not need the write-only telemetry ingest key used by app SDKs. Configure it with a dashboard token, a compatible read bearer, or a local `obs_session` cookie for development.

See [MCP server](/docs/mcp-server) for install and host configuration.

> \[!NOTE]
> The MCP server uses the `@obsunified` scope on public npm (`@obsunified/mcp-server`). The first-party SDKs use the `@obsunified` scope on public npm (`@obsunified/*`).

### npm install [#npm-install]

```bash
pnpm add @obsunified/telemetry-sdk
pnpm add @obsunified/analytics-sdk
```

## The identity skeleton [#the-identity-skeleton]

```mermaid
flowchart LR
  user["user_id"] --> session["session_id"]
  session --> interaction["interaction_id"]
  interaction --> trace["trace_id"]
  trace --> span["span_id"]
```

The browser SDK owns the left half (user / session / interaction). The server SDK owns the right half (trace / span). Agent helpers and MCP context propagation extend that same skeleton across agent runs, actions, and tool calls. They meet at the **`x-obs-interaction` request header** that the browser sets on outbound fetches and the server stamps onto the resulting span.

Once a signal carries `interaction_id`, the dashboard's [Connected Rail](/docs/what-to-expect) can pivot from any entity to "the click that caused this trace" in one click.

For CPU and off-CPU work, `interaction_id` is the entry key, not the profiler's storage key. Profiles are indexed by `trace_id`; when a profiled service labels samples with trace IDs, the path becomes:

```mermaid
flowchart LR
  interaction["interaction_id"] --> trace["trace_id"]
  trace --> profileIndex["profile_trace_index"]
  profileIndex --> profile["CPU/off-CPU profile"]
```

## @obsunified/analytics-sdk [#obsunifiedanalytics-sdk]

### Install [#install]

```bash
pnpm add @obsunified/analytics-sdk
```

For React hosts the SDK exports a provider component; for other hosts call the lower-level `installAutoCorrelate()` directly.

### React quick start [#react-quick-start]

```tsx check
import { AnalyticsProvider, AnalyticsErrorBoundary } from "@obsunified/analytics-sdk/react";

declare const ErrorPage: () => null;
declare const App: () => null;

export function Root() {
	return (
		<AnalyticsProvider
			collectorUrl="https://your-collector.example.com"
			apiKey={(import.meta as unknown as { env: Record<string, string> }).env.VITE_OBS_INGEST_KEY}
			trackPageViews
			captureErrors
			storagePrefix="myapp"
		>
			<AnalyticsErrorBoundary context="App" fallback={<ErrorPage />}>
				<App />
			</AnalyticsErrorBoundary>
		</AnalyticsProvider>
	);
}
```

The provider:

* Mints a `session_id` on first mount, persists it for \~30 minutes of inactivity
* Tracks page views automatically on `pushState` / `popstate`
* Captures uncaught errors and unhandled promise rejections
* Installs the **Mode A** auto-correlator: a global capture-phase listener on `click`, `submit`, `keydown` that mints a fresh `interaction_id`, plus a global `fetch` + `XMLHttpRequest` wrapper that injects the `x-obs-interaction` header on outbound requests
* Provides `useAnalytics()` hook for explicit calls

Interaction ids are 26-character uppercase Crockford-base32 ULIDs (for example `01JAQ8K3Z9X7M2N4P6Q8R0S1T3`). The browser SDK mints valid ids; if you stamp `x-obs-interaction` by hand (curl tests, other languages), use that format, because `@obsunified/telemetry-sdk` silently drops malformed values.

### Inside a component [#inside-a-component]

```tsx check
import { useAnalytics } from "@obsunified/analytics-sdk/react";

export function CheckoutButton() {
	const { trackInteraction, identify, withInteraction } = useAnalytics();

	const onSubmit = withInteraction(async () => {
		// fetch() inside here automatically carries x-obs-interaction
		const res = await fetch("/api/checkout", { method: "POST" });
		trackInteraction("checkout_submitted", { status: res.status });
	});

	return <button onClick={onSubmit}>Place order</button>;
}
```

### Identifying users [#identifying-users]

```ts
identify("user-42", { email: "alice@example.com", plan: "pro" });
```

Calls `POST /v1/identify` on the collector with `userId`, `visitorId`, `email`, `name`, `properties`. The endpoint upserts into `user_profiles` and uses `MIN(...)` to preserve the earliest `first_seen_at` on conflict.

For historical imports / backfills, you can also pass an optional `firstSeenAt` ISO timestamp:

```ts
await fetch(`${collector}/v1/identify`, {
	method: "POST",
	body: JSON.stringify({
		userId: "user-42",
		visitorId: "vis-abc",
		email: "alice@example.com",
		firstSeenAt: "2026-01-15T08:30:00Z",
	}),
});
```

Future timestamps are rejected silently — clock-skewed clients can't poison the table.

### Mode B: explicit interaction context [#mode-b-explicit-interaction-context]

Auto-correlation (Mode A) handles synchronous handlers and shallow `await` chains. Deeper async flows (debounces, setTimeout-queued work, state-machine transitions) escape the microtask cascade. For those, use `withInteractionContext`:

```ts check
import { withInteractionContext, currentInteractionId } from "@obsunified/analytics-sdk";

// Capture at click time
const id = currentInteractionId();

// Re-enter the context wherever the work actually happens
setTimeout(() => {
	withInteractionContext(id!, () => {
		fetch("/api/long-running"); // carries the click's interaction_id
	});
}, 500);
```

## @obsunified/telemetry-sdk [#obsunifiedtelemetry-sdk]

### Install [#install-1]

```bash
pnpm add @obsunified/telemetry-sdk
```

### Cloudflare Worker / Hono quick start [#cloudflare-worker--hono-quick-start]

```ts nocheck needs the hono app and Worker env bindings, which are not part of the SDK types
import {
	createRequestSpan,
	initObservability,
	recordFeatureFlag,
	runWithSpan,
	stampInteractionFromRequest,
	flushSpans,
	flushLogs,
	flushAICalls,
} from "@obsunified/telemetry-sdk";

app.use("*", async (c, next) => {
	initObservability({
		collectorUrl: c.env.OBS_COLLECTOR_URL,
		apiKey: c.env.OBS_INGEST_KEY,
		serviceName: "checkout-api",
		environment: c.env.OBS_ENVIRONMENT ?? "prod",
		serviceVersion: c.env.OBS_SERVICE_VERSION ?? "2.4.1",
		commitSha: c.env.OBS_COMMIT_SHA,
	});
	await next();
});

app.use("*", async (c, next) => {
	const span = createRequestSpan("checkout-api", `${c.req.method} ${c.req.path}`);
	span.setAttribute("http.request.method", c.req.method);
	// RFC 0004 — closes the click-to-trace loop. No-op if header is missing.
	stampInteractionFromRequest(span, c.req.raw);

	try {
		await runWithSpan(span, () => next());
		span.setStatus(c.res.status >= 400 ? 2 : 1);
	} finally {
		span.end();
		await Promise.all([flushSpans(), flushLogs(), flushAICalls()]);
	}
});
```

The same primitives work in plain Node.js — replace `c.env` with `process.env`, replace the Hono context with a node request handler.

### Child spans [#child-spans]

```ts check
import { withChildSpan } from "@obsunified/telemetry-sdk";

declare const db: { query(sql: string): Promise<unknown[]> };

const items = await withChildSpan("db.query.items", async (child) => {
	child.setAttribute("db.system", "postgres");
	child.setAttribute("db.statement", "SELECT * FROM items");
	return await db.query("SELECT * FROM items");
});
```

Child spans inherit the parent's trace context and interaction id automatically.

### Logs [#logs]

```ts check
import { createLogger } from "@obsunified/telemetry-sdk";

declare const cartId: string;
declare const items: unknown[];
declare const traceId: string;
declare const error: unknown;

const logger = createLogger("checkout-api");

logger.info("Cart loaded", { cartId, itemCount: items.length });
logger.error("Stripe webhook signature failed", { traceId, error });
```

Logs are flushed at request end via `flushLogs()`. If a span is active, the log inherits `trace_id`, `span_id`, `session_id`, and `interaction_id`.

### AI calls [#ai-calls]

The SDK exposes typed helpers for OpenInference-style LLM spans:

```ts check
import { setAISessionContext, startRetrieverSpan } from "@obsunified/telemetry-sdk";

declare const sessionId: string;
declare const userId: string;
declare const query: string;
declare const db: { vectorSearch(q: string): Promise<{ id: string; score: number; content: string }[]> };

setAISessionContext({ sessionId, userId });

const retriever = startRetrieverSpan({ query, name: "vector.search" });
try {
  const docs = await db.vectorSearch(query);
  retriever.addDocuments(docs);
} finally {
  retriever.end();
}
```

For lower-level LLM and tool spans, use `startLLMSpan()` and `startToolSpan()` as shown in [SDK API reference](/docs/sdk-reference#ai-and-llm-span-tracking).

### Cloudflare Workers bindings [#cloudflare-workers-bindings]

Workers services can wrap platform bindings from the Cloudflare-specific entry point:

```ts check
import { wrapD1, wrapFetch, wrapR2 } from "@obsunified/telemetry-sdk/cloudflare";

declare const env: {
  DATABASE: Parameters<typeof wrapD1>[0];
  STORAGE_BUCKET: Parameters<typeof wrapR2>[0];
};

const db = wrapD1(env.DATABASE);
const bucket = wrapR2(env.STORAGE_BUCKET, { bucketName: "blobs" });
const telemetryFetch = wrapFetch(globalThis.fetch);
```

## Wire-format compatibility [#wire-format-compatibility]

The collector accepts standard OTLP/HTTP at:

* `POST /v1/traces` — protobuf or JSON OTLP traces
* `POST /v1/logs` — protobuf or JSON OTLP logs

So any OTel-compatible producer (Python, Java/JVM, .NET, the Go collector, OpenInference instrumentations, Beyla eBPF agent) works alongside the first-party SDKs. To enable the [click-to-trace pivot](/docs/what-to-expect#scenario-a) in a native OTel SDK, copy the inbound `x-obs-interaction` request header onto the server span as `obs.interaction.id` (see [Python Flask instrumentation](/docs/instrument-python-flask)).

## IDE deep-linking configuration [#ide-deep-linking-configuration]

The dashboard supports deep-linking from traces, spans, and profile frames directly to files in your local editor.

### Saved IDE template [#saved-ide-template]

The selected editor scheme is saved in the browser's `localStorage` under the key `obs_ide_template`.

### IDE selector dropdown [#ide-selector-dropdown]

In the dashboard's top bar, the `IdeSelector` dropdown lets you select your preferred IDE:

* **VS Code**: `vscode://file/{absolutePath}:{lineNumber}`
* **Cursor**: `cursor://file/{absolutePath}:{lineNumber}`
* **WebStorm**: `webstorm://open?file={absolutePath}&line={lineNumber}`
* **Custom**: A custom URI template of your choice.

### Custom templates [#custom-templates]

When configuring a custom IDE template, the following placeholders are dynamically replaced:

* `{absolutePath}`: The absolute path of the file on your local machine (e.g. `/Users/username/project/file.ts`).
* `{relativePath}`: The repository-relative path of the file (e.g. `packages/dashboard/src/app.tsx`).
* `{lineNumber}`: The 1-indexed line number.
