# SDK Reference

The BugTape JavaScript/TypeScript SDK records browser activity and lets users submit bug reports with full context.

## Installation

Use the hosted browser module, or `npm install @bugtape/sdk` with a bundler. Apply your app's consent policy first.

```html
<script type="module">
  import { init } from 'https://app.bugtape.ai/bugtape.mjs';
  init({ apiKey: 'bt_live_your_key_here' });
</script>
```

With a bundler, install the same SDK from npm instead:

```bash
npm install @bugtape/sdk
```

```js
import BugTape from '@bugtape/sdk';

BugTape.init({ apiKey: 'bt_live_your_key_here' });
```

## Supported runtimes

| Runtime | Supported | Notes |
|---------|-----------|-------|
| Browser (modern) | Yes | Chrome, Firefox, Safari, Edge — ES2020+ |
| Node.js | No | SDK uses browser APIs (DOM, fetch, rrweb) |
| Next.js (browser) | Yes | Use the hosted module example; no SSR initialization |
| Next.js (server) | No | Use the HTTP API for server-side ingestion |
| React Native | No | Browser-only |

## Browser module exports

```javascript
import { init, identify, token } from 'https://app.bugtape.ai/bugtape.mjs';
```

This import belongs in a browser module. For Next.js use the [hosted module example](./examples/nextjs/README.md), or install `@bugtape/sdk` with your bundler and initialize it in a client component.

## Initialization

```typescript
const instance = init({ apiKey: 'bt_live_your_key_here' });
```

### `BugTapeConfig`

