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

# Browser SDK

> Client SDK for browsers — analytics events, structured logging, automatic sessions, and privacy controls.

```bash theme={null}
npm install @tell-rs/browser
```

A client-side singleton that handles device IDs, sessions, and delivery automatically. No user ID needed on `track` calls.

**No cookies. No IP addresses. No cross-site tracking.** Visitors are identified by a random device UUID stored in localStorage. The SDK collects anonymous device metadata only — you choose what properties to send. See [Audit & Privacy](/running-tell/audit-privacy) for the full privacy model.

## Quick start

```typescript theme={null}
import { tell } from "@tell-rs/browser";

tell.configure("a1b2c3d4e5f60718293a4b5c6d7e8f90");

tell.track("Page Viewed", { url: "/home" });
tell.identify("user_123", { name: "Jane", plan: "pro" });
```

Events called before `configure()` are queued and replayed once ready.

### Verify it works

Open your browser's DevTools Network tab and look for a `POST` request to `https://collect.tell.app/v1/events`. If you see a `202` response, the SDK is sending data successfully. In development mode, the SDK also logs to the console at `debug` level.

## Script tag (no bundler)

If you don't use a bundler, load the SDK directly from a CDN. The global build exposes a `Tell` object on `window`.

```html theme={null}
<script src="https://cdn.jsdelivr.net/npm/@tell-rs/browser@latest/dist/tell.global.js"></script>
<script>
  var tell = Tell.tell;
  tell.configure("YOUR_API_KEY");

  tell.track("Page Viewed", { url: location.pathname });
</script>
```

All configuration options, tracking methods, and privacy controls work the same as the npm install — only the loading mechanism differs.

### SPA page view tracking

For single-page apps without a framework router, hook into the History API to track navigations:

```html theme={null}
<script src="https://cdn.jsdelivr.net/npm/@tell-rs/browser@latest/dist/tell.global.js"></script>
<script>
  var tell = Tell.tell;
  tell.configure("YOUR_API_KEY");

  function trackPageView() {
    tell.track("Page Viewed", {
      url: location.href,
      path: location.pathname,
      referrer: document.referrer,
      title: document.title,
    });
  }

  trackPageView();

  var origPush = history.pushState;
  var origReplace = history.replaceState;
  history.pushState = function () {
    origPush.apply(this, arguments);
    trackPageView();
  };
  history.replaceState = function () {
    origReplace.apply(this, arguments);
    trackPageView();
  };
  window.addEventListener("popstate", trackPageView);
</script>
```

### Pin a version

Replace `@latest` with a specific version to avoid unexpected changes in production:

```
https://cdn.jsdelivr.net/npm/@tell-rs/browser@0.3.2/dist/tell.global.js
```

## Configuration

```typescript theme={null}
tell.configure("YOUR_API_KEY", {
  endpoint: "https://collect.tell.app",
  batchSize: 20,
  flushInterval: 5_000,
  sessionTimeout: 30 * 60 * 1000,
  maxSessionLength: 24 * 60 * 60 * 1000,
  persistence: "localStorage",
  respectDoNotTrack: false,
  botDetection: true,
  captureErrors: false,
  onError: (e) => console.error("[Tell]", e),
});
```

For local development, point the SDK at your Tell server:

```typescript theme={null}
tell.configure("YOUR_API_KEY", {
  endpoint: "http://localhost:8080",
  batchSize: 5,
  flushInterval: 2_000,
  logLevel: "debug",
});
```

## Tracking events

No user ID parameter — the SDK tracks the current user automatically (anonymous until you call `identify`). For a full explanation of the identity model (device ID, anonymous ID, user ID), see [Users & Identity](/tracking/users-and-identity).

```typescript theme={null}
tell.track("Feature Used", { feature: "dark_mode", enabled: true });
tell.identify("user_123", { name: "Jane", email: "jane@example.com" });
tell.group("company_456", { plan: "enterprise" });
tell.revenue(49.99, "USD", "order_789", { product: "annual_plan" });
tell.alias("anon_visitor_abc", "user_123");
```

For details on each event type and when to use them, see [Events & Properties](/tracking/events-and-properties).

### Standard event names

```typescript theme={null}
import { tell, Events } from "@tell-rs/browser";

tell.track(Events.PageViewed, { url: "/pricing" });
tell.track(Events.UserSignedUp, { source: "organic" });
tell.track(Events.FeatureUsed, { feature: "export" });
```

Constants cover user lifecycle, revenue, subscriptions, trials, shopping, engagement, and communication events. Custom string names always work too.

### Super properties

Properties automatically attached to every `track`, `group`, and `revenue` call:

```typescript theme={null}
tell.register({ app_version: "2.1.0" });

// Every track call now includes app_version
tell.track("Click", { button: "submit" });

tell.unregister("app_version");
```

