@mushi-mushi/web
v1.22.3
Published
Mushi Mushi browser SDK — embeddable bug reporting widget with Shadow DOM isolation
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: 'proj_xxx', // UUID from Admin → Projects
apiKey: 'mushi_xxx', // report:write key from Admin → Settings → API Keys
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.
What you get
- Shadow-DOM widget — full CSS isolation, light/dark auto-theme, keyboard-first (
Esc/⌘+Enter), honoursprefers-reduced-motion. - Capture — screenshot, console ring buffer, network (fetch interceptor), Web Vitals, and a repro timeline of routes + clicks.
- Resilient by default — IndexedDB offline queue with auto-sync, on-device spam pre-filter, client-side rate limiting, and a payload-size guard that degrades gracefully instead of wedging the queue.
- Never blocks your UI — the host element is zero-sized with
pointer-events: none; only the visible controls opt back in. Verify at runtime withMushi.diagnose(). - Privacy-first — 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.
Mushi.init({
projectId: 'proj_xxx',
apiKey: 'mushi_xxx',
capture: {
// 'rrweb' — full DOM replay (lazy-loaded; text + inputs masked by default)
// 'lite' — dependency-free coarse fallback
// 'sentry'— reuse an installed @sentry/replay session
// 'off' — default
replay: 'rrweb',
},
});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: 'business', message: 'cart.checkout_started', data: { itemCount: 3 } });
try {
await runCheckout();
} catch (err) {
// Normalises any throw, attaches breadcrumbs + sticky tags + Sentry context.
mushi.captureException(err, { level: 'error', 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',
// '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 80 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 (June 2026): 51 edge functions · 298 SQL migrations · 13 outbound plugins · 11 inbound adapters · 19 pipeline agents. Canonical counts: docs/stats.md · pnpm docs-stats
