How-to

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.

Applies toWhere:Web app

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.js script 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

html
<!-- 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.

Terminal
pnpm add @bugmojo/widget
# or: npm install @bugmojo/widget
# or: yarn add @bugmojo/widget
ts
import { 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 change
ts
if (typeof window !== 'undefined') {
  const { initWidgetBundled } = await import('@bugmojo/widget');
  await initWidgetBundled({ embedToken: '…', apiBase: 'https://www.bugmojo.com' });
}

Configuration options

OptionDefaultWhat it does
embedTokenrequiredPublic embed token (the data-project value). An identifier, not a secret.
apiBaseloader script originAPI origin. For npm usage, set it explicitly, for example https://www.bugmojo.com.
positionfrom server configLauncher position: 'bottom-right', 'bottom-left', 'right-middle' or 'left-middle'.
envNoneEnvironment label the server uses to pick the right capture ruleset. Not access control.
releaseNoneRelease or version string, used for minimum and maximum release gating.
nonceNoneCSP 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.
userNoneInitial reporter identity: { id?, email?, name?, cohort? }. cohort is matched against the ruleset to gate capture.
customDataNoneArbitrary metadata attached to every submission.
onOpen / onCloseNoneLifecycle callbacks, also available through api.on(…).
onSubmitNoneCalled 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.

MethodWhat 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.

Was this helpful?

Related articles