Integrate an SDK · Web

Web & React SDK

A framework-independent browser SDK, thin React helpers, and a Playwright runner the CLI launches per replay attempt. Capsules carry sdk.name: @bugcapsule/web and runtime.name: web.

Package (0.3.0, ESM only)Entry pointsWhat it is
@bugcapsule/web., ./coreThe SDK, no dependencies. /core is the environment-neutral model, codec, redaction and request-key core (also used in Node).
@bugcapsule/react., ./react-routerHooks and components. Peers: @bugcapsule/web 0.3.0 (exact), react ≥18, react-router ≥6.4 (optional)
@bugcapsule/web-replaybin bugcapsule-web-runnerNode ≥20 + playwright-core 1.63 runner. Dev dependency, only where you run bugcapsule run|verify

1. Install

!

The packages are not published to npm yet (UNLICENSED; publishing happens after review). Build tarballs from the SDK checkout and install those:

cd bug_capsule_web_sdk
npm install && npm run build && npm run pack:all     # dist-packages/*.tgz

cd ../your-app
npm install ../bug_capsule_web_sdk/dist-packages/bugcapsule-web-0.3.0.tgz \
            ../bug_capsule_web_sdk/dist-packages/bugcapsule-react-0.3.0.tgz
npm install -D ../bug_capsule_web_sdk/dist-packages/bugcapsule-web-replay-0.3.0.tgz
# Once published: npm install @bugcapsule/web @bugcapsule/react; npm install -D @bugcapsule/web-replay

2. Initialize before rendering

import { BugCapsule, networkIntegration, historyIntegration, localStorageIntegration } from '@bugcapsule/web';

await BugCapsule.init({
  appVersion: '1.4.0',
  appBuild: '52',
  mode: 'qa',                                   // DEFAULT IS 'production' (10% sampled) — set it
  gitSha: import.meta.env.VITE_BUGCAPSULE_GIT_SHA,
  backend: { url: 'https://api.bugcapsule.com', ingestKey: 'bci_…' }, // never a bcs_ secret key
  integrations: [
    networkIntegration({ rules: [{ method: 'POST', path: '/api/checkout', ignoreBodyFields: ['timestamp', 'nonce'] }] }),
    historyIntegration(),
    localStorageIntegration({ allowPrefixes: ['shop.'] }),
  ],
  allowReplay: import.meta.env.DEV || import.meta.env.VITE_BUGCAPSULE_ALLOW_REPLAY === 'true',
});

renderApp();

3. Get your origin allowed

Browsers only let the SDK reach BugCapsule from origins your project allows. Send your BugCapsule team the exact origins (scheme, host and port) your app runs on, for example:

https://app.example.com
https://staging.app.example.com
http://127.0.0.1:4317        # local QA builds

Until an origin is allowed, the browser console shows CORS errors and nothing is uploaded. The SDK never sends cookies to BugCapsule.

4. Make more bugs replayable

HookWhy
onClick={BugCapsule.action('cart.checkout', checkout)} + data-bugcapsule-action="cart.checkout"Semantic replay by id. The attribute is also the stable selector for click replay. Call action() after init.
BugCapsule.trackInput('login.email', inputEl)Typed text is replayed. Password inputs, data-bugcapsule-sensitive, { sensitive: true } and sensitive-looking ids record [REDACTED].
BugCapsule.check('cart_total_non_negative', total >= 0) / fail(id)Wrong behaviour that doesn't throw becomes a checkable failure
BugCapsule.reportError(e) / <BugCapsuleErrorBoundary>Errors your code or React catches never reach window.onerror
BugCapsule.registerState('cart', { serialize, restore })App-owned state (Redux, Zustand, a store) in checkpoints, restored in replay
indexedDbIntegration([{ name, snapshot, restore }])IndexedDB stores through adapters you write; the SDK never reads IndexedDB itself
BugCapsule.now() / date() instead of Date.now()Replay returns the recorded wall clock
BugCapsule.random() instead of Math.random()Seeded per session (mulberry32), same seed in replay

React

import { useBugCapsuleAction, useTrackedState, useBugCapsuleState,
         BugCapsuleErrorBoundary, BugCapsuleCaptureButton } from '@bugcapsule/react';
