Skip to main content

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:

Feedback text
The free-form description the reporter types. This is the core of every widget report.
Annotations
Marks the reporter draws on the page (boxes, highlights) to point at the problem.
Screenshot (optional)
A capture of the current view, only if the reporter chooses to attach one.
Page context
The current URL, viewport size, and user agent — lightweight metadata that helps you reproduce the issue.

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.

Privacy by design

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

PropTypeRequiredDescription
projectKeystringYesYour public widget key (bp_pub_…). Identifies the project and environment the report belongs to.
publicKeystringYes*Alias for projectKey. Pass one or the other, not both.
environmentstringRecommendedThe environment label for this build, e.g. "staging" or "production". Helps you separate reports by where they came from.
apiBaseUrlstringNoAPI 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 }NoIdentifies the reporter so reports are attributed. All fields are optional.
diagnosticsBugPortDiagnosticsConfigNoOpt-in in-page diagnostics capture (console, errors, fetch/XHR). Off unless enabled: true. See Diagnostics capture.
replayBugPortReplayConfigNoOpt-in masked DOM session replay (rrweb). Off unless enabled: true. Requires the optional rrweb peer. See Session replay.
offlineQueuebooleanNoOpt-in. Retry submissions that failed while offline (IndexedDB). Off by default. See Offline queue.
Either 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',
})
No-build sites

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.

CallbackFires whenArgument
onSubmittedA report is accepted by the APIresult — includes the report's dashboard URL
onErrorSubmission failserror — the failure that occurred
onSubmitPayloadJust before submitpayload — mutate it to enrich the report

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
}}
/>
💡
Use 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.

What in-page capture can and cannot do

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
  • window error events and unhandled promise rejections
  • fetch and XMLHttpRequest requests 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.

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, Bearer tokens, and provider-style secret keys — are stripped from console messages, error text, URLs, and bodies.
  • Request/response bodies are off by default. Enable requestBodies / responseBodies only if your team needs them. Binary and streaming responses, and file uploads (FormData/Blob/ArrayBuffer), are skipped. Bodies are capped at maxBodySizeBytes (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

FieldTypeDefaultDescription
enabledbooleanfalseMaster switch. Nothing is instrumented unless true.
consolebooleanfalseCapture console.log/info/warn/error/debug.
runtimeErrorsbooleanfalseCapture error events and unhandled rejections.
networkbooleanfalseShorthand enabling both fetch and xhr.
fetch / xhrbooleaninherit networkCapture each transport individually.
requestBodiesbooleanfalseCapture request body previews (text/JSON only).
responseBodiesbooleanfalseCapture response body previews (text/JSON only).
userControlledbooleantrueShow end-user consent toggles in the report form.
defaultAttachConsole / defaultAttachNetwork / defaultAttachRuntimeErrorsbooleanfalseStarting state of each toggle.
maxEventsnumber100Default cap per buffer (overridden by the specific caps below).
maxConsoleEvents / maxNetworkEvents / maxRuntimeErrorEventsnumber100/100/50Per-source ring-buffer caps.
maxBodySizeBytesnumber65536Max captured body size before truncation.
allowedUrls / deniedUrlsArray<string | RegExp>[]URL allow/deny lists for network capture.
redactHeadersstring[][]Extra header names to redact (merged with defaults).
redactPatternsRegExp[][]Extra value patterns to redact (merged with defaults).
beforeSendDiagnostic(event) => event | nullFinal 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

SymptomCauseFix
Toggles don't appearenabled or the specific source is off, or userControlled: falseSet enabled: true and the source flag; keep userControlled: true
A request is missingIt happened before the widget mounted, or matched deniedUrls / failed allowedUrlsMount the widget earlier; check your URL lists
No response bodyresponseBodies is off, or the response is binary/streamingEnable responseBodies; bodies are only read for text/JSON content types
A value looks over-redactedIt matched a key name or secret patternNarrow 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 redactPatterns you add).
  • Add data-bugport-exclude to any element to block its entire subtree from recording (also excluded from screenshots).
  • Use blockClass / blockSelector for app-wide block rules, or maskAllText: true to mask every text node.

Config reference

FieldTypeDefaultDescription
enabledbooleanfalseMaster switch. rrweb is never loaded unless true.
maxDurationMsnumber30000Rolling window to retain (clamped 1s–120s). A fresh full snapshot is checkpointed ~once per window, so retained replay covers 1–2× this.
maxEventsnumber10000Hard cap on retained events.
maxSizeBytesnumber5242880Drop the replay if the estimated payload exceeds this.
maskAllInputsbooleantrueMask every input/textarea value.
maskAllTextbooleanfalseMask every text node (aggressive).
blockSelectorstringCSS selector whose subtree is excluded (unioned with [data-bugport-exclude]).
blockClassstringbugport-blockClass whose subtree is excluded.
maskTextClassstringbugport-maskClass whose text is masked.
recordCanvasbooleanfalseRecord <canvas> contents.
mousemoveSamplingMs / scrollSamplingMsnumber50 / 100Sampling throttles.
userControlledbooleantrueShow the end-user replay toggle.
defaultAttachbooleanfalsePre-check the replay toggle.
redactPatternsRegExp[][]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.

note

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.

  1. 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).

    <BugPortWidget
    projectKey="bp_pub_local_xxxxxxxx"
    environment="local"
    apiBaseUrl="http://localhost:8000/v1"
    />
  2. Production

    Omit apiBaseUrl to use the hosted default, and use your production key.

    <BugPortWidget
    projectKey="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.

Do
  • Create a separate widget key per environment (local, staging, production)
  • Add every origin the widget runs on, including http://localhost:3000 for local dev
  • Rotate or revoke a key from the dashboard if it leaks or you stop using an origin
Don't
  • 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

SymptomCauseFix
403 / origin rejectedThe page's origin is not on the widget key's allowed-origins listAdd the origin (including the protocol and port) to the key in the dashboard
404 on submitapiBaseUrl is wrong or missing the version suffixMake sure apiBaseUrl ends in /v1 (e.g. http://localhost:8000/v1)
Nothing rendersRunning on the server, or React is too oldEnsure React 18+ and that the widget mounts client-side (see the Next.js note above)
Verify the request path

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.

Next steps