| Property | Type | Required | Default | Description |
|----------|------|----------|---------|-------------|
| `apiKey` | `string` | Yes | — | Project API key (`bt_live_*` or `bt_test_*`) |
| `endpoint` | `string` | No | `https://app.bugtape.ai/v1/ingest` | Ingest endpoint URL |
| `registeredIdentity` | `{ applicationId: string; buildId?: string; installationId?: string }` | No | omitted | Management-enrolled UUIDs. `applicationId` is required inside this object; optional build and installation must belong to that application and the capture key's project. Copy the populated snippet from Setup. |
| `release` | `string` | No | inferred | Deploy/release id on every occurrence. Prefer `init({ release })`. If omitted: `BUGTAPE_RELEASE`, then common CI SHA envs (`GITHUB_SHA`, `VERCEL_GIT_COMMIT_SHA`, …). |
| `summarize` | `boolean` | No | `true` | Set to `false` to disable the AI summary entirely |
| `recordingWindow` | `number` | No | `10` | Buffer duration in minutes |
| `maxEvents` | `number` | No | `50000` | Maximum events in ring buffer |
| `autoReport` | `boolean \| { enabled?, maxPerMinute?, interactionWindowMs?, reportUncaughtErrors?, reportHungRequests?, hangTimeoutMs?, reportDeadRoutes?, emptyExpectRules? }` | No | on | Sends a report with no user action for uncaught errors and unhandled rejections, requests that fail within `interactionWindowMs` (8000) of a click, requests that hang longer than `hangTimeoutMs` (15000, at least 1000), page loads that return 400 or more, and in-app not-found screens. At most `maxPerMinute` (6) automatic reports a minute. `false` turns all of it off; empty 2xx responses are reported only for `emptyExpectRules` you set. See [Capture semantics](capture-semantics.md). |
| `showButton` | `boolean` | No | `true` | Show the floating bug-report button. `false` keeps automatic capture and `open()` (open the form from your own link) but shows no button. |
| `badge` | `boolean \| { siteName?: string; ref?: string }` | No | `false` | Show the button as BugTape's orange **b** badge. Hover or focus shows Report a bug, What is BugTape? and Go to BugTape. See [Corner badge](#corner-badge). |
| `majorBugAlert` | `boolean \| MajorBugAlertOptions` | No | `true` | Signal a newly accepted high or critical automatic issue on the report button. See [Report button and signal](#floating-report-button-and-serious-issue-signal). |
| `position` | `string` | No | `'bottom-right'` | Widget position: `bottom-right`, `bottom-left`, `top-right`, `top-left` |
| `theme` | `string` | No | `'auto'` | Widget theme: `dark`, `light`, `auto` |
| `tokenize` | `{ publicKey: string }` | No | omitted | Seal client details: text in `[data-bugtape-token]` elements and values passed to `token()` leave as placeholders; the values travel sealed to this public key. See [Client-detail tokens](#client-detail-tokens). |
| `privacy` | `'standard' \| 'strict'` | No | `'standard'` | `'strict'` masks every character of page text in the replay as solid blocks ("Anna Smith" → "▇▇▇▇ ▇▇▇▇▇") and text-bearing attributes, strips query strings from URLs, drops request/response bodies and console objects — all in the browser before anything is sent. Errors and stacks are still sent after the PII scrub. |
| `disableDomRecording` | `boolean` | No | `false` | Disable DOM mutation recording |
| `disableNetworkRecording` | `boolean` | No | `false` | Disable network request recording |
| `disableConsoleRecording` | `boolean` | No | `false` | Disable console output recording |
| `rageClickSensitivity` | `string` | No | `'medium'` | Rage click detection: `very-sensitive`, `medium`, `low` |
| `context` | `object` | No | — | Occurrence metadata: `reporterEmail`, `reporterId`, `sessionId`, `userId`, `release`, `environment`, `platform`, `metadata`. Prefer top-level `release`. |
| `context.platform` | `string` | No | `'web'` | Capture platform: `web`, `ios`, `android`, `react-native`, `flutter`, `server`, `other`. Sent on every ingest; the server infers it from the user agent when a client omits it. On the v1 web/server path, platform is not part of the fingerprint. Native v2 hashes a canonical JSON blob that includes `platform`. |
| `pii` | `Partial<PiiConfig>` | No | `{ enabled: true }` | PII scrubbing configuration |
| `onSubmit` | `(report: BugReport) => void \| Promise<void>` | No | — | Custom submit handler (replaces default) |
| `onSuccess` | `(response: IngestResponse) => void` | No | — | Called after successful submission |
| `onError` | `(error: Error) => void` | No | — | Called on submission failure |

Do not pass `encryptionKey`: the SDK rejects it because storage/read/decryption is not supported end to end. Do not put an AI provider key in browser code. `onSubmit` replaces default delivery; an empty handler discards the submission path. Use `onSuccess` for acknowledgement callbacks.

### Registered application, build and installation

Use Setup's enrolled snippet to associate a real capture with its installation. The SDK takes the IDs inside `registeredIdentity`; they are **not top-level SDK options**:

```javascript
init({
  apiKey: captureKey,
  registeredIdentity: { applicationId, installationId, buildId },
});
```

The variables above are your enrolled IDs from Setup or authorized deployment configuration. Omit `buildId` until a build has been registered. Basic capture can omit the entire object, but it cannot confirm a particular installation. A build or installation ID requires its application's ID. Do not invent IDs or label a repaired build with a failed build's ID.

The SDK copies and validates this object on first initialization. Misplaced top-level `applicationId`, `buildId` or `installationId` causes an early error before recording or transport starts. Repeated `init` calls return the existing instance without applying new configuration. The HTTP ingest payload flattens the validated values to those three top-level fields; that wire format is different from the SDK configuration.

An ingest acknowledgement proves report acceptance in the stated project. Setup's installation receipt separately confirms a real capture from that enrolled installation. Neither receipt attests a binary, physical device or verified repair. Agent work grants and trusted runner verification have separate authority; see [Agents](./agents.md).

### `BugTapeInstance`

| Method | Returns | Description |
|--------|---------|-------------|
| `open()` | `void` | Open the bug report widget programmatically |
| `setMajorBugAlertEnabled(enabled)` | `void` | Enable or disable the button signal; remember the choice in this browser |
| `isMajorBugAlertEnabled()` | `boolean` | Read the effective signal preference |
| `isMajorAlertActive()` | `boolean` | Check whether the button is signaling an issue |
| `clearMajorBugAlert()` | `void` | Clear the current signal without changing the preference |
| `signalMajorBug(reason?)` | `void` | Request a signal for your own confirmed failure; respects the enabled preference and does not send a report |
| `getEvents()` | `BugTapeEvent[]` | Get all buffered events within the recording window |
| `destroy()` | `void` | Tear down the SDK, remove event listeners, remove widget |
| `buffer` | `RingBuffer` | Direct access to the ring buffer (advanced use) |

## Floating report button and serious-issue signal

The floating button opens the report form at any time. A person can describe what they tried to do and send the captured context with their report. Automatic capture works even when nobody opens the form. Apply your app's consent policy and choose its privacy settings before initialization.

With default delivery, the SDK signals only after the server accepts a new automatic issue with `high` or `critical` severity. Failed submissions, duplicate reports, known regressions and silenced issues do not trigger a new signal. Browser errors, failed requests and hangs can be classified as `high`; this is a capture severity, not a claim of lost sales or verified business impact.

The button gives three soft shadow glows, then keeps a steady signal. It does not make a sound. Reduced motion removes the glow animation. Opening the report form clears the signal; otherwise it clears after 90 seconds by default. It never opens the form on its own.

```javascript
const instance = init({
  apiKey: 'bt_live_your_key_here',
  showButton: true,
  majorBugAlert: {
    enabled: true,
    severities: ['critical', 'high'],
    autoClearMs: 90_000,
  },
});
```

Use `majorBugAlert: false` to set a disabled default, or choose `severities: ['critical']` for a narrower signal. `autoClearMs: 0` keeps the steady signal until the form opens or you clear it. A preference saved by `setMajorBugAlertEnabled()` overrides the initialization default. Use `instance.setMajorBugAlertEnabled(false)` to disable and clear it at runtime.

Use `showButton: false` to hide the floating button while keeping automatic capture. Call `instance.open()` from your own report link. A custom `onSubmit` replaces default delivery and has no ingest acknowledgement, so it does not trigger this automatic signal; `signalMajorBug()` is available for your own confirmed failure and does not verify or send evidence.

### Corner badge

`badge: true` (or `badge: { siteName: 'Acme Shop' }`) turns the report button into BugTape's orange **b** in the `position` corner. It is off by default, so existing installs keep the bug button.

```javascript
init({ apiKey: 'bt_live_your_key_here', badge: { siteName: 'Acme Shop' } });
```

- Hover or keyboard focus shows three options: **Report a bug** (the same report form), **What is BugTape?** (a short note that names `siteName`, or "this site") and **Go to BugTape** (`https://bugtape.ai/?ref=<your hostname>` in a new tab; set `ref` to change the value).
- With a mouse, a click on the badge opens the report form. On a touch screen the first tap shows the options.
- Escape closes the form, then the options. With reduced motion the options appear without the stagger.
- The badge does not drag. `showButton: false` hides it; `instance.open()` still works.

## Auto-captured events

Once initialized, the SDK automatically captures these event types:

### DOM events (`dom`)

Recorded via [rrweb](https://github.com/rrweb-io/rrweb). Captures full visual replay data including:
- Full DOM snapshots
- Incremental DOM mutations
- Mouse movement and clicks
- Scroll events (throttled to 150ms)
- Input changes (last value only)
- Focus/blur events

All text inputs are masked by default for privacy. With `privacy: 'strict'` all page text is masked as well (layout, clicks and scrolls are kept).

### Network events

| Event type | Data captured |
|------------|--------------|
| `network` | Resource timing: URL, duration, transfer size, initiator type |
| `network:fetch` | Fetch requests: URL, method, status code, duration |
| `network:fetch:error` | Failed fetch: URL, method, error message, duration |
| `network:xhr` | XHR requests: URL, method, status code, duration |

The SDK does not record its own requests: report delivery to the capture endpoint, presence beacons (`…/v1/ingest/presence`, or your `presence.endpoint`) and evidence uploads (`…/v1/evidence/…`). Other requests your app makes to the same host are recorded.

### Console events

| Event type | Data |
|------------|------|
| `console:log` | `{ level: "log", args: string[] }` |
| `console:warn` | `{ level: "warn", args: string[] }` |
| `console:error` | `{ level: "error", args: string[] }` |
| `console:info` | `{ level: "info", args: string[] }` |
| `console:debug` | `{ level: "debug", args: string[] }` |

Arguments are JSON-serialized with a 2,000-character limit per argument.

### Error events

| Event type | Data |
|------------|------|
| `error` | `{ message, filename, lineno, colno, stack }` |
| `error:unhandledrejection` | `{ message, stack }` |

### Rage click events (`rage-click`)

```typescript
{ x: number, y: number, clicks: number, target: string, url: string, innerText: string }
```

Sensitivity presets:

| Preset | Clicks | Window | Radius | Debounce |
|--------|--------|--------|--------|----------|
| `very-sensitive` | 2+ | 1500ms | 50px | 1s |
| `medium` | 3+ | 1000ms | 30px | 2s |
| `low` | 4+ | 800ms | 20px | 3s |

## Event structure

All events share this shape:

```typescript
interface BugTapeEvent {
  type: string;       // Event type identifier
  timestamp: number;  // Unix timestamp in milliseconds
  data: unknown;      // Event-specific payload
}
```

## Ring buffer behavior

- Events are stored in a time-windowed buffer (default: 10 minutes).
- When the event count exceeds `maxEvents`, the oldest 20% of events are evicted.
- PII scrubbing is applied at capture time (before events enter the buffer).
- Calling `getEvents()` automatically prunes expired events.

## Bug report structure

When a user submits via the widget, the SDK builds a `BugReport`:

```typescript
interface BugReport {
  title: string;
  description: string;
  severity: 'critical' | 'high' | 'medium' | 'low';
  screenshot: ScreenshotResult | null;
  audio: AudioResult | null;
  audioTranscript: string | null;
  aiSummary: BugSummary | null;
  events: BugTapeEvent[];
  environment: {
    url: string;            // window.location.href
    userAgent: string;      // navigator.userAgent
    viewport: string;       // e.g., "1920x1080"
    timestamp: string;      // ISO 8601
    language: string;       // navigator.language
  };
}
```

## Ingest payload

The SDK transforms the `BugReport` into the API payload:

```typescript
{
  title: string;
  description: string;
  severity: string;
  url: string;
  userAgent: string;
  viewport: string;
  events: BugTapeEvent[];
  screenshot: ScreenshotResult | null;
  audio: AudioResult | null;
  aiSummary: BugSummary | null;
  platform: string;            // context.platform ?? 'web'
  // plus context fields: reporterEmail, reporterId, sessionId, userId, release, environment, metadata
}
```

Submitted via `POST` to the configured `endpoint` with headers:
- `Content-Type: application/json`
- `X-BugTape-Key: <apiKey>`

Legacy screenshot/audio payload fields are not proof of retrievable media storage. See the [ingest contract](./ingest-schema.md) for the current storage boundary.

When the page already runs Datadog RUM, `metadata.datadog` carries the RUM ids from `DD_RUM.getInternalContext()`: `rum_session_id`, `rum_view_id` and `rum_application_id`. The view URL and name are not read, and BugTape never loads Datadog. If your app sets `metadata.datadog` itself, the SDK leaves it as is. See [Use BugTape with Datadog](./datadog.md).

## Ingest response

```typescript
interface IngestResponse {
  id: string;              // UUID — stable report ID
  fingerprint: string;     // 16-char hex dedup fingerprint
  deduplicated: boolean;   // true if matched an existing report
  regression: boolean;     // true if a resolved bug reoccurred
  is_regression: boolean;  // alias for regression
  warnings?: Array<'release_missing'>; // no release: BugTape cannot tell a regression from old noise
  schemaVersion?: 1;
  captureId?: string;      // stable capture acknowledgement when schema v1 is used
  occurrenceId?: string;   // required UUID in a schema v1 acknowledgement
  receivedAt?: string;
}
```

## Submission behavior

1. **Default flow**: SDK POSTs the report to the ingest endpoint.
Schema v1 submissions require a matching `captureId` and valid `occurrenceId` before the SDK invokes `onSuccess` or removes a queued report. Missing, mismatched, malformed or oversized receipts remain retryable. Response bodies are capped at64KiB, with the existing10-second deadline and stream cancellation. Self-hosted servers must support the v1 receipt before upgrading this SDK.

2. **Retry**: Retryable transport/server failures get one retry after a 1-second delay. Permanent validation/auth failures are not queued.
3. **Offline fallback**: Eligible failed captures can be stored in a bounded local retry queue when storage is available. Retry retains the original destination, credential scope and capture ID. `onError` reports whether the capture was saved; do not assume every failure was persisted.
4. **Custom submit**: If `onSubmit` is provided, it replaces the default submission. You can call the API yourself or route reports elsewhere.

## Client-detail tokens

Seal client details that your team needs to read later, so BugTape stores them but cannot read them.

```html
<td data-bugtape-token>Anna Smith</td>
```

```javascript
import { init, token } from 'https://app.bugtape.ai/bugtape.mjs';
init({ apiKey: 'bt_live_…', tokenize: { publicKey: 'btpk1_…' } });
const placeholder = token('Anna Smith'); // '⟦bt:1⟧'
```

- Text inside `[data-bugtape-token]` elements is replaced in the replay, click breadcrumbs and page headings by a placeholder (`⟦bt:N⟧`). The same value keeps the same placeholder on one page.
- `token(value)` returns the placeholder for a value you pass yourself (context, metadata, `identify({ name })`). Do not tokenize `userId`, `reporterEmail` or `email`: they count who a bug reached and drive notifications. Keep placeholders out of error messages and titles: they change between page loads, so they would split one issue into many.
- On submit, the values a report uses are sealed into `metadata.bugtapeTokens` with the public key: ECDH P-256, HKDF-SHA256 and AES-256-GCM, with a fresh key per report. The server keeps the sealed vault and returns it as `capture.clientTokens` from `GET /v1/bugs/:id/events?occurrenceId=`.
- The console opens the vault in the browser with the private key (`btsk1_…`), held in `sessionStorage` for that tab. Make a key pair in **Settings → Client details**.
- Without `tokenize.publicKey`, marked text is masked (`*` in the replay, `▇` from `token()`), never sent as it is.
- A lost private key cannot be recovered. Placeholders in reports sealed with it stay closed.
- Strict mode still masks all other page text and keeps placeholders, so the two work together.

## Encryption limitation

End-to-end encryption of whole reports is not supported. Setting `encryptionKey` causes submission to fail explicitly; use [client-detail tokens](#client-detail-tokens) to seal specific values. The API does not provide a retrievable encrypted-artifact/decryption path. Do not use this option or claim private ciphertext storage.

## PII scrubbing

Enabled by default. Scrubs the following patterns from all event data at capture time:

- Email addresses
- Credit card numbers (validated with Luhn algorithm)
- US Social Security numbers
- Phone numbers
- JWTs
- API keys / bearer tokens
- IPv4 addresses

Configure with `pii: { enabled: false }` to disable.

**Before a capture key on client-data UI:** complete the [privacy proof checklist](./privacy-proof-checklist.md) and run the [privacy fixture](./examples/privacy-fixture/). Default regex scrub is not enough for named clients in table text — mark containers with `data-bugtape-ignore` / `.bugtape-ignore` / `.rr-block`, set `privacy: 'strict'` (all page text masked in the browser), or set `disableDomRecording: true` / use server-only ingest.

## AI summaries

The SDK fetches a short AI-generated summary when the bug report panel opens. The summary contains:

- A suggested title
- A severity assessment (`critical` / `high` / `medium` / `low`)
- A 1–3 sentence description of what happened

The summary is included in the submitted report alongside the user-provided title and description.

### AI Summary (server-side)

This is the default path. The SDK sends the last ~300 events to BugTape's `/v1/ingest/ai-summary` endpoint, which calls the workspace's configured AI provider (OpenAI, Anthropic, or Google) server-side and returns the summary.

No additional client configuration is required — it uses the same `apiKey` as ingest. If the workspace has no AI provider configured, the server responds with `503` and the SDK silently falls back to submitting without a summary; the bug report is still filed.

Set `summarize: false` to disable the AI summary request entirely.

### Provider credentials

Omit the deprecated `openaiKey` option. Keep provider credentials on the server; do not place them in browser code or `NEXT_PUBLIC_*` variables. Use `summarize: false` if you do not want the summary request.

## Error boundaries

All SDK recorders are wrapped in error boundaries. If a recorder fails (e.g., rrweb throws on an unusual DOM), the SDK logs a warning and continues. Test the integration against your own app; this does not guarantee that every host interaction is harmless.