import { BugCapsuleRouteListener } from '@bugcapsule/react/react-router';

function Cart() {
  const [coupon, setCoupon] = useTrackedState('cart.coupon', '');
  const [cart, setCart] = useState(emptyCart);
  useBugCapsuleState('cart', cart, setCart);                  // checkpoint + restore
  const placeOrder = useBugCapsuleAction('cart.checkout', async () => { … });
  return <>
    <input value={coupon} onChange={(e) => setCoupon(e.target.value)} />
    <button data-bugcapsule-action="cart.checkout" onClick={() => void placeOrder()}>Checkout</button>
  </>;
}

<BrowserRouter>
  <BugCapsuleRouteListener />                         {/* route events with push/pop/replace */}
  <BugCapsuleErrorBoundary fallback={<Oops />}><Routes>…</Routes></BugCapsuleErrorBoundary>
  <BugCapsuleCaptureButton />                         {/* renders nothing in production or replay */}
</BrowserRouter>
ExportSignature
useBugCapsuleAction(id, handler) => handler
useTrackedInput(id, { sensitive? }) => ref callback for an uncontrolled input
useTrackedState(id, initial: string, { sensitive? }) => [value, setValue]
useBugCapsuleState(key, value, restore: (data) => void)
BugCapsuleErrorBoundaryfallback?: ReactNode | (error, reset) => ReactNode
BugCapsuleCaptureButtonlabel?, className?
BugCapsuleRouteListenerRender inside the router; combine with historyIntegration() for checkpoints

Integrations

Network: fetch and XMLHttpRequest

networkIntegration({
  fetch: true, xhr: true,                       // or fetchIntegration() / xhrIntegration()
  rules: [{ method: 'POST', path: '/api/orders/:id', ignoreBodyFields: ['nonce'] }],
  ignoreQuery: ['_', 'ts', 'timestamp', 'nonce', 'cachebust'],   // default
  maxBinaryBytes: 64 * 1024, maxKeyBodyBytes: 64 * 1024, maxBodyWaitMs: 30_000,
  exclude: ({ method, url }) => url.includes('/api/payments'),   // keeps method, URL, status, timing only
});

Storage, history, IndexedDB

localStorageIntegration({ allowPrefixes: ['shop.'], denyPrefixes: ['shop.token'] })   // allowPrefixes is required
sessionStorageIntegration({ allowPrefixes: ['checkout.'] })
historyIntegration({ sanitizeLocation: (l) => l.replace(/\/reset\/[^/]+/, '/reset/:token') })
indexedDbIntegration([{ name: 'offline-cart', snapshot: async () => readCart(), restore: async (d) => writeCart(d) }])

historyIntegration wraps pushState/replaceState and listens to popstate/hashchange. Cookies, DOM content, input values (except tracked inputs) and storage outside the allowlist are never captured.

Configuration reference

FieldDefaultNotes
appVersion, appBuildrequired
appId, appName, flavornullMax 200 chars
mode'production''debug' | 'qa' | 'production'
enabledtruefalse = no capture setup at all
backendnone{ url, ingestKey }; without it captures are saved locally only
gitSha, gitDirtynullMissing SHA or dirty tree → build.gitSha gap
stateSchemaVersion1
integrations[]A state integration is always appended
checkpointIntervalMs / windowMs / maxEvents30 000 / 300 000 / 20 000
productionSampleRate0.1Your project's sample rate can only lower it
autoCaptureOnFailure / maxCapsulesPerSessiontrue / 3Limit applies to auto-capture
captureNetworkBodies / maxBodyBytestrue / 256 KiB
redactKeys, redactHeaders, redactPatterns[]redactPatterns: RegExp[]
beforeRecord(type, data)—After built-in redaction; return null to drop
beforeCheckpoint(integration, data)—Return null/undefined to omit that snapshot
outboxLimits10 / 10 MiB / 7 days{ maxCount, maxBytes, maxAgeMs }
maxStaleConfigMs7 days
ignoreError(error)—Return true if not a failure
labels{}At most 100
capturePointerstrue
sensitiveSelector'[data-bugcapsule-sensitive]'Password inputs are always sensitive
allowReplayfalseWire to a dev/test build flag only
storageName'bugcapsule'IndexedDB database name
setupTimeoutMs5000

