NPM Widget
@bugport.ai/widget is a small React component that lets your users report bugs from inside your own app — no browser extension required. The reporter writes feedback, optionally annotates the page or attaches a screenshot, and the widget submits a structured report straight to your BugPort project. It is the right choice when you want feedback capture available to every user of a web app (customers, testers, teammates) rather than only to people who have installed the browser extension.
v0.1.0 The npm package ships a React component and an imperative mount API. A CDN/IIFE build for no-build sites is Planned and is not shipped yet.
What the widget captures
The widget is intentionally minimal and reporter-driven. It only collects what the person filing the report actually triggers:
By default it does not record console logs, network traffic, or session replay. You can opt in to lightweight in-page diagnostics (console, JavaScript errors, and fetch/XHR activity) — see Diagnostics capture — and to a masked DOM session replay — see Session replay. Both are off until you enable them, gated behind end-user consent toggles, and limited to what page JavaScript can observe. For full browser-level fidelity (complete network capture, video recording), use the browser extension. The widget is the lighter-weight, in-product path.
Because the widget captures only feedback, annotations, an optional screenshot, and page context, there is far less surface area for accidentally storing secrets. Screenshots can still contain whatever is visible on screen, so treat them like any other user-submitted media. See Security & Privacy for recommended practices.
Install
npm install @bugport.ai/widget
The package requires React 18+ and renders on the client only.
Basic React usage
Render BugPortWidget once, high in your tree (for example in your root layout), and pass it a public widget key. You create that key per project per environment in the dashboard — see Create a project for how to generate bp_pub_… keys and configure allowed origins.
import { BugPortWidget } from '@bugport.ai/widget'
export function App() {
return (
<>
{/* your app */}
<BugPortWidget
projectKey="bp_pub_xxxxxxxxxxxxxxxx"
environment="production"
/>
</>
)
}
That is enough to get a working feedback launcher. By default the widget talks to the hosted API at https://api.bugport.ai/v1; you only set apiBaseUrl when pointing at a local or self-hosted backend.
Props
| Prop | Type | Required | Description |
|---|---|---|---|
projectKey | string | Yes | Your public widget key (bp_pub_…). Identifies the project and environment the report belongs to. |
publicKey | string | Yes* | Alias for projectKey. Pass one or the other, not both. |
environment | string | Recommended | The environment label for this build, e.g. "staging" or "production". Helps you separate reports by where they came from. |
apiBaseUrl | string | No | API base URL. Defaults to https://api.bugport.ai/v1. Override for local/self-hosted. Must end in /v1. |
user | { id?: string; email?: string; name?: string } | No | Identifies the reporter so reports are attributed. All fields are optional. |
diagnostics | BugPortDiagnosticsConfig | No | Opt-in in-page diagnostics capture (console, errors, fetch/XHR). Off unless enabled: true. See Diagnostics capture. |
replay | BugPortReplayConfig | No | Opt-in masked DOM session replay (rrweb). Off unless enabled: true. Requires the optional rrweb peer. See Session replay. |
offlineQueue | boolean | No | Opt-in. Retry submissions that failed while offline (IndexedDB). Off by default. See Offline queue. |
projectKey or its alias publicKey is required — supply exactly one.
Attaching a user makes triage far easier because each report names who hit the bug:
<BugPortWidget
projectKey="bp_pub_xxxxxxxxxxxxxxxx"
environment="production"
user={{ id: 'u_123', email: 'jordan@acme.com', name: 'Jordan Lee' }}
/>
Imperative mounting
If you are not rendering inside a React tree — or you want to mount the widget from a script tag in an existing build — use the imperative API. Both initBugPortWidget and the global window.BugPort.init take the same options as the component props.
import { initBugPortWidget } from '@bugport.ai/widget'
initBugPortWidget({
projectKey: 'bp_pub_xxxxxxxxxxxxxxxx',
environment: 'production',
user: { email: 'jordan@acme.com' },
})
When the package is loaded on the page, it also exposes a global initializer:
window.BugPort.init({
projectKey: 'bp_pub_xxxxxxxxxxxxxxxx',
environment: 'production',
})
The imperative API still requires the npm package to be bundled and on the page. A standalone CDN/IIFE build that you can drop in via a single <script src="…"> tag is Planned and not available yet. For now, install from npm and bundle it with your app.
Callbacks
Three optional callbacks let you hook into the submit lifecycle. Each receives a single argument.
| Callback | Fires when | Argument |
|---|---|---|
onSubmitted | A report is accepted by the API | result — includes the report's dashboard URL |
onError | Submission fails | error — the failure that occurred |
onSubmitPayload | Just before submit | payload — mutate it to enrich the report |
onSubmitted(result) — confirm and link
Use this to give the reporter feedback that their report landed, and to surface the dashboard link (for example to your own support team).
<BugPortWidget
projectKey="bp_pub_xxxxxxxxxxxxxxxx"
environment="production"
onSubmitted={(result) => {
toast.success('Thanks! Your report was sent.')
console.log('View in dashboard:', result.dashboardUrl)
}}
/>
onError(error) — handle failures gracefully
Surface a friendly message and log the failure to your own monitoring so a misconfigured key or origin does not silently swallow reports.
<BugPortWidget
projectKey="bp_pub_xxxxxxxxxxxxxxxx"
environment="production"
onError={(error) => {
toast.error('We could not send your report. Please try again.')
myMonitoring.captureException(error)
}}
/>
onSubmitPayload(payload) — enrich before submit
Mutate the outgoing payload to attach app-specific context — a build hash, the active feature flag, the current route name. This keeps your reports richer without changing what the reporter has to do.
<BugPortWidget
projectKey="bp_pub_xxxxxxxxxxxxxxxx"
environment="production"
onSubmitPayload={(payload) => {
payload.environment = `prod@${__BUILD_SHA__}`
return payload
}}
/>
onSubmitPayload to stamp every report with the data your team always asks for first — build version, tenant, or feature flag — so triage starts with answers instead of questions.Diagnostics capture
The widget can optionally attach in-page diagnostics to a report: recent console messages, JavaScript errors, and fetch/XHR network activity. This is off by default. You turn it on with the diagnostics prop, and the reporter decides — per report — what actually gets attached.
The widget runs inside your page's JavaScript environment, so it can only observe what page JS can observe:
- ✅
console.*calls made after the widget initializes - ✅
windowerror events and unhandled promise rejections - ✅
fetchandXMLHttpRequestrequests made after the widget initializes
It cannot see requests that happened before it mounted, requests made by other tabs or workers you don't control, browser-internal traffic, or anything requiring extension permissions. For complete browser-level network capture and session replay, use the browser extension.
The widget can capture in-page fetch/XHR activity after it initializes. For full browser-level capture, use the browser extension.
Enabling capture
<BugPortWidget
projectKey="bp_pub_xxxxxxxxxxxxxxxx"
environment="production"
diagnostics={{
enabled: true,
console: true,
runtimeErrors: true,
network: true, // shorthand for fetch + xhr
responseBodies: false, // keep response bodies off unless you need them
userControlled: true, // show consent toggles in the report form
defaultAttachConsole: false,
defaultAttachNetwork: false,
maxEvents: 100,
maxBodySizeBytes: 64 * 1024,
}}
/>
Instrumentation installs when the widget mounts and is fully removed when it unmounts — no globals stay patched. Original console, fetch, and XMLHttpRequest behaviour is always preserved; the wrappers never throw into your app, and they read response bodies off a Response.clone() so your code still receives the original stream.
How user consent works
When userControlled is true (the default), the report form shows an "Attach technical details" section with a checkbox for each capture source you enabled:
- Console logs — "Include recent console messages from this page."
- Network activity — "Include recent API requests from this page. Sensitive headers are filtered."
- JavaScript errors — "Include recent page errors and unhandled promise failures."
Only sources you enabled in config appear. The reporter can turn each on or off before submitting. Their starting state comes from the defaultAttach* flags. If you set userControlled: false, no toggles are shown and only the defaultAttach* sources are attached automatically.
Diagnostics are included only when (a) you enabled capture in config and (b) the source is consented for that submission. A report with everything toggled off submits exactly as it did before this feature existed.
Privacy and redaction
Diagnostics are scrubbed before they are stored or sent:
- Sensitive headers (
authorization,cookie,set-cookie,x-api-key, …) are replaced with[REDACTED]. - Sensitive URL query params and JSON body keys (
token,access_token,password,secret,session,jwt, …) are redacted. - Value patterns — JWTs,
Bearertokens, and provider-style secret keys — are stripped from console messages, error text, URLs, and bodies. - Request/response bodies are off by default. Enable
requestBodies/responseBodiesonly if your team needs them. Binary and streaming responses, and file uploads (FormData/Blob/ArrayBuffer), are skipped. Bodies are capped atmaxBodySizeBytes(64 KB default) and flagged when truncated. - Buffers are bounded. Only the most recent N events per source are kept (
maxEvents/maxConsoleEvents/maxNetworkEvents).
You can extend redaction and filtering per app:
diagnostics={{
enabled: true,
network: true,
allowedUrls: [/api\.myapp\.com/], // only capture these
deniedUrls: ['/auth', '/billing'], // never capture these
redactHeaders: ['x-tenant-id'], // extra header names to scrub
redactPatterns: [/ACME-\d{6}/g], // extra value patterns to scrub
beforeSendDiagnostic: (event) => {
// Final hook: mutate, or return null to drop the event entirely.
if (event.type === 'network' && event.url.includes('/internal')) return null
return event
},
}}
Config reference
| Field | Type | Default | Description |
|---|---|---|---|
enabled | boolean | false | Master switch. Nothing is instrumented unless true. |
console | boolean | false | Capture console.log/info/warn/error/debug. |
runtimeErrors | boolean | false | Capture error events and unhandled rejections. |
network | boolean | false | Shorthand enabling both fetch and xhr. |
fetch / xhr | boolean | inherit network | Capture each transport individually. |
requestBodies | boolean | false | Capture request body previews (text/JSON only). |
responseBodies | boolean | false | Capture response body previews (text/JSON only). |
userControlled | boolean | true | Show end-user consent toggles in the report form. |
defaultAttachConsole / defaultAttachNetwork / defaultAttachRuntimeErrors | boolean | false | Starting state of each toggle. |
maxEvents | number | 100 | Default cap per buffer (overridden by the specific caps below). |
maxConsoleEvents / maxNetworkEvents / maxRuntimeErrorEvents | number | 100/100/50 | Per-source ring-buffer caps. |
maxBodySizeBytes | number | 65536 | Max captured body size before truncation. |
allowedUrls / deniedUrls | Array<string | RegExp> | [] | URL allow/deny lists for network capture. |
redactHeaders | string[] | [] | Extra header names to redact (merged with defaults). |
redactPatterns | RegExp[] | [] | Extra value patterns to redact (merged with defaults). |
beforeSendDiagnostic | (event) => event | null | — | Final per-event hook; return null to drop. |
Where diagnostics appear
Captured console messages and JavaScript errors show in the bug's Console panel; network activity shows in the Network panel — the same panels used for browser-extension reports. Errors are folded into the console stream as error-level rows. Reports with no diagnostics show the normal empty states.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Toggles don't appear | enabled or the specific source is off, or userControlled: false | Set enabled: true and the source flag; keep userControlled: true |
| A request is missing | It happened before the widget mounted, or matched deniedUrls / failed allowedUrls | Mount the widget earlier; check your URL lists |
| No response body | responseBodies is off, or the response is binary/streaming | Enable responseBodies; bodies are only read for text/JSON content types |
| A value looks over-redacted | It matched a key name or secret pattern | Narrow with allowedUrls, or post-process in beforeSendDiagnostic |
Session replay
The widget can optionally attach a short, masked DOM session replay to a report — a recording of the last few seconds of DOM activity on the page, played back in the dashboard. Like diagnostics, it is off by default, consent-gated, and privacy-masked before anything leaves the page. It records only the page's own DOM (never cross-origin frames or browser-level state — for that fidelity use the browser extension).
Optional rrweb dependency
Replay is powered by rrweb (MIT). It is an optional dependency loaded lazily at runtime, so it never bloats your bundle unless replay is enabled. Install it in your app to turn replay on:
npm install rrweb
If rrweb is not installed, replay silently does nothing — the rest of the widget is unaffected.
Enabling replay
<BugPortWidget
projectKey="bp_pub_xxxxxxxxxxxxxxxx"
environment="production"
replay={{
enabled: true,
maxDurationMs: 30000, // rolling window to retain (default 30s)
}}
/>
A Session replay toggle then appears in the report form's "Attach technical details" section. The replay is included only when (a) you enabled it in config and (b) the reporter consented for that submission.
Privacy and masking
Masking defaults are privacy-first:
- All inputs are masked by default (
maskAllInputs: true) — typed values never leave the page. - Recorded text is run through the same redactor as diagnostics (JWTs, bearer tokens, provider secret keys, plus any
redactPatternsyou add). - Add
data-bugport-excludeto any element to block its entire subtree from recording (also excluded from screenshots). - Use
blockClass/blockSelectorfor app-wide block rules, ormaskAllText: trueto mask every text node.
Config reference
| Field | Type | Default | Description |
|---|---|---|---|
enabled | boolean | false | Master switch. rrweb is never loaded unless true. |
maxDurationMs | number | 30000 | Rolling window to retain (clamped 1s–120s). A fresh full snapshot is checkpointed ~once per window, so retained replay covers 1–2× this. |
maxEvents | number | 10000 | Hard cap on retained events. |
maxSizeBytes | number | 5242880 | Drop the replay if the estimated payload exceeds this. |
maskAllInputs | boolean | true | Mask every input/textarea value. |
maskAllText | boolean | false | Mask every text node (aggressive). |
blockSelector | string | — | CSS selector whose subtree is excluded (unioned with [data-bugport-exclude]). |
blockClass | string | bugport-block | Class whose subtree is excluded. |
maskTextClass | string | bugport-mask | Class whose text is masked. |
recordCanvas | boolean | false | Record <canvas> contents. |
mousemoveSamplingMs / scrollSamplingMs | number | 50 / 100 | Sampling throttles. |
userControlled | boolean | true | Show the end-user replay toggle. |
defaultAttach | boolean | false | Pre-check the replay toggle. |
redactPatterns | RegExp[] | [] | Extra value patterns redacted from recorded text. |
When a reporter consents, the masked replay is compressed (gzip via the browser's native CompressionStream, falling back to uncompressed JSON) and uploaded through the same presigned-URL flow as screenshots, then attached to the bug. The replay never blocks the report: if its upload fails, the bug is still submitted.
The dashboard replay player ships in a later release. With this version the widget captures, masks, compresses, and uploads the rolling window; it is stored alongside the bug's other media.
Offline queue
By default a failed submission surfaces an error and the reporter retries. If you opt in with offlineQueue, submissions that fail because the browser is offline are persisted to IndexedDB and retried automatically — when the browser reconnects and on the next widget mount.
<BugPortWidget projectKey="bp_pub_xxxxxxxxxxxxxxxx" offlineQueue />
The widget authenticates with a public project key in the request body (no secret token), so nothing sensitive is persisted. The queue caps at 10 items, expires entries after 7 days, and gives up after 5 attempts. Queuing only triggers on transient network errors — server rejections (validation, rate limits) still surface immediately.
Local development vs production
The only difference between environments is the apiBaseUrl and which widget key you use.
- Local / self-hosted
Point the widget at your running backend and use a key whose allowed origins include your dev origin (for example
http://localhost:3000).<BugPortWidgetprojectKey="bp_pub_local_xxxxxxxx"environment="local"apiBaseUrl="http://localhost:8000/v1"/> - Production
Omit
apiBaseUrlto use the hosted default, and use your production key.<BugPortWidgetprojectKey="bp_pub_prod_xxxxxxxx"environment="production"/>The default base URL is
https://api.bugport.ai/v1.
Allowed origins
Public widget keys are safe to ship in client code because they are origin-restricted. Each bp_pub_… key carries an allowed-origins list, and the API rejects submissions from any origin not on that list. Before a key works on a given site, add that site's origin to the key's allowed origins in the dashboard.
- Create a separate widget key per environment (local, staging, production)
- Add every origin the widget runs on, including
http://localhost:3000for local dev - Rotate or revoke a key from the dashboard if it leaks or you stop using an origin
- Reuse a production key on staging or local — keep environments isolated
- Forget to add new preview/branch origins, or submissions will be rejected with a 403
React and client-side rendering notes
The widget mounts in the browser and touches DOM/browser APIs, so it must run on the client.
- React 18+ is required.
- Client-side only. In a server-rendered framework, ensure the component is not rendered during SSR.
- Next.js. Render the widget inside a Client Component — add the
'use client'directive at the top of the file that uses it. If you hit hydration or "window is not defined" issues, load it with a dynamic import that disables server rendering. The general pattern looks like this:
'use client'
import dynamic from 'next/dynamic'
const BugPortWidget = dynamic(
() => import('@bugport.ai/widget').then((m) => m.BugPortWidget),
{ ssr: false },
)
export function Feedback() {
return (
<BugPortWidget
projectKey="bp_pub_xxxxxxxxxxxxxxxx"
environment="production"
/>
)
}
The exact dynamic-import approach varies by framework version; the key requirement is simply that the widget initializes in the browser, not on the server.
Common integration errors
| Symptom | Cause | Fix |
|---|---|---|
| 403 / origin rejected | The page's origin is not on the widget key's allowed-origins list | Add the origin (including the protocol and port) to the key in the dashboard |
| 404 on submit | apiBaseUrl is wrong or missing the version suffix | Make sure apiBaseUrl ends in /v1 (e.g. http://localhost:8000/v1) |
| Nothing renders | Running on the server, or React is too old | Ensure React 18+ and that the widget mounts client-side (see the Next.js note above) |
A quick way to diagnose a 404 is to open your browser's network tab and confirm submissions hit a URL ending in /widget/bugs under a base that ends in /v1.