@mushi-mushi/web
v1.29.0
Published
Bug report and user feedback widget for the web. The bug mediator for AI-built apps: plain-English diagnosis + a ready fix, in your editor.
Maintainers
Readme
@mushi-mushi/web
Your AI wrote it. Mushi tells you why it broke.
Framework-agnostic browser SDK for Mushi Mushi — an embeddable bug-reporting widget that captures a screenshot, the route, the user's note, and the last few seconds of console + network activity, then hands you a plain-English diagnosis in your editor. Renders inside a Shadow DOM, so your CSS never leaks in or out.
One-command setup:
npx mushi-mushiinstalls this package for vanilla-JS apps (or alongside a framework SDK).Framework SDKs:
@mushi-mushi/react(Next.js / React) ·@mushi-mushi/vue·@mushi-mushi/svelte·@mushi-mushi/angular·@mushi-mushi/react-native·@mushi-mushi/capacitor
Install
npm install @mushi-mushi/web
# or: npx mushi-mushiQuick start
import { Mushi } from '@mushi-mushi/web';
Mushi.init({
projectId: '00000000-0000-0000-0000-000000000000', // UUID from Admin → Projects
apiKey: 'mushi_xxx', // report:write key from Setup → Verify (not Settings → BYOK)
widget: { position: 'bottom-right', theme: 'auto' },
capture: { console: true, network: true, screenshot: 'on-report' },
});That's the whole integration. A floating 🐛 launcher appears; a report is one click away.
Same-origin tunnel (ad blockers)
Point ingest at a first-party proxy — the same pattern as Sentry tunnel.
Host APIs must allowlist host + project + /v1/ (empty project list → 503).
Mushi.init({
projectId: '00000000-0000-0000-0000-000000000000',
apiKey: 'mushi_xxx',
tunnel: '/api/mushi-tunnel',
// Published SDKs that ignore `tunnel` still honor apiEndpoint:
apiEndpoint: '/api/mushi-tunnel',
ignoreErrors: ['ResizeObserver loop', /^Script error\.?$/],
denyUrls: [/chrome-extension:/i],
replaysOnErrorSampleRate: 0,
});ignoreErrors / denyUrls apply to automatic captures only. User widget
feedback always sends. Pagehide uses fetch({ keepalive: true }), then a
same-origin sendBeacon envelope the tunnel unwraps.
Runtime config
With runtimeConfig: 'auto' (default), the SDK fetches console settings from
GET /v1/sdk/config and merges them over your init — so you can tune the widget
from Projects → SDK install without rebuilding. Host-wired values (banner
trigger, capture flags) win over console defaults.
→ Runtime config docs · maintainer deep-dive: SDK_RUNTIME_CONFIG.md
Features
- Shadow-DOM widget with CSS isolation, light/dark auto-theme, keyboard-first (
Esc/⌘+Enter), andprefers-reduced-motion. - Screenshot, console ring buffer, network (fetch interceptor), Web Vitals, and a repro timeline of routes + clicks.
- IndexedDB offline queue with auto-sync, on-device spam pre-filter, client-side rate limiting, and a payload-size guard that degrades instead of wedging the queue.
- Host element is zero-sized with
pointer-events: none; only visible controls opt back in. Verify withMushi.diagnose(). - Selector-level screenshot masking/blocking, a one-tap "Remove screenshot" control, and a built-in PII scrubber.
Trigger modes
Choose where and when the launcher shows — auto, edge-tab, attach (bring your own button), manual, or hidden — plus viewport-aware smartHide, route/selector suppression, and anchor CSS that tracks your app shell's tab bars or mini-players.
// Bring your own launcher — Mushi injects no UI of its own.
const mushi = Mushi.init({
projectId: 'proj_xxx',
apiKey: 'mushi_xxx',
widget: { trigger: 'attach', attachToSelector: '[data-mushi-feedback]' },
});
mushi.attachTo('#support-menu-feedback');→ Full posture matrix in Trigger modes. Coherent defaults via SDK presets (production-calm / beta-loud / internal-debug / manual-only).
Mushi.init({
projectId: 'proj_xxx',
apiKey: 'mushi_xxx',
proactive: {
rageClick: true,
longTask: true,
apiCascade: true,
errorBoundary: true, // catch global error / unhandledrejection
cooldown: { maxProactivePerSession: 2, dismissCooldownHours: 24, suppressAfterDismissals: 3 },
},
});Each trigger respects its config flag (set rageClick: false to disable). Fatigue prevention (session limits, cooldowns, permanent suppression) is always on.
Mushi.init({
projectId: 'proj_xxx',
apiKey: 'mushi_xxx',
privacy: {
maskSelectors: ['[data-private]', 'input'], // painted over before serialisation
blockSelectors: ['[data-payment]', '[data-auth-token]'], // removed entirely
allowUserRemoveScreenshot: true, // one-tap "Remove screenshot" in panel
},
});The details step renders the attached screenshot as a visible preview (not just a checkmark) with a Remove control and a configurable privacy caption (widget.screenshotSensitiveHint). A "Mark up" overlay lets reporters highlight / blur / arrow before submitting.
npm install rrweb # only for replay: 'rrweb' — your app installs itMushi.init({
projectId: 'proj_xxx',
apiKey: 'mushi_xxx',
capture: {
// 'rrweb' — full DOM replay (every text node and input masked)
// 'lite' — dependency-free, records clicks only
// 'sentry'— reuse an installed @sentry/replay session
// 'off' — default
replay: 'rrweb',
// Hand the SDK your own import of rrweb.
rrweb: () => import('rrweb'),
},
});replay: 'rrweb' needs rrweb in your app (npm install rrweb). It is an optional peer dependency, so apps that don't record replay don't download it.
rrweb: () => import('rrweb') is what makes 'rrweb' mode work in a bundled app. The SDK cannot import rrweb itself: a published package's import('rrweb') is invisible to your bundler, so rrweb never reaches your build. An import written in your code is one your bundler sees, and it code-splits rrweb into its own chunk that loads only for sessions sampled into replay. Without the loader the SDK tries a global rrweb (the UMD build from a <script> tag), then a bare import('rrweb') (which resolves only under an import map); if neither works, it records clicks only ('lite') and warns once in the console. The rrweb option ships in @mushi-mushi/web 1.29.
Records continuously from init (so you capture the moments before the report), trimmed to a rolling window. Already on Sentry Replay? See coexistence.
const mushi = Mushi.init({ projectId: 'proj_xxx', apiKey: 'mushi_xxx' });
mushi.identify('usr_42', { email: '[email protected]', segment: 'beta' });
mushi.setTags({ plan: 'pro', region: 'apac' });
mushi.addBreadcrumb({ category: 'custom', level: 'info', message: 'cart.checkout_started', data: { itemCount: 3 } });
try {
await runCheckout();
} catch (err) {
// Normalises any throw, attaches breadcrumbs + sticky tags + Sentry context.
mushi.captureException(err, { severity: 'high', tags: { surface: 'checkout' } });
}installAutoBreadcrumbs() ships route changes, console.error/warn, and [data-testid] clicks for free. PII is scrubbed before anything leaves the browser. Auto-detects @sentry/browser v7–v9 and captures the active scope into MushiReport.sentryContext, with deep-links both ways.
The widget's "Your reports" tab lets reporters see team replies and respond, signed via HMAC against the public API key (no auth user required): mushi.listMyReports(), mushi.listMyComments(id), mushi.replyToReport(id, text). Call mushi.identify() and add a rewards block to track activity, tiers, and points. See Rewards & contributor identity.
const mushi = Mushi.init({
projectId: 'proj_xxx',
apiKey: 'mushi_xxx',
// Optional. Defaults: enabled, consent 'implied', DNT/GPC respected, sampleRate 1.
analytics: { consent: 'required' },
});
mushi.setConsent('granted'); // only needed with consent: 'required'
mushi.track('checkout_started', { plan: 'pro' }); // snake_case names, flat properties
mushi.identify('usr_42'); // stitches earlier anonymous events to the personEvents batch to POST /v1/sdk/events (20 per batch or every 5 s, flushed on pagehide, spilled to localStorage when offline). Names must match ^[a-z][a-z0-9_]{1,63}$; properties are flat string | number | boolean | null, PII-looking keys (email, phone, token, …) are dropped, and string values run through the PII scrubber. analytics.enabled: false turns it off; navigator.doNotTrack / Global Privacy Control are honoured by default. The console's Users & Funnels page reads the same events; retention defaults to 90 days per project.
const mushi = Mushi.init({
projectId: 'proj_xxx',
apiKey: 'mushi_xxx',
// 'auto' (default) skips the runtime-config fetch on localhost endpoints;
// pass `true` to force everywhere, `false` to disable entirely.
runtimeConfig: 'auto',
capture: { network: true, ignoreUrls: [/\/api\/internal\//] },
proactive: { apiCascade: { enabled: true, ignoreUrls: ['https://feature-flags.example.com'] } },
});
mushi.show();
mushi.hide();
mushi.setTrigger('manual'); // switch posture at runtime
mushi.openWith('bug'); // open straight into a category
mushi.attachTo('#support-menu'); // bind the launcher to your elementInternal Mushi requests are tagged X-Mushi-Internal and excluded from network capture + apiCascade, so an unconfigured CSP or a flaky local Supabase stack can never make Mushi report on Mushi. For advanced composition, createProactiveManager() and setupProactiveTriggers() are exported so you can wire friction detection into your own UI.
const mushi = Mushi.init({ /* ... */ });
mushi.setScreen({ name: 'Chat', route: '/chat', feature: 'roleplay' });The SDK auto-records route changes (pushState / popstate / hashchange), clicks (with selector + text snippet), and screen events into a 120-entry ring buffer, shipped as MushiReport.timeline. The admin renders it as a chronological "what happened before the report" card.
Mushi.init({
projectId: 'proj_xxx',
apiKey: 'mushi_xxx',
widget: {
trigger: 'banner',
bannerConfig: {
variant: 'brand', // 'neon' | 'brand' | 'subtle'
position: 'top',
message: 'Mushi is in beta — spotted something off? Tell us.',
label: 'Beta', // pill before the message; `false` hides it
bugCta: '🐛 Report a bug',
featureCta: true,
links: [{ label: 'My submissions', href: 'https://app.example.com/feedback' }],
},
},
});All banner text renders via textContent (never HTML), so CMS-sourced copy can't inject markup. message and label can also be driven remotely per project from the dashboard runtime config — change copy without a deploy.
Mushi.init({
projectId: 'proj_xxx',
apiKey: 'mushi_xxx',
capture: {
discoverInventory: {
enabled: true,
throttleMs: 60_000,
routeTemplates: ['/practice/[id]', '/lessons/[slug]'],
},
},
});Ships throttled, PII-free observations (route template, page title, [data-testid] values, recent fetch paths, query-param keys only, hashed user/session id) to POST /v1/sdk/discovery. The server aggregates them and drafts a first-pass inventory.yaml you can accept in /inventory ▸ Discovery. See Inventory & gates.
Host pass-through contract
Mushi never blocks your app's UI. The host element (#mushi-mushi-widget) is position: fixed, zero-sized, with pointer-events: none — only the visible controls opt back into pointer-events: auto inside the Shadow DOM. In native WebView shells (iOS WKWebView, some Android Chromium builds) where pointer-events: none isn't always honoured on fixed overlays, use hideOnSelector or Mushi.hide() during fullscreen flows. Verify at runtime:
const health = await Mushi.diagnose();
console.assert(health.widgetHostPointerSafe, 'Mushi host is blocking UI!');Health check
const health = await Mushi.diagnose();
// → { apiEndpointReachable, cspAllowsEndpoint, widgetMounted, widgetHostPointerSafe, ... }Runs before Mushi.init() too — call it from a debug console or installer wizard to surface CSP / endpoint problems with zero risk of booting the widget.
Test utilities
Deterministic Playwright / jsdom helpers, published as a separate entry-point so production bundles pay nothing for them:
import { triggerBug, openReport, waitForQueueDrain, expectMushiReady } from '@mushi-mushi/web/test-utils';Every helper no-ops when Mushi.getInstance() returns null, so cloud-vs-local conditional-wiring tests don't need a branch.
Known limitations
Screenshot capture uses canvas / SVG foreignObject serialization — it does not work with cross-origin iframes, tainted <canvas> elements, or pages with strict CSP. Best-effort on single-origin SPAs.
Bundle size
~7 KB brotli, enforced at 89.5 KB gzipped (105 KB uncompressed) in CI. Requires @mushi-mushi/core (installed automatically, not bundled inline). The widget's visual system — washi paper, sumi ink, vermillion 朱 accent, system serif — lives in src/styles.ts.
License
MIT
Monorepo scale (July 2026): 59 edge functions · 374 SQL migrations · 13 outbound plugins · 11 inbound adapters · 19 pipeline agents. Canonical counts: docs/stats.md · pnpm docs-stats