Methods

BugCapsule.action(id, handler)                    // wraps handler
BugCapsule.actionWithPayload(id, (payload) => …)
BugCapsule.trackInput(id, el, { sensitive })      // returns dispose()
BugCapsule.trackValue(id, set, { sensitive })     // { changed(value), dispose() } for custom widgets
BugCapsule.fail(id, message?); BugCapsule.check(id, condition, message?); BugCapsule.reportError(e)
BugCapsule.log(message, 'debug' | 'info' | 'warn' | 'error')
BugCapsule.route(location, source?, action?)      // explicit route events
BugCapsule.registerState(key, { serialize, restore }) // returns unregister; stateChanged(key)
await BugCapsule.capture({ note })                // CaptureResult | null
BugCapsule.now(); BugCapsule.date(); BugCapsule.random()
await BugCapsule.refreshRemoteConfig(); await BugCapsule.flushUploads()
await BugCapsule.pendingUploads(); await BugCapsule.savedCaptures()
BugCapsule.isInitialized; BugCapsule.isRecording; BugCapsule.isReplaying; BugCapsule.mode; BugCapsule.session

Replay with the CLI

{
  "runtime": "web",
  "web": {
    "url": "http://127.0.0.1:4317/",
    "installCommand": "npm ci",
    "buildCommand": "npm run build",
    "serveCommand": "npm run preview -- --port 4317 --strictPort",
    "env": { "VITE_BUGCAPSULE_ALLOW_REPLAY": "true" }
  }
}
bugcapsule run <id> -n 3                                  # builds (on the capsule's commit when pinned), serves, replays
bugcapsule verify <id> -n 3                               # builds the working tree
bugcapsule run <id> --app-url http://127.0.0.1:4317/      # against an already running build

The CLI passes BUGCAPSULE_GIT_SHA/BUGCAPSULE_GIT_DIRTY in the build environment; map them to your bundler's env (the shop demo's vite.config.ts maps them to VITE_BUGCAPSULE_GIT_SHA with define). The CLI refuses to start when something already serves web.url (it might be an old build).

Per attempt, bugcapsule-web-runner:

  1. Fetches /settings and /capsule from the CLI's loopback server.
  2. Launches Chromium with the recorded viewport, scale factor and locale, service workers blocked, a fresh browser context.
  3. Blocks every request to an origin other than the app's (and web.allowOrigins) and reports network.escaped.
  4. Restores localStorage/sessionStorage in an init script before any page script, exposes the Playwright bindings, and opens the checkpoint's location.
  5. The app's own SDK (with allowReplay: true) runs the engine: real pointer input through Playwright at recorded coordinates (hit-tested against the recorded target), semantic actions, tracked inputs, synthetic lifecycle; fetch/XHR served from the capsule.
  6. Posts the report to /result and prints a BUGCAPSULE_RESULT:: line. Default timeout 120 s.
✓

Production protection. Replay activates only when allowReplay: true and the page was opened by the runner. With allowReplay false nothing (URLs, storage, messages, scripts) can turn replay on. A script already in a page with allowReplay: true could imitate the runner bindings, one more reason to keep such builds out of production.

Uploads and storage

Shop demo

examples/shop: React app with four planted bugs (race, legacy cart, discontinued item, coupon) and a local API (api-server.mjs, port 4318). Environment: VITE_BUGCAPSULE_BACKEND_URL, VITE_BUGCAPSULE_INGEST_KEY, VITE_BUGCAPSULE_ALLOW_REPLAY, VITE_BUGCAPSULE_DEMO_FIX (race|legacy|discontinued|coupon), VITE_SHOP_API_URL.

node examples/shop/api-server.mjs                       # shop API on 127.0.0.1:4318
cd examples/shop
VITE_BUGCAPSULE_BACKEND_URL=https://api.bugcapsule.com VITE_BUGCAPSULE_INGEST_KEY=bci_… \
VITE_BUGCAPSULE_ALLOW_REPLAY=true npx vite build && npx vite preview --port 4317 --strictPort

Browsers and limitations

BrowserCaptureReplay
Chromium / Chrome / Edgetestedtested
Firefox, Safaristandard APIs, untestednot implemented