Documentation

Browsonic SDK Documentation

Real-time error and event capture for JavaScript applications. The @browsonic/sdk package is the canonical surface — framework adapters (React, Vue, Svelte, Angular, Next.js, Astro, Remix) wrap it without changing the wire format.

Last updated:

Introduction

Browsonic SDK is a lightweight, zero-dependency JavaScript library that automatically captures frontend errors and anomalies in real time. It is published on the public npm registry as @browsonic/sdk under the Apache 2.0 licence.

What it captures

Console errors
console.error, warn, info
Unhandled exceptions
window.onerror events
Promise rejections
Unhandled promise rejections
Network failures
HTTP 4xx / 5xx responses

Key features

FeatureDescription
Zero dependenciesNo external libraries required.
Automatic collectionIntercepts console, errors, and fetch automatically.
Smart batchingGroups events and flushes at a configurable interval.
DeduplicationPrevents duplicates via fingerprinting.
Offline supportPersists the queue to localStorage when offline.
Privacy firstAutomatic redaction of sensitive keys.
Circuit breakerAuto-disables on repeated internal errors.

Installation

Install with your preferred package manager.

npm

Terminal window
npm install @browsonic/sdk

pnpm

Terminal window
pnpm add @browsonic/sdk

yarn

Terminal window
yarn add @browsonic/sdk

Quick Start

Get up and running in under two minutes.

Basic setup

import { getBrowsonic } from '@browsonic/sdk';
const sdk = getBrowsonic();
sdk.init({
apiEndpoint: 'https://api.browsonic.com',
appKey: 'your-app-key',
apiKey: 'pk_live_…', // mint in dashboard (required when trackPageViews is on, which is the default)
});
// That's it. Errors are now captured automatically.

React integration

src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { getBrowsonic } from '@browsonic/sdk';
import App from './App';
const sdk = getBrowsonic();
sdk.init({
apiEndpoint: import.meta.env.VITE_BROWSONIC_API,
appKey: import.meta.env.VITE_BROWSONIC_APP_KEY,
apiKey: import.meta.env.VITE_BROWSONIC_API_KEY,
environment: import.meta.env.MODE,
clientVersion: import.meta.env.VITE_GIT_SHA,
debug: import.meta.env.DEV,
});
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

Framework-specific adapters (@browsonic/react, @browsonic/vue, @browsonic/svelte, @browsonic/angular, @browsonic/nextjs, @browsonic/astro, @browsonic/remix) add router instrumentation and navigation breadcrumbs on top of the core SDK. See the framework adapter guides for install and quick-start code for each one.

Configuration

Every option init() honours on @browsonic/sdk 3.27, grouped by purpose. Defaults are the SDK's own; the server can lower sampleRate and the replay ceiling at runtime, never raise them.

Required

ParameterTypeDescription
apiEndpointstringBase URL of your Browsonic API.
appKeystringApplication identifier for multi-tenant routing.
apiKeystringTenant API key — sent as X-API-KEY. Required when trackPageViews is on (the default).

Environment & release

ParameterTypeDefaultDescription
environmentstring"production"Environment name shown with every event.
releasestring | nullnullRelease / build identifier of the host app — the value source maps are uploaded under and symbolication resolves against.
clientVersionstring | nullnullClient version tag (surfaced as Versions in the dashboard).
debugbooleanfalseVerbose console logging.

Sampling & consent

ParameterTypeDefaultDescription
sampleRatenumber0.1Head-based session sampling for non-error events (0–1). Errors and fatals are always sent; page views, Web Vitals and breadcrumb-only events ship from the sampled share of sessions. The server can push a lower rate.
minSampleRatenumber0Floor (0–1) below which the server-pushed sample rate cannot drive the effective rate.
pageViewSampleRatenumber1Sample rate for non-initial page views on high-traffic SPAs; the first page view is always sent.
respectGPCbooleantrueHonour Global Privacy Control: when the browser signals it, the visitor ID falls back to a fresh, unlinkable UUID per call.
hasConsented() => booleannullHost-supplied consent gate. Until it answers, events are held (page views up to 10 s); only an explicit false drops them.
visitorIdStrategy'cookie' | 'localStorage' | 'session' | 'none'"session"Where the visitor ID lives: "cookie" (1-year host-only cookie), "localStorage", "session" or "none".

Page views & App Atlas