Event-specific properties override super properties when keys conflict. See [Events & Properties](/tracking/events-and-properties) for more on property patterns.

## Structured logging

```typescript theme={null}
tell.logError("Payment failed", { error: "card_declined", amount: 9.99 });
tell.logInfo("User signed in", { method: "oauth" });
tell.logWarning("High memory usage");
```

Nine severity levels from `logEmergency` to `logTrace`. See [Logs](/tracking/logs) for the full level reference and when to use each.

## Sessions

Sessions are managed automatically. The session ID is persisted to localStorage, so page navigations within the same site continue the same session. A new session starts after 30 minutes of inactivity (configurable via `sessionTimeout`) or after 24 hours (configurable via `maxSessionLength`). The SDK also captures UTM parameters from the URL as super properties.

For the full session lifecycle model, see [Sessions](/tracking/sessions).

## Data collected automatically

The browser SDK captures device context on every session. No PII is collected — all fields are anonymous device and browser metadata.

| Data                                | Example value                          | Field                                               |
| ----------------------------------- | -------------------------------------- | --------------------------------------------------- |
| Full page URL (incl. query strings) | `https://example.com/pricing?ref=blog` | `context.url`                                       |
| Page title                          | `Pricing - Acme`                       | `context.title`                                     |
| Referrer URL                        | `https://google.com/search?q=acme`     | `context.referrer`                                  |
| Referrer domain                     | `google.com`                           | `context.referrer_domain`                           |
| UTM parameters                      | `utm_source=twitter`                   | Super properties                                    |
| Browser + version                   | `Chrome 120`                           | `context.browser`, `context.browser_version`        |
| OS + version                        | `macOS 14.2`                           | `context.os_name`, `context.os_version`             |
| Device type                         | `desktop`                              | `context.device_type`                               |
| Screen dimensions                   | `2560 x 1440`                          | `context.screen_width`, `context.screen_height`     |
| Viewport dimensions                 | `1280 x 720`                           | `context.viewport_width`, `context.viewport_height` |
| Device pixel ratio                  | `2`                                    | `context.device_pixel_ratio`                        |
| CPU cores                           | `8`                                    | `context.cpu_cores`                                 |
| Device memory                       | `16` (GB)                              | `context.device_memory`                             |
| Locale                              | `en-US`                                | `context.locale`                                    |
| Timezone                            | `America/New_York`                     | `context.timezone`                                  |
| Connection type                     | `4g`                                   | `context.connection_type`                           |
| Touch support                       | `false`                                | `context.touch`                                     |

<Warning>
  **URLs include query strings.** If your app puts tokens or sensitive data in URLs (e.g. `?token=abc`), use `beforeSend` or the `redact()` utility below to strip them before events leave the browser.
</Warning>

## Redaction & beforeSend

The `beforeSend` hook lets you transform or drop events before they're queued. Return the modified event, or `null` to drop it entirely.

### Drop events from routes

```typescript theme={null}
tell.configure("YOUR_API_KEY", {
  beforeSend: (event) => {
    const url = event.context?.url;
    if (typeof url === "string" && new URL(url).pathname.startsWith("/internal")) {
      return null; // drop internal pages
    }
    return event;
  },
});
```

### Strip query params from URLs

```typescript theme={null}
tell.configure("YOUR_API_KEY", {
  beforeSend: (event) => {
    if (event.context?.url && typeof event.context.url === "string") {
      const u = new URL(event.context.url);
      u.searchParams.delete("token");
      u.searchParams.delete("api_key");
      return { ...event, context: { ...event.context, url: u.toString() } };
    }
    return event;
  },
});
```

### Remove PII from properties

```typescript theme={null}
tell.configure("YOUR_API_KEY", {
  beforeSend: (event) => {
    if (event.traits?.email) {
      return { ...event, traits: { ...event.traits, email: "[REDACTED]" } };
    }
    return event;
  },
});
```

### Scrub log data

```typescript theme={null}
tell.configure("YOUR_API_KEY", {
  beforeSendLog: (log) => {
    if (log.data?.password) {
      return { ...log, data: { ...log.data, password: "[REDACTED]" } };
    }
    return log;
  },
});
```

### User opt-out via beforeSend

```typescript theme={null}
tell.configure("YOUR_API_KEY", {
  beforeSend: (event) => {
    if (localStorage.getItem("analytics-optout")) return null;
    return event;
  },
});
```

### `redact()` utility

For common redaction patterns, use the built-in `redact()` factory instead of writing `beforeSend` by hand:

