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"
});
OptionType · defaultNotes
writeKeystring · requiredThrows if missing. Client-safe, write-only.
ingestUrlstring · Sonder cloudAny collector implementing POST /ingest.
buildIdstring · sonder-js@0.1.0Release stamp per deploy; powers confirm/regress.
flushIntervalMsnumber · 5000Also flushes on beforeunload.
maxBatchSizenumber · 25Buffer flushes early at this size.
sampleRatenumber · 1Per-event keep probability, 0 to 1.
autoCaptureboolean · truefalse: only explicit track/capture calls.
debugboolean · falseReserved.

The client

MemberWhat it does
sessionIdRandom 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

EventWhen
viewOnce at init, with the current route.
clickAny click, attributed to the nearest meaningful element (button, a, input, select, textarea, [role], [data-sonder-name], form).
inputOn change. The value is always [masked]; only the field's label/role/section are recorded.
submitForm submissions.
navSPA route changes (pushState/replaceState/popstate), parametrized.
rageclick3+ clicks on the same target within 700 ms. Carries a state probe.
dead_clickA click causing no DOM mutation, no navigation, and no network activity within 700 ms. Carries a state probe.
error_clickAn uncaught JS exception surfaced via window.onerror.
identifyExplicit identify() calls.

Element attribution

  • Label, first match wins: data-sonder-namearia-label → visible text → name/placeholder → associated <label>titlealt → tag name. Truncated at 120 chars, masked.
  • Selector, stable-first: data-sonder-namedata-testidid → role + label → tag. Never positional nth-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.sendBeacon so they survive page unload; larger batches use fetch with the X-Sonder-Write-Key header.
  • Payload shape: { write_key, session_id, person_id, sdk_build, sent_at, events }.

All exports

ExportKindPurpose
initSonder, createSonderClientfunctionCreate the client (with/without the window.sonder global).
maskText, maskJsonfunctionThe exact masking the SDK applies; useful in tests.
encodeRibbonfunctionRLE-encode a list of events into a ribbon.
parametrizeRoutefunction/settings/123/settings/:id.
describeElementfunctionLabel/role/selector/section for a DOM element, as autocapture sees it.
SonderOptions, SonderClient, SonderEvent, SonderTarget, RibbonEvent, SonderRoletypeThe full public type surface.
@sonderhq/sdk/react: SonderProvider, useSondercomponent · hookReact context wrapper. React is an optional peer dependency.
@sonderhq/sdk/nextre-exportSame 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+.