ParameterTypeDefaultDescription
trackPageViewsbooleantruePage-view events that power the App Atlas route map. Needs apiKey.
atlasbooleanfalseThe one switch that turns App Atlas on: page views, screen titles, navigation triggers and the task lifecycle.
normalizePageViewRoutesbooleantrueCollapse page-view URLs to a stable route key at source (/users/123/users/:id; query and fragment dropped).
pageViewRoutePatternsArray<{ test: RegExp; replace: string }>[]Declarative route rewrites ({ test, replace }) applied to the raw URL before generic normalisation.
beforeSendPageView((url: string) => string | null) | nullnullFinal hook on a page-view route after normalisation and redaction; return a string to override, null to drop.
hashRoutingbooleanfalseTreat the URL fragment as the route for hash-router apps (#/users/123/users/:id).
manualPageViewsbooleanfalseKeep the collector live for trackPageView(route) but do not auto-fire page views.
captureNavTriggersbooleantrueAttach the accessible name of the click or submit that caused each route change.
captureDocumentTitlebooleantrueSend document.title (redacted, 120 chars) with page views so Atlas can name screens.

Timeline & network

ParameterTypeDefaultDescription
includeTelemetrybooleantrueAttach the telemetry timeline (console, network, navigation, interactions) to errors.
captureLevelsEventLevel[]["error"]Console levels captured as events.
captureXHRbooleantrueCapture XMLHttpRequest in addition to fetch.
networkTelemetrybooleantrueRecord successful network requests as timeline entries (failures are always recorded).
trackNavigationbooleantrueRoute-change breadcrumbs in single-page apps.
trackVisitorbooleanfalseClick / input interaction breadcrumbs. Off for privacy; values are never captured, only element identity, type, length and pattern.
visitorobject{ click: true, input: true }Which interactions to record when trackVisitor is on.
captureStorageobject{ local: false, session: false }Snapshot localStorage / sessionStorage alongside an error; optional keys allow-list.
captureCookieValuesbooleanfalseInclude cookie values (names are always visible). A built-in session / CSRF cookie blocklist applies regardless.
sessionContextobject{ allowKeys: [] }App-defined session-level context with an explicit allowKeys egress policy — an allow list, not a deny list.

Redaction

ParameterTypeDefaultDescription
redactPatternsRegExp[][]Extra PII regular expressions applied to outgoing messages, URLs and referrers, on top of the built-in e-mail / phone / national-ID / card rules.
redactKeysstring[]built-in setExact key names redacted from storage, cookies and user context (extends the built-in set).
redactKeyPatternsstring[]["token", …]Key-name patterns matched on token boundaries, the slow-path fallback to redactKeys.
redactCookieNamesstring[][]Cookie names whose values are always redacted.

Web Vitals, replay & widget

ParameterTypeDefaultDescription
trackWebVitalsbooleanfalseCollect LCP / FCP / CLS / TTFB / FID / INP from config — the route for a script-tag host that cannot import webVitalsPlugin().
disableReplaybooleanfalseHard kill-switch: sessionReplayPlugin() never starts, whatever the plan allows.
widgetRulesWidgetRule[][]Client-side in-app prompt rules, evaluated in the browser.
widgetRulesEndpointstring | booleanfalseFetch server-defined widget rules: true for the default path, or a URL.
widgetPositionWidgetPosition"bottom-right"Corner the in-app widget renders in.
cspNoncestringnullNonce attached to the widget's shadow-root <style> under a strict Content Security Policy.

Transport & diagnostics

ParameterTypeDefaultDescription
compressbooleanautoGzip event batches via CompressionStream when the server says it decodes them; false never, true always.
persistQueuebooleanfalsePersist the offline event queue to localStorage across reloads. Dismissal sends that miss the keepalive budget are parked and retried regardless.
internalDiagnosticsbooleanfalsePOST the SDK's own metrics (init and flush latency, queue depth, drop reasons) to /v1/diagnostics.
captureAsyncStack'manual' | 'global' | falsefalseLonger async stack traces: "manual" wraps what you hand it; "global" is deprecated.

Hooks

ParameterTypeDefaultDescription
onError(event: BrowsonicEvent) => boolean | voidnullCalled before an error is reported; return false to suppress, or mutate the event.
onErrorStorm(phase: 'enter' | 'exit', count: number) => voidnullCalled once when the SDK enters storm mode (too many errors in a short window) and once when it exits.
onUnsupportedVersion(minVersion: string, currentVersion: string) …nullCalled when the server says this SDK version is below its minimum.
onIngestRejectedobjectnullCalled when ingest permanently refuses a batch (a 4xx other than 429); those events are dropped.

Ignore rules & guards

ParameterTypeDefaultDescription
ignoreExtensionsbooleantrueIgnore errors thrown from browser-extension code.
ignoreScriptErrorsbooleantrueIgnore opaque cross-origin "Script error" messages.
ignorePatternsstring[][]Stack-trace substrings that suppress an error.
ignoreMessagesstring[][]Error-message patterns that suppress an error.
ignoreUrlsstring[][]URL patterns whose errors are ignored.
abortInExtensionContextbooleanfalseRefuse to initialise inside a browser-extension context.
abortForBotsbooleanfalseRefuse to initialise for known bot user agents.
botPatternsreadonly string[]built-in listCustom bot user-agent fragments; replaces the built-in list.

Accepted, but no longer configurable

Since SDK 3.25.0 these keys are internal constants. init() still accepts them for compatibility, names the ones you set once on the console, and ignores them — each was a tuning number no deployment could get right (a 1 ms flush interval or a batch size of 0 used to switch error tracking off with a single console line as the only sign):flushIntervalMs, requestTimeoutMs, maxBatchSize, cooldownMs, maxPayloadBytes, errorStormThreshold, errorStormWindowMs, errorStormCooldownMultiplier, internalDiagnosticsIntervalMs, maxStackFrames, maxQueueSize, maxValueLength, maxTelemetryEntries.

Complete example

import { COMMON_THIRD_PARTY_PATTERNS } from '@browsonic/sdk';
sdk.init({
// Required
apiEndpoint: 'https://api.browsonic.com',
appKey: 'my-production-app',
apiKey: 'pk_live_…',
// Environment
environment: 'production',
clientVersion: 'v2.1.0',
debug: false,
// Batching
flushIntervalMs: 10000,
maxBatchSize: 25,
// Deduplication
cooldownMs: 60000,
// Capture
captureLevels: ['warn', 'error'],
// Queue
maxQueueSize: 300,
persistQueue: true,
// Privacy
redactKeys: ['token', 'password', 'secret'],
// Ignore Rules (filter noise)
ignoreExtensions: true,
ignoreScriptErrors: true,
ignorePatterns: COMMON_THIRD_PARTY_PATTERNS,
ignoreMessages: ['ResizeObserver loop'],
});

API Reference

All methods available on the SDK instance.

captureMessage(message, level?)

Capture a message manually. level defaults to info.

sdk.captureMessage('User completed checkout', 'info');
sdk.captureMessage('Payment failed', 'error');

captureError(error)

Capture an Error instance manually.

try {
await riskyOperation();
} catch (error) {
sdk.captureError(error as Error);
}

setUser(user) · clearUser()

Attach a user context to all subsequent events. Sensitive keys (token / password / secret / auth) are redacted before transport.

sdk.setUser({
id: 'user-123',
email: 'user@example.com',
plan: 'premium',
});
// On logout
sdk.clearUser();

addMetadata(key, value) · removeMetadata(key) · clearMetadata()

Attach custom key/value pairs to every subsequent event.

sdk.addMetadata('feature', 'checkout');
sdk.addMetadata('cartValue', 99.99);
sdk.addMetadata('isNewUser', true);
sdk.removeMetadata('feature');
sdk.clearMetadata();

identify(subscriber) · clearIdentity()

Declares which subscriber this browser belongs to — the person behind the account — and keys the cross-app user journey (Professional).setUser describes the account in your own system;identify names the person.

trackPageView(route?, options?)

Reports a page view, optionally with a route template such as/users/:id. Automatic with trackPageViews; use it with manualPageViews: true when your router owns navigation.

addBreadcrumb(breadcrumb) · setTag(key, value) · setContext(name, ctx) · setExtra(key, value)

Attach a manual timeline entry, an indexed key/value, a named object of structured context, or a free-form value to every subsequent event. Each has a matching remove… / clear….

newSession() · getSessionHealth() · markSessionCrashed()

Sessions end after 30 minutes without telemetry, on destroy()and on clearIdentity(); newSession() ends one in place. getSessionHealth() answers 'ok','errored' or 'crashed';markSessionCrashed() declares the session unrecoverable.

enterCriticalPath(options) · exitCriticalPath() · isInCriticalPath()

Marks a business-critical flow (checkout, sign-up) so errors inside it are weighted and surfaced as such.

register(plugin) · updateConfig(partial)

register adds an opt-in plugin and must be called beforeinit(). updateConfig changes config at runtime; keys a collector snapshots at install are refused with a console message.

Lifecycle: flush(), pause(), resume(), destroy()

MethodDescription
flush()Force immediate flush of queued events. Returns a Promise.
pause() / resume()Pause and resume event collection without losing the in-memory queue.
destroy()Destroy the SDK and release all listeners and timers.
getState()Returns 'uninitialized' | 'initializing' | 'running' | 'paused' | 'destroyed'.
getPendingCount()Number of events currently queued for transport.

Event Types

Built-in event types captured by the SDK.

TypeLevelSourceDescription
console_debuginfoconsole.debug()Debug log (verb preserved on telemetry).
console_infoinfoconsole.info()Informational log.
console_warnwarnconsole.warn()Warning log.
console_errorerrorconsole.error()Error log.
errorerrorwindow.onerrorUnhandled exception.
fatalfatalmanualCapture-on-purpose fatal report.
unhandledrejectionerrorPromise rejectionUnhandled promise rejection.
network_errorwarn / errorfetch() / XHRHTTP 4xx / 5xx response.

Wire format

interface BrowsonicEvent {
eventId: string; // Unique UUID
timestamp: string; // ISO 8601
type: EventType;
level: EventLevel;
message: string;
stack?: string | null;
context: EventContext; // url, referrer, pageAge
telemetry?: Timeline; // events leading up to error
metadata?: MetadataEntry[];
}
interface EventBatch {
batchId: string;
timestamp: string;
appKey: string;
environment: string;
clientVersion?: string;
sessionId: string;
sessionContext: SessionContext;
user?: UserContext;
events: BrowsonicEvent[];
}

Telemetry Timeline

Chronological events leading up to each error — the debugging context that turns a stack trace into a story.

Categories

Console
console.log, warn, error calls.
Network
Fetch & XMLHttpRequest requests.
Navigation
SPA route changes (History API).
Visitor
User clicks & inputs (privacy-safe).

How it works

  1. The SDK collects events in a ring buffer (FIFO, configurable size).
  2. When an error fires, the last N events ship with the error payload.
  3. The dashboard renders the timeline in the Timeline tab of each event.
  4. It answers the only question that matters: what happened right before this error?

Configuration

sdk.init({
apiEndpoint: 'https://api.browsonic.com',
appKey: 'my-app',
apiKey: 'pk_live_…',
// Telemetry Timeline
maxTelemetryEntries: 50, // ring buffer size (default 20)
includeTelemetry: true, // attach with errors (default true)
// Network telemetry
captureXHR: true,
networkTelemetry: true,
// Navigation
trackNavigation: true,
// Visitor (see Visitor Tracking)
trackVisitor: false, // OFF by default for privacy
});

Visitor Tracking

Track user interactions (clicks, inputs) in a privacy-safe manner.

Privacy-first design

Visitor tracking is off by default. When you enable it, actual input values are never stored — only patterns and lengths. Password fields are skipped entirely.

What is collected

DataExamplePurpose
Element tagbutton, inputIdentify element type.
Element text"Submit Order"Human-readable identification.
Element IDsubmit-btnIdentify specific element.
CSS classesbtn primaryDebugging context.
Input typeemail, textUnderstand input purpose.
Value length16Know input size without content.
Value patternemail, numericUnderstand input format.

What is never collected

  • Actual input values (e.g. "john@example.com").
  • Password field content — completely skipped.
  • Credit card numbers.
  • Any typed text content.

Configuration

sdk.init({
trackVisitor: true,
visitor: {
click: true, // track click events
input: true, // track input events
inputThrottleMs: 500, // throttle (default 500 ms)
},
});

Session Replay

Reconstruct what the user saw and did in the moments around an error — a DOM-level recording that plays back next to the stack trace. Session Replay is an opt-in plugin; it is not part of the default SDK.

Professional plan

Session Replay is a Professional-tier feature. On lower plans the plugin registers cleanly but the server withholds the recording entitlement, so nothing is captured. The effective sample rate is min(sampleRate × 100, plan ceiling %).

Setup

Import the plugin from the @browsonic/sdk/replaysub-path and register it before init(), exactly like any other plugin.

import { getBrowsonic } from '@browsonic/sdk';
import { sessionReplayPlugin } from '@browsonic/sdk/replay';
const sdk = getBrowsonic();
// Register the plugin BEFORE init().
sdk.register(sessionReplayPlugin());
sdk.init({
apiEndpoint: 'https://api.browsonic.com',
appKey: 'web',
apiKey: 'pk_live_…',
clientVersion: '1.0.0',
});

Masking defaults

Recording is privacy-first by default — matching Sentry parity. You have to loosen masking deliberately; you never have to remember to turn it on.

OptionDefaultEffect
maskAllTexttrueEvery text node is masked before it leaves the browser.
maskAllInputstrueInput values are masked. Password inputs stay masked even when this is false.
blockAllMediatrueImages, video and <canvas> render as sized placeholders.
sampleRate1Host-requested rate (0–1); clamped by the plan ceiling.
sdk.register(sessionReplayPlugin({
maskAllText: true, // default — mask every text node (Sentry parity)
maskAllInputs: true, // default — password inputs stay masked even if false
blockAllMedia: true, // default — images / video / canvas become placeholders
blockSelector: '.pii', // extra elements to block, rendered as a sized box
}));

Recordings are kept for 90 days on Professional and30 days on Enterprise, in a per-workspace store of5 GiB (Professional) or 10 GiB(Enterprise) — the numbers your workspace is on are shown underSettings → Session Replay. When the store passes 90 % the SDK is told to record a smaller sample, and at 100 % it stops recording rather than upload segments ingest would refuse; the dashboard says so on the Replay page.

The active sample ceiling is controlled from the dashboard underSettings → Session Replay — there is no server-bypassing replay switch in the browser.

Web Vitals

Report Core Web Vitals from real sessions. Like Session Replay, it is an opt-in plugin: the collector is a small native PerformanceObserver, but it changes what the SDK reports, so you turn it on consciously.

Setup

webVitalsPlugin is exported from the main@browsonic/sdk entry. Register it beforeinit().

import { getBrowsonic, webVitalsPlugin } from '@browsonic/sdk';
const sdk = getBrowsonic();
// Register BEFORE init(). Not in the default plugin set —
// you opt in explicitly.
sdk.register(webVitalsPlugin());
sdk.init({
apiEndpoint: 'https://api.browsonic.com',
appKey: 'web',
apiKey: 'pk_live_…',
clientVersion: '1.0.0',
});

What it collects

MetricNameMeasures
LCPLargest Contentful PaintLoading — when the largest element renders. Attribution: the element and the resource URL behind it.
INPInteraction to Next PaintResponsiveness — latency of user interactions. Attribution: the target and the input / processing / presentation phases.
CLSCumulative Layout ShiftVisual stability — unexpected layout movement. Attribution: the element that shifted.
FCPFirst Contentful PaintLoading — when the first text or image renders.
TTFBTime to First ByteServer and network — with the DNS / connect / request / response breakdown.
FIDFirst Input DelayResponsiveness of the first interaction (kept for older browsers; INP is the primary metric).

Each sample carries the route it was measured on, and since SDK 3.27 the attribution above plus long-animation-frame breadcrumbs, so a slow score points at a component rather than a page.

Samples surface in the dashboard on the Performance page. A per-session cap (maxSamples, default 30) guards against runaway observers in long-lived SPAs.

Source Maps

Ship minified bundles and still read original TypeScript in every stack frame. @browsonic/build-tools provides bundler plugins for Vite, Webpack, Rollup and esbuild that emit and upload source maps to Browsonic at build time.

Professional plan

Symbolication of uploaded maps is a Professional-tier feature. The upload runs on any plan; symbolicated frames appear once the tenant is entitled.

release must equal clientVersion

The release you upload maps under has to match theclientVersion you pass to init()byte-for-byte — that string is the only key the service uses to pair a runtime error with its map. If they drift, frames stay minified.

Vite plugin

Emit hidden source maps (generated, uploaded, but not referenced from the shipped bundle) and add the plugin. Webpack, Rollup and esbuild use the matching sub-path (@browsonic/build-tools/webpack, /rollup,/esbuild).

vite.config.ts
import { defineConfig } from 'vite';
import { browsonicSourceMaps } from '@browsonic/build-tools/vite';
export default defineConfig({
// Emit hidden maps: symbolication works, but the map URL is
// NOT referenced from the shipped bundle.
build: { sourcemap: 'hidden' },
plugins: [
browsonicSourceMaps({
appKey: 'web',
release: '1.0.0', // MUST equal the SDK's clientVersion
// token: process.env.BROWSONIC_SOURCEMAP_TOKEN (default)
baseUrl: 'https://api.browsonic.com',
}),
],
});

Upload token

Uploads authenticate with a dedicated source-map token. It is abuild-time secret — read fromBROWSONIC_SOURCEMAP_TOKEN when thetoken option is omitted. Never inline it into browser code. If the token is missing, the plugin prints a warning and skips the upload rather than failing the build.

Terminal window
# Build-time secret — mint under Settings → Source Maps in the
# dashboard. Never expose this in the browser bundle.
export BROWSONIC_SOURCEMAP_TOKEN=bsm_…

Privacy & Security

How Browsonic handles sensitive data and what compliance posture you inherit by default.

Data collection summary

Data typeCollectedHow it is stored
Error messages✅ YesFull text.
Stack traces✅ YesFull text.
Console logs✅ YesFull text (telemetry).
Network URLs✅ YesFull URL — no body / headers.
User clicks⚠️ OptionalElement info only.
User inputs⚠️ OptionalPattern + length only.
Session Replay (DOM)⚠️ Opt-in pluginReconstructed DOM + interactions; text, inputs and media masked by default.
App Atlas screenshots⚠️ Opt-inRendered from masked replay data; masked titles are rejected, not un-masked.
Input values❌ NeverN/A.
Passwords❌ NeverCompletely skipped.

Privacy-safe defaults

SettingDefaultDescription
trackVisitorfalseVisitor tracking off by default.
trackNavigationtrueURL changes only, no user data.
networkTelemetrytrueURLs only — no request / response bodies.
captureAsyncStackfalsePerformance opt-in feature.

Automatic redaction

These keys are automatically redacted inlocalStorage, sessionStorage, cookies, and user context:

const DEFAULT_REDACT_KEYS = [
'token', 'password', 'authorization',
'secret', 'key', 'credential', 'auth',
];
sdk.init({
redactKeys: [
...DEFAULT_REDACT_KEYS,
'ssn',
'creditCard',
'bankAccount',
],
redactCookieNames: ['session_id', 'csrf_token'],
});

Pattern-based redaction

In addition to key-based redaction, the SDK scans free-text values (error messages, telemetry payloads, captured network metadata) and replaces matches with [REDACTED]:

PatternWhat it catches
Emailname@host.tld tokens anywhere in a value.
JWTeyJ… three-segment Base64 tokens.
Credit card12–19 digit runs (with optional spaces / hyphens).
Opaque secret32-char-or-longer continuous [A-Za-z0-9_-] tokens.

HTTP header filtering

Network telemetry captures only headers on an explicit allowlist — everything else is dropped. A hard blocklist (authorization, cookie, set-cookie, token, api-key, password, …) is dropped even if a user tries to add it to the allowlist. Header values are also passed through pattern redaction as defense in depth.

Compliance

GDPR
Data minimization (patterns, not values). Purpose limitation (debugging only). Consent-based tracking (off by default).
CCPA
Transparent data collection. Opt-out via configuration. No sale of personal data.

For the full security policy — how to report a vulnerability, response targets, scope and the current state of npm release provenance — see the security page.

Troubleshooting

Common issues and how to resolve them.

SDK not capturing events

  1. Check initialization — sdk.getState() should return "running".
  2. Enable debug mode: sdk.updateConfig({ debug: true }).
  3. Verify captureLevels includes the event type you expect.

Events not reaching the server

  1. Check pending count — sdk.getPendingCount().
  2. Force flush — await sdk.flush().
  3. Verify apiEndpoint is correct and CORS is configured.
  4. Check the Network tab for 4xx / 5xx responses.

High event volume

  1. Increase cooldown — cooldownMs: 300000 (5 minutes).
  2. Reduce capture levels — captureLevels: ['error'].
  3. Increase flush interval — flushIntervalMs: 60000.