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

# Sazabi Browser SDK

> Capture browser sessions — navigation, clicks, errors, and network calls with trace context — with the Sazabi browser SDK.

[All Sazabi Browser SDK integration capabilities](https://docs.sazabi.com/catalogs/integrations/sazabi-browser-sdk)

Capture what happens in your users' browsers with `@sazabi/browser`, the official Sazabi browser SDK. It emits a session-scoped stream of navigation, interactions, console output, JavaScript errors, and network calls as OTLP log records, and injects W3C trace context into your API requests so a frontend symptom and its backend cause share a trace id.

## About

`@sazabi/browser` is a zero-dependency browser library you install in your own web app. It lets you:

* Follow a single visitor's session across tabs, from first page load to error
* Catch uncaught exceptions and unhandled promise rejections with stack traces
* See rage clicks and dead clicks — the interactions that signal frustration
* Join frontend network calls to backend logs on a shared `trace_id`
* Use AI to investigate a browser session end to end

The SDK is configured with your intake URL alone. The URL embeds a write-only public key, which is why it is safe to ship in a browser bundle: it can write telemetry into your project and never read from it.

### What gets captured

Every event carries `session.id`, `session.window_id`, `session.distinct_id`, `app.screen.url`, and an `app.event_type` naming the kind of event:

| `app.event_type` | What it records |
| - | - |
| `navigation` | Page loads and in-app route changes, including programmatic `pushState` navigation |
| `pointer` | One episode per mouse, touch, or pen contact, from down to up, with a sampled path; long contacts emit partial rows |
| `click`, `rage_click`, `dead_click` | Taps and keyboard clicks, and the repeated or no-op clicks that signal frustration |
| `input` | One event per field-interaction episode — which field, never what was typed |
| `network` | `fetch` and `XMLHttpRequest` calls with method, status, duration, and the injected trace context |
| `error` | Uncaught exceptions and unhandled rejections, with `exception.type`, `exception.message`, and `exception.stacktrace` |
| `log` | Lines your app emits through the SDK's `log()` |
| `custom` | Marks you emit with `addEvent()` |
| `lifecycle` | The tab going to the background and coming back |
| `sdk_health` | The SDK reporting its own delivery trouble (`app.sdk.diagnostic`), at most once per condition per page |

By default the SDK also mirrors `console.error` and `console.warn` into the stream, leaving the original console behavior untouched.

Session replay recordings and Core Web Vitals are not captured.

SDK versions before 0.3.0 emit the same keys under a `web.` prefix (`web.event_type`, `web.element.name`, `web.page.url`); rows from those versions keep that shape until the app upgrades.

### Configuration

| Option | Default | Purpose |
| - | - | - |
| `network.capture` | `true` | Emit network events for fetch/XHR |
| `network.propagateTraceContext` | `true` | Inject `traceparent` into eligible requests |
| `network.allowlist` | same-origin only | Origins eligible for header injection |
| `network.sampledFlag` | `false` | Mark minted traces sampled (`-01`) for backend tracers |
| `network.resourceFallback` | `true` | Observe requests that bypass instrumentation |
| `network.requestIdHeaders` | common platform headers | Response headers probed for a request id |
| `console.capture` / `console.levels` | `true`, `["error", "warn"]` | Console mirroring |
| `input.capture` | `true` | Field-interaction episodes |
| `consent` | off | Hold all capture until it resolves true |
| `flushIntervalMs` | `5000` | Batch flush cadence |

### Privacy controls

* **Input values are never captured** — not even value lengths. An `input` event records which field was engaged, for how long, how many edits, and whether content was pasted; never keystrokes or contents. Request and response bodies are never captured either.
* **Element names come from developer-authored sources only** — `aria-label`, `<label>`, `placeholder`, and button captions — and only for interactive elements. Clicks on plain containers, where user content lives, record selectors only.
* **Mask or rename any element.** Add `data-sazabi-mask` to suppress text capture for an element's whole subtree, or `data-sazabi-name` to set its reported name explicitly.
* **Consent gating.** Pass `consent: () => boolean | Promise<boolean>` and the SDK installs no listeners, no patches, and makes no network calls until it resolves true. `identify()` calls made while consent is pending are buffered and applied when capture starts.
* **Client-asserted identity.** `session.distinct_id` is whatever the browser claims. Treat it as a claim, not as proof of identity — the same is true of every field on these events, since the intake URL is extractable from any page that ships it.
* **Reversible by construction.** Every patch is transparent, idempotent, and reversible; SDK failures are invisible to your app, and `shutdown()` restores the original functions.

## Prerequisites

* A web app you can add an npm dependency to and redeploy.
* A Sazabi project.

## Set up in the dashboard

**Step 1**

### Open the log source

Go to [**Settings > Log streams**](https://app.sazabi.com/settings/log-source-providers), choose **Sazabi Browser SDK**, and click **Add**.

**Step 2**

### Copy your intake URL

Sazabi shows your complete intake URL at the top of the setup screen. That URL is all the SDK needs — pass it as `intakeUrl`, with or without the `/v1/logs` path.

**Step 3**

### Install the SDK

```bash theme={"system"}
npm install @sazabi/browser
```

**Step 4**

### Initialize in your entry module

Import the register entrypoint on the **first line** of your app's entry
module (`src/main.tsx`, `src/index.ts`, `app/layout.tsx`), then call
`init()`:

```ts theme={"system"}
// Import first, because a module that runs earlier can capture the native
// fetch/XHR/history references before the SDK wraps them.
import "@sazabi/browser/register";

import { init } from "@sazabi/browser";

init({
  intakeUrl: "<your intake URL>",
  serviceName: "my-web-app",
  serviceVersion: process.env.GIT_SHA,
  environment: "production",
});
```

The register import installs dormant instrumentation before any other
module can capture the native `fetch`, `XMLHttpRequest`, and `history`
references. It is optional but recommended: `init()` alone also
instruments, but only at call time, and every static import in your entry
file evaluates before its first statement — so an auth client or HTTP
wrapper constructed at module scope wins that race.

Requests that slip past instrumentation are still observed through a
resource-timing fallback, and the SDK emits a once-per-origin
`instrumentation_gap` diagnostic naming the escaped origin, so the gap is
visible rather than silent.

`init()` is idempotent, never throws into your app, and no-ops during
server-side rendering, so it is safe in a shared entry module.

**Step 5**

### Correlate with your backend

Same-origin requests get a `traceparent` header by default. If your API is
on another origin, add it to the network allowlist:

```ts theme={"system"}
init({
  // …
  network: {
    allowlist: ["https://api.example.com"],
  },
});
```

Your API's CORS configuration must include `traceparent` in
`Access-Control-Allow-Headers`, or the browser preflight fails and those
requests break. Your backend also has to run each request inside the
incoming trace context for its log lines to share the same `trace_id`. If
your backend runs OpenTelemetry, that extraction already happens by
default; set `network: { sampledFlag: true }` if you also want your
backend tracer to *record* those traces — the log join works either way.

The SDK additionally records platform request ids echoed on responses
(`x-request-id`, `cf-ray`, `x-vercel-id`, and similar) as
`app.network.request_id`, giving you an exact join key against CDN and
platform logs with no backend changes. Reading those cross-origin
requires the header in `Access-Control-Expose-Headers`.

**Step 6**

### Attribute sessions to users (optional)

```ts theme={"system"}
import { addEvent, identify, log, reset } from "@sazabi/browser";

identify("user_123", { plan: "pro" }); // on sign-in — internal id, not an email
addEvent("checkout_started", { cartValue: 42 }); // custom mark
log("ERROR", "checkout failed", { orderId }); // app log line
reset(); // on sign-out
```

`reset()` rotates the session and window ids, so later activity on the same
device never threads into the previous user's timeline. Switching
identities without a `reset()` rotates automatically.

**Step 7**

### Hand off the change

Commit these changes on a new branch, push it, and open a pull request. Keep the intake URL and any keys in environment variables or a secret store, and commit only configuration that references them.

## Set up with the CLI

You can also register the Sazabi Browser SDK log source with the [Sazabi CLI](https://docs.sazabi.com/reference/cli.md) (installed and authenticated — see [CLI reference](https://docs.sazabi.com/reference/cli/log-sources.md)).

Registering the source mints the same intake URL the dashboard shows above — the only value the SDK needs:

```bash theme={"system"}
sazabi log-sources create sazabi_browser_sdk --mode manual
```

## Verify

1. **Use your app**: load it and click through a few pages. The SDK flushes every 5 seconds, and again when the tab is hidden or closed, so events land within about 10 seconds.

2. **Check the browser**: in the network tab, filter for `intake` — POSTs to `/v1/logs` should return 200.

3. **Confirm arrival**: open the [Intake page](https://docs.sazabi.com/guides/instrument/intake.md) in the Sazabi dashboard and confirm records from the **Sazabi Browser SDK** source appear, or ask the agent in a thread: "Show me recent browser sessions."

## Troubleshooting

**No requests to intake at all** — Confirm `init()` runs before your framework mounts. Check the browser console for the `[@sazabi/browser]` configuration warning, which means `intakeUrl` or `serviceName` was empty. If you pass a `consent` callback, confirm it resolves `true`.

**Requests return 401** — The intake URL's embedded key was deactivated, or the URL belongs to another project. Check the log stream in [**Settings > Log streams**](https://app.sazabi.com/settings/log-source-providers) and copy the current intake URL.

**Some requests are missing from the stream** — Another library captured native `fetch` before the SDK did. Move `import "@sazabi/browser/register";` to the true first line of your entry module. The SDK falls back to resource-timing data for these requests and emits a one-time instrumentation-gap diagnostic per origin.

**Network events have no trace id** — `traceparent` is injected only into same-origin requests and origins you added to `network.allowlist`. For cross-origin APIs, confirm the response allows the `traceparent` request header in CORS.

**Stream shows Inactive** — Not an error: it means no telemetry arrived in the last hour. If you expected telemetry, check that the build calling `init()` has deployed and someone has loaded the app since, that a `consent` gate is not resolving false, and that build-time configuration did not inline an empty string for `intakeUrl` (`init()` warns once in the console and disables itself).

### Log stream states

Each log stream for this source has a status visible in [**Settings > Log streams**](https://app.sazabi.com/settings/log-source-providers), derived from recent telemetry activity on the log stream's key:

| Status | Meaning |
| - | - |
| **Active** | Telemetry has been received from this log stream in the last hour. |
| **Inactive** | The stream is live, but no telemetry has been received in the last hour. Expected before your first telemetry arrives, or whenever the source is idle. |
| **Paused** | You paused the log stream, so it no longer accepts telemetry. Historical telemetry already received remains visible. Resume it from the log stream's action menu — the same intake URL starts accepting again, with no reconfiguration. |

## Further reading

> [card] [Intake](https://docs.sazabi.com/guides/instrument/intake.md)
>
> See per-source ingestion volume and health in the Intake page.

> [card] [Browser SDK reference](https://docs.sazabi.com/reference/browser-sdk.md)
>
> Every init option, capture type, and API of @sazabi/browser.

> [card] [Instrument a browser app](https://docs.sazabi.com/guides/instrument/frontend.md)
>
> The task-oriented walkthrough of the same setup.


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