```typescript theme={null}
import { tell, redact, redactLog, SENSITIVE_PARAMS } from "@tell-rs/browser";

tell.configure("YOUR_API_KEY", {
  beforeSend: redact({
    // Drop events from internal routes
    dropRoutes: ["/internal", "/health"],
    // Strip sensitive query params from all URLs
    stripParams: [...SENSITIVE_PARAMS, "session_id"],
    // Replace matching property/trait keys with "[REDACTED]"
    redactKeys: ["email", "phone", "ssn"],
  }),
  beforeSendLog: redactLog({
    redactKeys: ["password", "credit_card"],
  }),
});
```

`redact()` and `redactLog()` return standard `beforeSend` functions. You can combine them with your own in an array:

```typescript theme={null}
tell.configure("YOUR_API_KEY", {
  beforeSend: [
    redact({ stripParams: SENSITIVE_PARAMS }),
    myCustomHook,
  ],
});
```

<Tip>
  Client-side redaction is your first line of defense. For defense in depth, also configure [server-side pipeline redaction](/pipeline/transforms/redact) to catch anything that slips through.
</Tip>

## Error auto-capture

Set `captureErrors: true` to automatically log uncaught errors and unhandled promise rejections.

```typescript theme={null}
tell.configure("YOUR_API_KEY", { captureErrors: true });
```

### What it captures

**`window.onerror`** — uncaught exceptions:

* `message` — the error message
* `filename` — source file URL
* `lineno` / `colno` — line and column number
* `stack` — stack trace (when available)

**`unhandledrejection`** — unhandled promise rejections:

* `message` — the rejection reason (or `"Unhandled promise rejection"`)
* `stack` — stack trace (when the reason is an Error)

Both are logged via `logError` with service `"browser"` and appear in your Tell logs dashboard.

### When to use vs. your own error boundary

`captureErrors` is a catch-all safety net for errors that escape your application code. If you use React error boundaries or framework error handlers, you may prefer to log to Tell explicitly from those:

```typescript theme={null}
tell.logError(error.message, { stack: error.stack, component: "Checkout" });
```

## Privacy

```typescript theme={null}
tell.optOut();               // Stop all tracking — events are dropped locally
tell.optIn();                // Resume tracking
tell.isOptedOut();           // Check current state
tell.reset();                // Clear user, device ID, session, super properties
```

Opt-out state is persisted to localStorage. Set `respectDoNotTrack: true` to honor the browser's Do Not Track setting.

## Lifecycle

```typescript theme={null}
tell.enable();          // Re-enable after disable()
tell.disable();         // Pause tracking
tell.reset();           // Clear user, device, session (e.g. on logout)
await tell.flush();     // Flush pending events
await tell.close();     // Flush + shut down
```

## Error handling

Tracking calls (`track`, `identify`, `group`, `revenue`, `alias`) never throw. Validation errors and network failures route through the `onError` callback:

```typescript theme={null}
tell.configure("YOUR_API_KEY", {
  onError: (err) => console.error("[Tell]", err.message),
});
```

## Retry behavior

On HTTP send failure (5xx or network error), the SDK retries with exponential backoff: 1-second base delay, 1.5x multiplier, 20% jitter, capped at 30 seconds. After `maxRetries` attempts (default: 5), the batch is dropped and reported via `onError`.

The SDK also checks `navigator.onLine` before each attempt and skips if the browser reports offline.

## Browser-specific features

**Bot detection** — automatically disables tracking when `navigator.webdriver` is set or the user agent contains "headless". Controlled by `botDetection` (default: true).

**Page unload** — the SDK flushes via `navigator.sendBeacon` on `beforeunload` and `visibilitychange` to minimize data loss.

**Pre-init queue** — events called before `configure()` are buffered (up to 1,000) and replayed once the SDK is ready.

## Configuration reference

| Parameter           | Default                    | Description                                     |
| ------------------- | -------------------------- | ----------------------------------------------- |
| `endpoint`          | `https://collect.tell.app` | HTTP collector URL                              |
| `batchSize`         | 20                         | Events per batch before auto-flush              |
| `flushInterval`     | 5,000 ms                   | Time between auto-flushes                       |
| `maxRetries`        | 5                          | Retry attempts on send failure                  |
| `closeTimeout`      | 5,000 ms                   | Max wait on `close()`                           |
| `networkTimeout`    | 10,000 ms                  | HTTP request timeout                            |
| `sessionTimeout`    | 1,800,000 ms               | Inactivity time before new session              |
| `maxSessionLength`  | 86,400,000 ms              | Max session duration (prevents zombie sessions) |
| `persistence`       | `localStorage`             | `"localStorage"` or `"memory"`                  |
| `respectDoNotTrack` | false                      | Honor browser DNT setting                       |
| `botDetection`      | true                       | Auto-disable for bots/headless                  |
| `captureErrors`     | false                      | Auto-log uncaught errors                        |
| `logLevel`          | `error`                    | SDK log verbosity                               |
| `maxQueueSize`      | 1,000                      | Max queued items before oldest drops            |
