---
title: SDK
description: Set up @openperf/sdk in the browser, Node.js and Bun, and report errors, users and spans.
---

```bash
npm install @openperf/sdk
```

| Entry point             | For                                                           |
| ----------------------- | ------------------------------------------------------------- |
| `@openperf/sdk/browser` | Browser apps. ES modules only.                                |
| `@openperf/sdk/node`    | Node.js 22.15 or later.                                       |
| `@openperf/sdk/bun`     | Bun. It throws on Node.js.                                    |
| `@openperf/sdk/cli`     | The `openperf` CLI for [releases and source maps](/releases). |

The browser, Node.js and Bun entry points export the same API: `init`, `captureException`, `setUser`, `setTag`, `addBreadcrumb`, `startSpan` and `flush`.

## Browser

```ts
import { init } from "@openperf/sdk/browser";

init({
  dsn: "https://opf_…@ingest.example.com",
  environment: "production",
  release: "1.4.2",
  tracePropagationTargets: ["https://api.example.com"],
});
```

Call `init` once, as early as possible. It collects:

- **Errors**: uncaught errors and unhandled promise rejections.
- **Page loads and navigations**: one trace per page load and per path change in the history.
- **Web vitals**: LCP, INP, CLS, FCP and TTFB, sent when the page is hidden.
- **Requests**: a span for every `fetch` and `XMLHttpRequest`.
- **Breadcrumbs**: clicks, `console.log`, `console.warn`, `console.error` and navigations, attached to errors.

Data is sent in batches and flushed when the page is hidden.

The browser reads no environment variables: pass `dsn`, `environment` and `release` in code, for example from your bundler's env.

Requests get a `traceparent` header so backend spans join the browser trace. By default only same-origin requests get it. `tracePropagationTargets` replaces that with your own list: a string matches when the URL contains it, a `RegExp` when it matches. To keep same-origin requests, add your own origin to the list. A cross-origin backend must allow the `traceparent` header in its CORS settings.

To accept browser data only from your sites, list them under **Allowed origins** in the project settings. One origin per line, up to 20. Empty accepts any origin; requests from an unlisted origin get `403`.

## Node.js

Put `init` in its own file, `instrument.mjs`, so it runs before your app loads:

```js
import { init } from "@openperf/sdk/node";

init({ serviceName: "checkout" });
```

For an ES module app, load the file with `--import`. `init` registers the loader hooks itself, so ES modules get instrumented:

```bash
node --import ./instrument.mjs app.mjs
```

For a CommonJS app, use `require` in `instrument.cjs` and start with `node --require ./instrument.cjs app.js`.

## Bun

Create `instrument.ts`:

```ts
import { init } from "@openperf/sdk/bun";

init({ serviceName: "checkout" });
```

```bash
bun --preload ./instrument.ts app.ts
```

To preload it every time, add it to `preload` in `bunfig.toml`.

## What servers collect

On Node.js and Bun, `init` sets up OpenTelemetry with the standard Node.js instrumentations and Fastify:

- **Traces** for incoming and outgoing HTTP, frameworks such as Express and Fastify, databases, caches and queues. Bun also traces `Bun.serve` and `fetch`.
- **Errors**: uncaught exceptions, and exceptions an instrumentation records on a span, such as an Express handler that throws.
- **Logs** from pino, winston and bunyan, unless `captureLogs` is `false`.

On an uncaught exception the SDK reports it, prints it and exits with code `1` after up to 2 seconds of sending. If your app registers its own `uncaughtException` handler, the SDK only reports and leaves the rest to it.

Data is flushed when the process exits on its own. Call `await flush()` before `process.exit()` and at the end of a serverless handler.

`OTEL_NODE_ENABLED_INSTRUMENTATIONS` and other standard OpenTelemetry variables still apply.

## Options

| Option                    | Runtime      | Default                | Description                                                                  |
| ------------------------- | ------------ | ---------------------- | ---------------------------------------------------------------------------- |
| `dsn`                     | All          | `OPENPERF_DSN`         | The project DSN. Without it, `init` does nothing.                            |
| `environment`             | All          | `OPENPERF_ENVIRONMENT` | Such as `production` or `staging`. Data without one goes to `default`.       |
| `release`                 | All          | `OPENPERF_RELEASE`     | The [release](/releases) version. Needed for source maps and release health. |
| `serviceName`             | All          | `OTEL_SERVICE_NAME`    | The service shown on traces, logs and errors.                                |
| `tracesSampleRate`        | All          | `1`                    | Share of traces to send, from `0` to `1`.                                    |
| `tracePropagationTargets` | Browser      | Same origin            | URLs that get a `traceparent` header.                                        |
| `captureLogs`             | Node.js, Bun | `true`                 | Send pino, winston and bunyan logs.                                          |
| `instrumentations`        | Node.js, Bun | `[]`                   | More OpenTelemetry instrumentations to register.                             |

Environment variables are read only on Node.js and Bun. An option passed to `init` wins over its variable. A malformed DSN or a sample rate outside `0` to `1` throws. A second `init` is ignored.

### Sampling

`tracesSampleRate` decides per trace, so a sampled trace keeps all its spans. Errors from `captureException` and uncaught errors are always sent. Exceptions that an instrumentation records on a span travel with that span: below `1`, the ones in unsampled traces are lost. Report errors you always need with `captureException`.

## API

```ts
import { captureException, setTag, setUser, startSpan } from "@openperf/sdk/node";

setUser({ id: "u_123", email: "ada@example.com" });
setTag("plan", "pro");

await startSpan({ name: "charge card", op: "payment" }, async (span) => {
  span.setAttribute("amount", 4200);
  await chargeCard();
});

captureException(error, { fingerprint: "payment-declined" });
```

| Function                            | Does                                                                                                                               |
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `captureException(error, context?)` | Reports an error. `fingerprint` groups all errors with the same value together. `handled` defaults to `true`.                      |
| `setUser(user)`                     | Adds `id`, `email` and `name` to later errors and spans. `null` clears it.                                                         |
| `setTag(key, value)`                | Adds a string, number or boolean attribute to later errors and spans.                                                              |
| `addBreadcrumb(breadcrumb)`         | Records a step with a `category`, `message`, `level` and `data`. Errors carry the last 100.                                        |
| `startSpan(options, callback)`      | Runs `callback` in a span named `name`, with an optional `op` and `attributes`. A thrown error or rejected promise fails the span. |
| `flush()`                           | Sends everything queued.                                                                                                           |

On a server, `setUser`, `setTag` and `addBreadcrumb` inside a request apply to that request only. Outside a request, and in the browser, they apply to everything after the call.

Before `init`, the functions do nothing and `startSpan` only runs its callback, so libraries can call them safely.
