@bugzar/sdk
Embeddable in-app QA session recorder for any React frontend. Drop in one component and capture a replayable bug report — rrweb DOM, console, network, and storage — with no browser extension.
npm install @bugzar/sdkUsage
import { Bugzar } from '@bugzar/sdk';
function App() {
return (
<>
<YourApp />
<Bugzar
onExport={async (blob, meta) => uploadToYourStorage(`qa/${meta.startedAt}.html`, blob)}
/>
</>
);
}A floating QA button appears in the bottom-right corner. Click to start
recording, interact with your app, then click again to stop. A self-contained
replay HTML is built and handed to onExport, which uploads it to your own
storage (S3/R2/…) — that URL is the shareable replay.
What it captures
- DOM — full rrweb recording, replayable with
rrweb-player - Console — every level (
log/info/warn/error/debug) + grouping, with stack traces - Network —
fetchandXMLHttpRequest(method, URL, status, headers, bodies, timing) - Storage —
localStorage/sessionStoragesnapshots (cookies are never captured) - Web Vitals — LCP / CLS / INP / TTFB
Privacy & redaction
Captured data is token-scrubbed at capture time, before it ever leaves the page:
- Inputs —
rrwebalways masks password fields; setmaskto mask all text inputs. - Network — request/response bodies have values under sensitive keys
(
password,token,authorization,secret,api_key, …) and any JWT-shaped value replaced with[REDACTED]; credential headers are masked. - Storage — values under sensitive keys, bare JWTs, and token sub-keys inside
JSON values (e.g. Supabase/Auth0
{ access_token, refresh_token }) are redacted. Cookies are never captured. - Console —
Bearertokens and embedded JWTs in log args are scrubbed. - App-state —
captureStatesnapshots get the same key/JWT masking, then yourredactState.
This is best-effort, not a guarantee: free-form bodies (custom RPC frames,
GraphQL with inline literals) can't be auto-redacted without false positives. For
full control, pass onBeforeUpload to scrub the whole bundle yourself before
it is uploaded. When self-hosting, report URLs are public-by-URL — treat them
accordingly.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
onExport |
(blob: Blob, meta: ExportMeta) => Promise<string | void> |
– | Receive the built self-contained replay HTML on stop / pick-finish. Upload it to your storage (S3/R2/…) and return the public URL. meta.mode is 'session' or 'design'. |
onStart |
() => void |
– | Called when recording starts. |
mask |
boolean |
true |
Mask all text inputs (passwords are always masked regardless). |
position |
'bottom-right' | 'bottom-left' | 'top-right' | 'top-left' |
'bottom-right' |
Toolbar corner. |
offset |
number | { x?: number; y?: number } |
20 |
Inset (px) from the anchored corner edges. A number sets both axes; { x, y } sets them independently (a missing axis falls back to 20). Applies to the toolbar and the review drawer. |
theme |
'light' | 'dark' | 'auto' |
'auto' |
Color theme. |
autoHide |
boolean |
false |
Tuck the toolbar off the anchored edge; it slides in only while the cursor is over the corner hoverZone, while in use, or for 2s after. Mouse-only. |
hoverZone |
{ width?: number; height?: number } |
{ width: 300, height: 30 } |
Size (px) of the invisible corner region you hover to reveal the auto-hidden toolbar — shrink it when the default zone overlaps your own UI. A missing axis keeps its default. Only used when autoHide is on. |
endpoint |
string | { url: string; headers?: Record<string, string> } |
– | Bugzar Worker base URL — the Jira backend only (auth + AI draft + server-side issue creation). Set together with jira to enable the review drawer. Use the object form to send auth headers on every request. |
onError |
(error: Error) => void |
– | Called if the upload fails (when endpoint is set). |
onBeforeUpload |
(bundle: ReportBundle) => ReportBundle | Promise<ReportBundle> |
– | Last-chance scrub of the bundle before upload. Strip secrets the built-in redaction misses. See Privacy & redaction. |
design |
boolean |
true |
Show the "Pick" button for design-feedback element annotation. |
onAnnotate |
(annotations: DesignAnnotation[]) => void |
– | Called with the picked elements + notes on Done (a notification — onExport still produces the design HTML). |
jira |
{ projectKey: string; clientId?: string; enabled?: boolean; defaultEpicKey?: string } |
– | Enable the review drawer (requires endpoint). With clientId it's per-user OAuth (filed as the reviewer); with enabled it uses the Worker's service account. The ticket links to the onExport URL. See Jira publishing. |
onPublished |
(result: { issueKey: string; issueUrl: string; stubbed: boolean }) => void |
– | Called after a publish attempt. stubbed === true means the Worker was unconfigured and no real issue was created — do not treat it as filed. |
captureState |
() => unknown |
– | Capture host app-state into the bundle's state timeline at start/stop/throttle. Each snapshot is serialized + redacted. |
redactState |
(state: unknown) => unknown |
– | Redact each state snapshot (runs after the built-in key/JWT masking). |
The bundle
interface ReportBundle {
events: RrwebEvent[]; // rrweb — replay with rrweb-player
console: ConsoleEntry[];
network: NetworkEntryPayload[];
storage: StorageSnapshotPayload[];
vitals: WebVitals; // lcp / cls / inp / ttfb
resources: ResourceTimingEntry[]; // Resource Timing waterfall (store-only)
state: StateSnapshot[]; // host app-state timeline (via captureState)
system: SystemInfo; // device/browser/environment snapshot (store-only)
meta: {
url: string; userAgent: string;
viewport: { width: number; height: number };
startedAt: number; endedAt: number; durationMs: number;
};
}All types are exported from the package.
Design feedback (element picker)
The toolbar's Pick button starts an in-page element picker — hover to
highlight, click to select, and add a note per element. On Done you get
structured annotations an AI agent can grep for:
<Bugzar
onAnnotate={(annotations) => {
annotations.forEach((a) => console.log(a.selector, a.componentName, a.note));
// e.g. "main > button.primary" "<SubmitButton>" "spacing looks off"
}}
/>interface DesignAnnotation {
id: string;
selector: string; // unique CSS selector
tagName: string;
textContent: string;
cssClasses: string;
rect: { x: number; y: number; width: number; height: number };
componentName?: string; // React component name, when detectable
note: string;
}Set design={false} to hide the Pick button. startDesignPick() is also
exported for programmatic use.
Web sharing — bring your own storage
onExport hands you the self-contained replay HTML (the full viewer + data
inlined). Upload it to any static host (S3, R2, GitHub Pages, …) and that URL is
the shareable replay — no Bugzar backend needed.
Just want a file to attach (no host)? The SDK ships downloadReplay — a
drop-in onExport that saves the HTML to disk: <Bugzar onExport={downloadReplay} />.
<Bugzar
onExport={async (blob, meta) => {
const key = `qa/${meta.mode}-${meta.startedAt}.html`;
await fetch(presignedPutUrl(key), { method: 'PUT', body: blob });
return publicUrl(key); // returning the URL lets a Jira ticket link to it
}}
/>onExport fires on recording stop and design-pick finish (meta.mode is
'session' or 'design'). The returned URL becomes the Jira ticket's replay link
when jira + endpoint are configured (below). The Worker (endpoint) is the
Jira backend only — it never hosts reports.
Running your own Jira backend? See Self-hosting guide — deploy the Cloudflare Worker (Workers AI + Jira) and wire the SDK in ~5 minutes.
Jira publishing (optional)
Set jira and endpoint to turn stop into a review drawer that files a
Jira issue for you:
<Bugzar
endpoint="https://bugzar-backend.<your-subdomain>.workers.dev"
jira={{ enabled: true, projectKey: 'BUGZAR', defaultEpicKey: 'BUGZAR-1' }}
onExport={async (blob, meta) => uploadToYourStorage(`qa/${meta.startedAt}.html`, blob)}
onPublished={({ issueKey, issueUrl, stubbed }) => {
if (!stubbed) window.open(issueUrl); // a real issue was filed
}}
/>On stop the bundle uploads, then the drawer opens with a read-only capture
summary (events · console errors · failed requests · LCP), an editable
Title / Description / Epic, and an AI polish button that drafts the issue
from the captured session. The Epic field resolves a full key
(CBPFE-3991), a bare issue number (3991), or a pasted Jira browse URL
(…/browse/CBPFE-3991). Publishing files the issue through the Worker's Jira
service account — the browser never holds an Atlassian token.
Requires a configured Worker + Jira service account. If the Worker is not configured, publish returns a
STUB-…placeholder: the drawer surfaces it as explicitly not a real issue (no clickable link) andonPublishedreceivesstubbed: true. Never treat a stubbed result as filed.
The Annotate button shares the same drawer in design-feedback mode — pick elements, annotate, and file a design issue the same way.
Per-user OAuth (file as the reviewer)
Pass jira.clientId (instead of / in addition to enabled) to switch the drawer
to per-user Atlassian OAuth: each reviewer connects their own account once and
the ticket is filed as them — no shared service account.
<Bugzar
endpoint="https://bugzar-backend.<your-subdomain>.workers.dev"
jira={{ clientId: 'YOUR_ATLASSIAN_OAUTH_CLIENT_ID', projectKey: 'BUGZAR' }}
/>On stop/finish the report uploads, then the drawer shows Connect Atlassian for
a first-time reviewer (a login popup; tokens are saved in localStorage for next
time). Once connected it shows the AI-drafted ticket + the connected account, and
File Jira ticket files it as that user. The secret never touches the browser —
only the public clientId is a prop; the token exchange runs in the Worker.
One-time setup (Atlassian admin + Worker owner):
- In the Atlassian developer console, create an OAuth 2.0 (3LO) app with the Jira
scopes and register the redirect URI:
https://bugzar-backend.<your-subdomain>.workers.dev/oauth/callback - Put the credentials on the Worker:
wrangler secret put ATLASSIAN_CLIENT_IDandwrangler secret put ATLASSIAN_CLIENT_SECRET. - Pass the same client id as
jira.clientId.
Reviewers must have permission to create issues in
projectKeyon a Jira site their account can access.
Headless engine (useBugzar)
Want your own "Report a bug" button instead of the floating toolbar? Drive the same start/stop/upload engine from a hook:
import { useBugzar } from '@bugzar/sdk';
function MyButton() {
const { recording, elapsed, start, stop } = useBugzar({ endpoint });
return (
<button onClick={recording ? stop : start}>
{recording ? `Stop (${elapsed}s)` : 'Report a bug'}
</button>
);
}How it works
<Bugzar /> mounts a floating toolbar into document.body via a React
portal (SSR-safe) and instruments the page only while recording — it patches
console, fetch/XHR, and storage, runs rrweb, and restores everything on
stop. The capture engine (@bugzar/capture-core) has zero chrome.*
dependencies.
Requirements
reactandreact-dom≥ 18 (peer dependencies)- A browser (capture is DOM-based; SSR renders nothing until hydration)
Server-side Jira publishing (jira, M4), Resource Timing + app-state capture
(captureState, M5/M6) shipped on top of upload + replay URL (endpoint, M2)
and the design element picker (onAnnotate, M3).
License
Apache-2.0