BugTape docs
Capture and Error Semantics
How the SDK captures, buffers, deduplicates and submits.
How the BugTape browser SDK (@bugtape/sdk) captures, buffers, deduplicates, and submits bug reports.
Automatic capture
Once BugTape.init() is called, these recorders start:
| Recorder | Events captured | Can disable? |
|---|---|---|
| DOM | rrweb visual replay (mutations, mouse, scroll, input) | disableDomRecording: true |
| Network | Fetch, XHR, resource timing (URL, status, duration) | disableNetworkRecording: true |
| Console | console.log/warn/error/info/debug | disableConsoleRecording: true |
| Errors | error events — uncaught exceptions with stack traces | disableErrorRecording: true |
| Rejections | unhandledrejection — unhandled promise rejections | disableErrorRecording: true |
| Rage clicks | Rapid repeated clicks on same element | disableRageClickDetection: true |
| Clicks and navigation | Click targets and in-app route changes | Always on |
| Not-found screens | In-app routes that settle on a "not found" screen | autoReport: { reportDeadRoutes: false } |
Events stay in an in-memory ring buffer in the page. By default it keeps the last 10 minutes (recordingWindow) and at most 50,000 events (maxEvents). Older events are dropped. The PII scrubber (pii) runs on each event before it enters the buffer. Each report carries the buffer contents at the time it is sent.
The SDK also sends small presence beacons for the console's live view. presence: false turns them off.
Privacy modes
- Standard (
privacy: 'standard', the default): typed input values are masked. Text incontenteditable,.rr-maskand[data-bugtape-token]elements is masked. Other page text is recorded. - Strict (
privacy: 'strict'): page text in the replay becomes ▇ before it leaves the browser. Text-bearing attributes are masked, URLs lose query strings and fragments, request and response bodies are not kept, and console objects are dropped. Error messages and stacks are still sent after the PII scrub. - Errors only (
disableDomRecording: true): no replay. Errors, requests and console output are still recorded unless you turn them off.
Elements marked data-bugtape-ignore, .bugtape-ignore or .rr-block are left out of the replay in every mode.
Sensitive tables / client grids: mark with data-bugtape-ignore / .bugtape-ignore / .rr-block before any capture key. Prove with the privacy proof checklist and privacy fixture. Default PII regex is not enough for named clients in visible text.
All recorders are wrapped in error boundaries. If a recorder fails, the SDK logs a warning and continues — it never crashes the host application.
Browser crash and tab close
The SDK does not attempt to capture browser crashes or beforeunload events. If the browser tab crashes or is closed:
- Buffered events are lost (they are in-memory only).
- A report that is still being sent is not saved for retry. The SDK saves a report only after a send fails.
- A report saved in
localStorageafter a failed send is resubmitted on the nextBugTape.init()(see Offline and flaky network behavior).
The SDK records nothing before init() runs. Errors thrown earlier in the page load are not captured.
For crash monitoring, use a complementary service or beacon API.
Unhandled error capture
error event
Captures uncaught exceptions globally (window.addEventListener('error', …)). Event data:
{ type: "error", data: { message, filename, lineno, colno, stack } }The SDK does not call preventDefault() — errors still propagate to the browser console and any other error handlers.
unhandledrejection
Captures unhandled promise rejections. Event data:
{ type: "error:unhandledrejection", data: { message, stack } }Auto-reporting
Automatic reporting is on by default. The SDK sends a report, with the buffered events, when it sees one of these:
| Trigger | When | Severity |
|---|---|---|
| Uncaught error | Any error event | high |
| Unhandled rejection | Any unhandledrejection event | high |
| Failed request | A fetch or XHR fails with a network error or HTTP 400 or above, within 8 seconds of a click (interactionWindowMs) | high for 5xx and network errors, medium otherwise |
| Hung request | A fetch or XHR tied to a recent click has no response after 15 seconds (hangTimeoutMs) | high |
| Failed page load | The page itself returned HTTP 400 or above, in browsers that expose the page's HTTP status (once per URL and status in a tab) | high for 5xx, medium otherwise |
| Not-found screen | An in-app route settles on a "not found" screen | medium |
| Empty response | A successful response is empty, only for rules you set in emptyExpectRules (none by default) | medium |
Aborted requests are not reported. Requests to BugTape itself and to common analytics, ads and monitoring hosts are ignored.
Error reports need error recording: disableErrorRecording: true also stops them. Failed and hung request reports need network recording.
Automatic reports are marked captureSource: 'automatic'. Reports from the form are marked user_report. If you set a custom onSubmit, automatic reports go to that callback too.
To turn automatic reporting off (recording and the report form still work):
import { init } from '@bugtape/sdk';
init({ apiKey: 'bt_live_xxx', autoReport: false });autoReport: { enabled: false } does the same. To tune it:
init({
apiKey: 'bt_live_xxx',
autoReport: {
reportUncaughtErrors: false, // keep request reports, skip JS error reports
reportHungRequests: true,
hangTimeoutMs: 20_000, // minimum 1000
interactionWindowMs: 8000,
maxPerMinute: 6,
reportDeadRoutes: true,
},
});Manual issue reporting
Users file reports through the SDK widget (floating bug button) or your own UI. The widget flow:
- User clicks bug button (bottom right by default; set
position) → widget opens. - The widget asks the BugTape server for a short AI summary of the buffered events.
summarize: falseturns this off. (openaiKeyis deprecated.) - User fills in title, description, severity.
- User clicks submit → report sent to API with all buffered events.
Screenshots and voice notes in the report form are not available yet. On the hosted service the Screenshot and Voice buttons are disabled, and the form says so. Report details and recorded events are still sent.
With showButton: false there is no floating button. Automatic reports still run. Call instance.open() from your own link to open the form.
After a new automatic high or critical issue is accepted, the button shows a quiet signal. Duplicates and known regressions do not. majorBugAlert: false turns it off.
For programmatic reporting without the widget, POST directly to /v1/ingest.
Deduplication
Server-side fingerprinting
Every report is fingerprinted on the server. The error type and message come from the report title:
SHA256(errorType : normalizedMessage : normalizedUrl).slice(0, 16)- Same fingerprint + open issue: report count incremented, no new issue created.
- Same fingerprint + closed issue (resolved, verified fixed or awaiting verification): BugTape compares releases. A report from a newer release reopens the issue as a regression. A report from the same or an older release only adds to the count. A report with no release is marked unknown, not a regression. A closed issue with no release to compare against reopens as a regression.
- New fingerprint: new issue created.
Set release in init() so BugTape can compare releases. Without it, the SDK tries BUGTAPE_RELEASE and common CI commit variables.
Client-side throttling
The SDK throttles automatic reports within each page load:
- The same failure is sent at most once a minute (same error message, file and line; or same request, status and click target).
- At most 6 automatic reports are sent per minute in total (
autoReport.maxPerMinute).
Reports from the form are not throttled. The hosted ingest limit is 100 requests a minute for each capture key and IP address.
Event ordering
Events are stored with millisecond Unix timestamps (event.timestamp). The SDK does not guarantee strict ordering — events from different recorders may interleave. The server stores events in insertion order.
When displaying events, sort by timestamp. For DOM replay, rrweb events contain their own internal ordering.
Session correlation
The SDK gives each browser tab a session ID and sends it as sessionId with every report. It is kept in sessionStorage (bugtape:session-id), so it survives reloads in the same tab. A new tab gets a new ID. To use your own ID, pass context.sessionId.
To link reports to a user, send a user ID or email:
import { init, identify } from '@bugtape/sdk';
init({
apiKey: 'bt_live_xxx',
context: { sessionId: 'your-session-id' }, // optional: replaces the tab session ID
});
// After sign-in:
identify({ userId: 'user-123', email: 'alice@example.com' });Reports sent after identify() carry that user. You can also set context.userId and context.reporterEmail in init().
Attachments and replay data
Screenshots
The SDK never puts a screenshot in the /v1/ingest body (screenshot: null). On the hosted service the Screenshot button in the report form is disabled.
With a custom onSubmit, the form can capture a screenshot with modern-screenshot (DOM-to-image). Form values are masked and elements marked private are left out. The image goes only to your callback.
Audio notes
Voice notes are not available yet. The built-in sender refuses audio, and the SDK always sends audio: null.
With a custom onSubmit, the Voice button records audio with the MediaRecorder API (WebM, Opus codec where the browser supports it) and passes it to your callback.
Session replay
DOM events recorded by rrweb are included in the events array. The BugTape console can replay these events visually. The replay player is read-only — the rrweb events are opaque blobs stored as-is.
Offline and flaky network behavior
Retry logic
- SDK submits the report via
fetch(). Each attempt times out after 10 seconds. - If the request fails with a network error, a timeout, or HTTP 408, 425, 429 or 5xx, the SDK retries once after a 1-second delay.
- Other errors (for example HTTP 400, 401 or 402) are not retried. The report is dropped and
onErroris called. - If the retry also fails, the report is serialized and stored in
localStorageunder a BugTape-specific key. The queue keeps at most 10 reports, for up to 24 hours.onQueueEventreports when a report is saved, delivered or dropped.
Pending report resubmission
On the next BugTape.init() call (e.g., page reload), the SDK checks localStorage for pending reports and resubmits them. Each one goes only to the endpoint and capture key it was saved with. Successfully submitted pending reports are cleared from storage. Reports older than 24 hours are removed without being sent.
Buffered events during offline
Events continue to be captured in the ring buffer while offline. If the user submits a report while offline, the events captured up to that point are included. The report is stored locally and resubmitted on the next BugTape.init() (usually the next page load).
Network failure events
Network failures are captured as network:fetch:error and network:xhr:error events and included in the buffer. When hung-request reporting is on, a request with no response after hangTimeoutMs adds a network:fetch:hang or network:xhr:hang event.
Best practices for auto-reporting
- Start with the defaults. The SDK already throttles automatic reports. Lower
autoReport.maxPerMinuteif one page sends too many.
- Set
release. BugTape needs it to tell a regression from an old failure.
- Identify users. Call
identify()after sign-in so reports show who was affected.
- Expect 402 at the plan limit. If the API returns 402 (plan limit), the SDK does not retry. The report is dropped and
onErroris called.
- Expect 429 under load. The SDK retries a 429 once after 1 second, then stores the report and sends it again on the next
init().
- Don't suppress widget reports. Even with auto-reporting, keep the widget available. User-submitted reports include context (title, description, what the user tried to do) that auto-reports lack.