Embed the feedback widget on your website
Add one script tag, or the @bugmojo/widget npm package, so visitors can report bugs with a session replay and console and network logs attached.
The BugMojo widget is the framework-agnostic on-site feedback and bug-capture widget. Each report goes straight into your BugMojo project, where your team, and your AI coding agents over MCP, can act on it. There are two ways to deliver it:
- Hosted loader (recommended): a lightweight
/widget.jsscript served by BugMojo. You do not need the npm package for this. - Same-origin npm package:
@bugmojo/widget, for sites whose Content-Security-Policy blocks the hosted script.
Package: @bugmojo/widget on npm · Full reference: the README on GitHub.
Find your embed token
Both options need your project's embed token. Copy it from Project Settings → Widget in the BugMojo dashboard at www.bugmojo.com. It is a public identifier, not a secret, so it is safe to put in your page's HTML.
Option 1: the hosted script tag
<!-- Place early in <head> so capture installs before your app emits events -->
<script
src="https://www.bugmojo.com/widget.js"
data-project="YOUR_PUBLIC_EMBED_TOKEN"
data-position="bottom-right"
data-env="production"
async
></script>Place it early in <head> so capture installs before your app emits events. The loader boots the widget and lazy-loads a separate widget-capture.js chunk holding the rrweb recording engine, so rrweb never bloats the initial script.
Besides data-project, data-position and data-env, the loader reads data-release (release gating) and data-nonce (CSP nonce). To configure it from code instead, define a window.bugmojoSettings object. Once the widget boots, the loader exposes window.BugMojo and dispatches a bugmojo:ready event.
Option 2: the npm package
Use @bugmojo/widget when your Content-Security-Policy blocks the hosted script. Everything, rrweb included, ships in your own bundle, so you can serve the widget from your own origin under a strict script-src 'self' policy.
pnpm add @bugmojo/widget
# or: npm install @bugmojo/widget
# or: yarn add @bugmojo/widgetimport { initWidgetBundled } from '@bugmojo/widget';
const api = await initWidgetBundled({
embedToken: 'YOUR_PUBLIC_EMBED_TOKEN', // the data-project identifier (not a secret)
apiBase: 'https://www.bugmojo.com',
position: 'bottom-right',
});
// Imperative API (the hosted loader exposes the same object as window.BugMojo):
api.setUser({ id: 'u_123', email: 'jane@example.com' });
api.open();
api.destroy(); // clean teardown on SPA route changeif (typeof window !== 'undefined') {
const { initWidgetBundled } = await import('@bugmojo/widget');
await initWidgetBundled({ embedToken: '…', apiBase: 'https://www.bugmojo.com' });
}Configuration options
| Option | Default | What it does |
|---|---|---|
embedToken | required | Public embed token (the data-project value). An identifier, not a secret. |
apiBase | loader script origin | API origin. For npm usage, set it explicitly, for example https://www.bugmojo.com. |
position | from server config | Launcher position: 'bottom-right', 'bottom-left', 'right-middle' or 'left-middle'. |
env | None | Environment label the server uses to pick the right capture ruleset. Not access control. |
release | None | Release or version string, used for minimum and maximum release gating. |
nonce | None | CSP nonce applied to the Shadow-DOM <style>. |
screenshot | 'none' | 'none' derives the visual from the rrweb snapshot, with masking already applied. 'html2canvas' is best-effort and needs window.html2canvas on the page. 'displaymedia' shows a browser permission prompt. |
user | None | Initial reporter identity: { id?, email?, name?, cohort? }. cohort is matched against the ruleset to gate capture. |
customData | None | Arbitrary metadata attached to every submission. |
onOpen / onClose | None | Lifecycle callbacks, also available through api.on(…). |
onSubmit | None | Called after a submission with { category, number?, pendingModeration? }. |
Control the widget from JavaScript
The hosted loader exposes this API as window.BugMojo, and initWidgetBundled returns the same object.
| Method | What it does |
|---|---|
open() / close() | Open or close the feedback panel. |
show() / hide() | Show or hide the launcher button. The JS API keeps working while it is hidden. |
capture() | Start recording, if the capture gate allows it, and open the panel directly on the Bug category. |
setUser(user) | Merge the reporter identity into the current one. setReporter is an alias. |
setCustomData(data) | Merge custom metadata into what is attached to submissions. |
on(event, cb) / off(event, cb) | Subscribe to or remove a listener for 'open', 'close' or 'submit'. on returns an unsubscribe function. |
destroy() | Full teardown: stops rrweb, restores the native console, fetch and XHR, and removes the UI. Idempotent. |
Privacy
Console and network capture is PII-redacted in the browser, before anything leaves it. The session replay is an rrweb DOM recording kept in a rolling buffer, not a video. With the default screenshot: 'none', the screenshot is derived from the rrweb snapshot, so masking has already been applied.
Your project's server ruleset also governs capture. Sampling is sticky, so a tab session is consistently in or out of the sample. Through short outages the widget serves the last-good config, but capture turns off after a 24-hour hard cap. Before you put the widget in front of visitors, read disclose the BugMojo widget to your visitors, and verify redaction yourself in DevTools.
Common questions
Why doesn't the widget appear?
Check the browser console. "[BugMojo] missing embed token" means no data-project or embedToken was provided. "[BugMojo] widget failed to initialize" means the config fetch failed: verify apiBase and that the token matches your project's widget settings. If config fails completely, the JS API still exists but the UI does nothing, so the widget never breaks your page.
Why do reports arrive without replay, console or network data?
Capture is gated by the server ruleset. It can be disabled for your environment, sampled out by sample_rate, restricted by cohort (set user.cohort), or blocked by require_login when no reporter id or email is set. The widget also skips instrumentation when another BugMojo capture, such as the browser extension, is already patching the page. Config changes take effect within the cache TTL, 60 seconds by default.
My Content-Security-Policy blocks the hosted widget.js. What now?
That is what the npm package is for. Bundle initWidgetBundled into your own app and serve it from your origin under script-src 'self'. Pass a nonce if your policy requires one for the Shadow-DOM style tag.
How do I remove the widget on an SPA route change?
Call destroy(). It is idempotent and restores the native console, fetch and XHR before removing the UI, so you can safely initialize the widget again later.
Why do I get "does not provide an export named 'record'"?
You imported the package in a bare Node runtime: a plain script, a Vitest node environment, or SSR with externalized dependencies. Use a bundler, or a client-only dynamic import.
Related articles
- Install the React SDK for React and Next.jsAdd @bugmojo/react to mount the BugMojo widget, catch render errors with their component stack, and file bugs straight from your code.
- Disclose the BugMojo widget to your visitorsWhat the always-on widget buffer does, when data is uploaded, and copy you can paste into your own privacy policy.
- Redact PII on the client before it leaves the browserControl what personal data the extension masks, so sensitive text never leaves the browser in a bug report.
- Verify redaction yourself in DevToolsProve that BugMojo masks your data by watching the actual capture payload leave your browser.
- Install the React Native and Expo SDKAdd @bugmojo/react-native so users can shake their phone to report a bug, with console and network data redacted on the device before upload.

