SDK API reference
@sonderhq/sdk 0.1.0 · pure ESM · zero runtime dependencies ·
TypeScript types included. This page documents the SDK as built; the samples are
typechecked against it in CI.
initSonder(options)
Creates the client, starts autocapture and the flush timer, and exposes the instance
as window.sonder. Use createSonderClient(options) for the
same client without the global.
import { initSonder } from "@sonderhq/sdk";
export const sonder = initSonder({
// Required. The workspace write key: client-safe, write-only.
writeKey: "snd_pk_your_write_key",
// Release stamping: how Sonder knows which deploy a session ran, so issues
// can auto-resolve after your fix ships and auto-regress if it comes back.
buildId: "web@2026-07-03.1",
// Transport tuning. Defaults shown.
flushIntervalMs: 5000,
maxBatchSize: 25,
// Keep a fraction of events on very high-traffic pages. Default 1 (all).
sampleRate: 1,
// Set false to disable autocapture and send only explicit track() calls.
autoCapture: true,
// Self-hosted or region-pinned collector. Defaults to Sonder cloud.
ingestUrl: "https://ingest.sonder.so/ingest"
});
| Option | Type · default | Notes |
|---|---|---|
writeKey | string · required | Throws if missing. Client-safe, write-only. |
ingestUrl | string · Sonder cloud | Any collector implementing POST /ingest. |
buildId | string · sonder-js@0.1.0 | Release stamp per deploy; powers confirm/regress. |
flushIntervalMs | number · 5000 | Also flushes on beforeunload. |
maxBatchSize | number · 25 | Buffer flushes early at this size. |
sampleRate | number · 1 | Per-event keep probability, 0 to 1. |
autoCapture | boolean · true | false: only explicit track/capture calls. |
debug | boolean · false | Reserved. |
The client
| Member | What it does |
|---|---|
sessionId | Random UUID per browser tab, persisted in sessionStorage (sonder.session_id). Not a cookie; never cross-site. |
track(name, properties?, target?) | Custom event. Properties are masked recursively before buffering. |
identify(personId, traits?) | Attaches a person reference to the session. The ID itself is passed through PII masking (an email used as an ID becomes [masked]: prefer opaque IDs). Traits are masked. |
capture(event) | Low-level: buffer a fully-specified SonderEvent. |
flush() | Encode the buffer as a ribbon and send it now. Returns a promise. |
destroy() | Stop timers, disconnect observers, restore fetch/History patches, remove listeners. |
What autocapture records
| Event | When |
|---|---|
view | Once at init, with the current route. |
click | Any click, attributed to the nearest meaningful element (button, a, input, select, textarea, [role], [data-sonder-name], form). |
input | On change. The value is always [masked]; only the field's label/role/section are recorded. |
submit | Form submissions. |
nav | SPA route changes (pushState/replaceState/popstate), parametrized. |
rageclick | 3+ clicks on the same target within 700 ms. Carries a state probe. |
dead_click | A click causing no DOM mutation, no navigation, and no network activity within 700 ms. Carries a state probe. |
error_click | An uncaught JS exception surfaced via window.onerror. |
identify | Explicit identify() calls. |
Element attribution
- Label, first match wins:
data-sonder-name→aria-label→ visible text →name/placeholder→ associated<label>→title→alt→ tag name. Truncated at 120 chars, masked. - Selector, stable-first:
data-sonder-name→data-testid→id→ role + label → tag. Never positionalnth-child. - Section: nearest
section[aria-label]/nav[aria-label], else the nearest heading of the enclosing section.
The state probe
Attached to rageclick and dead_click only:
"state_probe": {
"error_visible": true,
"error_text": "Email [masked] is invalid", // visible [role=alert] text, masked, ≤160 chars
"spinner_visible": false, // [aria-busy=true] / [data-loading=true]
"empty_state": false, // [data-empty=true]
"disabled_target": false, // was the clicked control disabled?
"near_text": "Billing" // nearest heading/state text, masked, ≤160 chars
}
Masking rules
- Patterns replaced with
[masked]in every string: emails, payment-card numbers, US phone numbers, SSNs, digit runs of 9+. - Strings containing raw HTML are replaced entirely.
- Object keys masked recursively regardless of value:
value, input, password, email, phone, card, credit_card, ssn, token, secret, authorization, cookie. data-sonder-mask: any element inside a marked subtree contributes[masked]to labels and state probes.- The ingest server applies the same rules again before storage.
Ribbon encoding and transport
- Consecutive identical events collapse into one with a
count(run-length encoding) at flush time. - Routes are parametrized: purely numeric and UUID-like path segments become
:id. - Small batches (≤5 encoded events) go via
navigator.sendBeaconso they survive page unload; larger batches usefetchwith theX-Sonder-Write-Keyheader. - Payload shape:
{ write_key, session_id, person_id, sdk_build, sent_at, events }.
All exports
| Export | Kind | Purpose |
|---|---|---|
initSonder, createSonderClient | function | Create the client (with/without the window.sonder global). |
maskText, maskJson | function | The exact masking the SDK applies; useful in tests. |
encodeRibbon | function | RLE-encode a list of events into a ribbon. |
parametrizeRoute | function | /settings/123 → /settings/:id. |
describeElement | function | Label/role/selector/section for a DOM element, as autocapture sees it. |
SonderOptions, SonderClient, SonderEvent, SonderTarget, RibbonEvent, SonderRole | type | The full public type surface. |
@sonderhq/sdk/react: SonderProvider, useSonder | component · hook | React context wrapper. React is an optional peer dependency. |
@sonderhq/sdk/next | re-export | Same provider with "use client" applied. |
The wizard
npx sonder-init <write_key> [--ingest-url <url>] [--board-url <url>]
Detects the framework, injects the init/provider files, writes
.sonder/config.json, POSTs a verification batch, and prints the ribbon
the collector accepted. Requires Node 18+.