JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 1158
  • Score
    100M100P100Q104567F
  • License MIT

Mushi Mushi browser SDK — embeddable bug reporting widget with Shadow DOM isolation

Package Exports

  • @mushi-mushi/web
  • @mushi-mushi/web/test-utils

Readme

@mushi-mushi/web

Browser SDK for Mushi Mushi — embeddable bug reporting widget with Shadow DOM isolation.

One-command setup: npx mushi-mushi installs this package for vanilla-JS apps (or alongside the framework SDK for Vue, Svelte, Angular).

Framework SDKs: @mushi-mushi/react (Next.js / React) · @mushi-mushi/vue (Nuxt / Vue) · @mushi-mushi/svelte (SvelteKit / Svelte) · @mushi-mushi/angular · @mushi-mushi/react-native · @mushi-mushi/capacitor

Features

  • Shadow DOM widget with full CSS isolation from host page
  • Console log capture (ring buffer)
  • Network request capture (fetch interceptor)
  • Screenshot capture (canvas-based)
  • Web Vitals / performance metrics
  • IndexedDB offline queue with auto-sync
  • On-device pre-filter (blocks spam before server submission)
  • Client-side rate limiting (token bucket self-throttle)
  • Light/dark theme with auto-detection (prefers-color-scheme)
  • Trigger modes (0.6+) — auto / edge-tab / attach (bring-your-own-button) / manual / hidden, plus smartHide, hideOnSelector, hideOnRoutes, configurable inset and respectSafeArea
  • Runtime trigger APIsMushi.show(), Mushi.hide(), Mushi.attachTo(selector), Mushi.setTrigger(mode), Mushi.openWith(category)
  • Widget anchor (0.9+) — widget.anchor accepts raw CSS (including var() and env()) so the launcher honours your app shell's tab bars, docks, mini-players, and cookie banners without Shadow-DOM patching
  • Presets (0.9+) — preset: 'production-calm' | 'beta-loud' | 'internal-debug' | 'manual-only' flips a coherent bundle of widget / capture / proactive defaults so prod apps stay quiet and internal builds stay loud
  • Proactive triggers — rage click, long task, API cascade failure detection
  • Report fatigue prevention — session limits, cooldowns, permanent suppression
  • Privacy controls (0.9.1+) — privacy.maskSelectors, privacy.blockSelectors, privacy.allowUserRemoveScreenshot for selector-level screenshot redaction and a one-tap "Remove screenshot" button in the panel
  • Repro timeline (0.10+) — auto-captures route changes, clicks, and SDK lifecycle into a normalised MushiReport.timeline; pair with Mushi.setScreen({ name, route, feature }) for screen-level grouping in the admin
  • Two-way replies (0.11+) — the panel ships a "Your reports" view that polls comments authored by the dev team and lets the reporter reply, all signed with HMAC against the public API key (no auth user required)
  • SDK identity & freshness (0.8+) — every report ships sdkPackage + sdkVersion; the widget polls /v1/sdk/latest-version and surfaces an outdated banner (configurable via widget.outdatedBanner)
  • Self-noise filters (0.7.1+) — internal Mushi requests are tagged with X-Mushi-Internal and excluded from network capture + apiCascade; configurable capture.ignoreUrls and proactive.apiCascade.ignoreUrls for host-app endpoints you also don't want counted
  • Mushi.diagnose() (0.7.1+) — one-call CSP / runtime-config / capture / widget health check (also runs without an init for pre-install smoke tests)
  • Keyboard-first: Esc to close, ⌘/Ctrl + Enter to submit, focus-trapped panel
  • Honours prefers-reduced-motion (animations collapse to instant)

Design language — "Mushi Mushi Editorial"

The widget is intentionally not a generic SaaS chatbot. The visual system is defined in src/styles.ts and uses:

  • Paper + sumi ink — warm washi cream surface (#F8F4ED), deep ink type (#0E0D0B), and a subtle paper grain. No flat white modal.
  • Vermillion 朱 (#E03C2C) — single signature accent, used as a hanko stamp colour for the active state, focus underline, submit button, and the success-step 朱印 ring.
  • System serif display — Iowan Old Style → Palatino → Georgia stack for headings. Pure system fonts: zero web-font fetch, zero FOUT.
  • Mono metadataui-monospace for the 01 / 03 step ledger and receipt timestamp, evoking a printer's contents page.
  • Rule lines, not boxes — categories render as an editorial contents list with 1px hairline separators.

All design tokens are named by material (paper, ink, rule, vermillion) rather than role (primary, secondary) so the palette is hard to dilute via a generic rename later.

Contents

Proactive Manager (proactive-manager.ts)

Controls report prompt frequency to prevent fatigue:

  • maxProactivePerSession (default 2) — cap per browser session
  • dismissCooldownHours (default 24) — suppress after dismissal
  • suppressAfterDismissals (default 3) — permanently disable after N consecutive dismissals
  • Smart dedup — same trigger type not shown twice per session

Proactive Triggers (proactive-triggers.ts)

Auto-detects conditions that should prompt the user:

  • Rage click — 3+ clicks in < 500ms on same element
  • Long task — > 5s main thread block (PerformanceObserver)
  • API cascade — 3+ failed requests in 10s window
  • Error boundary — global window.error and unhandledrejection events (opt-in via errorBoundary: true)

Each trigger respects its config flag — set rageClick: false to disable rage click detection, etc.

Known Limitations

Screenshot capture uses canvas/SVG foreignObject serialization. This 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 22 KB gzipped via size-limit in CI (budget raised for the 2026 editorial widget refresh — still a single-digit-kilobyte footprint). Requires @mushi-mushi/core as a dependency (not bundled inline). The ./test-utils entry is a separate artifact and is never pulled into production bundles.

Quick Start

import { Mushi } from '@mushi-mushi/web';

Mushi.init({
  projectId: 'proj_xxx',
  apiKey: 'your-api-key',
  widget: { position: 'bottom-right', theme: 'auto' },
  capture: { console: true, network: true, screenshot: 'on-report' },
});

Bring your own launcher (trigger: 'attach')

For mature production apps, prefer hosting the launcher inside your own help menu, settings page, or beta banner. Mushi will not inject any 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');
mushi.hide();

Smart-hide (trigger: 'auto' with viewport awareness)

Mushi.init({
  projectId: 'proj_xxx',
  apiKey: 'mushi_xxx',
  widget: {
    trigger: 'auto',
    smartHide: { onMobile: 'edge-tab', onScroll: 'shrink', onIdleMs: 900 },
    inset: { bottom: 96, right: 20 },
    hideOnSelector: '[data-fullscreen-player]',
    hideOnRoutes: ['/checkout/payment'],
    respectSafeArea: true,
  },
});

See Trigger modes for the full posture matrix (auto / edge-tab / attach / manual / hidden).

With Proactive Triggers

Proactive triggers are wired into Mushi.init() automatically when config.proactive is provided. The SDK opens the widget when a trigger fires, gated by fatigue prevention:

Mushi.init({
  projectId: 'proj_xxx',
  apiKey: 'your-api-key',
  proactive: {
    rageClick: true,
    longTask: true,
    apiCascade: true,
    errorBoundary: true,
    cooldown: {
      maxProactivePerSession: 2,
      dismissCooldownHours: 24,
      suppressAfterDismissals: 3,
    },
  },
});

For manual composition (advanced), the lower-level APIs are also exported:

import { createProactiveManager, setupProactiveTriggers } from '@mushi-mushi/web';

const manager = createProactiveManager({ maxProactivePerSession: 2 });

setupProactiveTriggers({
  onTrigger: (type, context) => {
    if (manager.shouldShow(type)) {
      // Custom handling
    }
  },
});

Self-noise filters and CSP diagnostics

Out of the box the SDK tags every request it makes with X-Mushi-Internal and skips those URLs in capture.network + proactive.apiCascade, so an unconfigured CSP or a flaky local Supabase stack can no longer make Mushi report on Mushi:

Mushi.init({
  projectId: 'proj_xxx',
  apiKey: 'mushi_xxx',
  // 'auto' (default) skips the runtime-config fetch on localhost endpoints —
  // pass `true` to force it everywhere, `false` to disable entirely.
  runtimeConfig: 'auto',
  capture: {
    network: true,
    ignoreUrls: [/\/api\/internal\//, 'https://posthog.example.com'],
  },
  proactive: {
    apiCascade: {
      enabled: true,
      ignoreUrls: ['https://feature-flags.example.com'],
    },
  },
});

const health = await Mushi.diagnose();
// → { apiEndpointReachable, cspAllowsEndpoint, widgetMounted, shadowDomAvailable,
//     dialogSupported, runtimeConfigLoaded, captureScreenshotAvailable,
//     captureNetworkIntercepting, sdkVersion }

Mushi.diagnose() works before Mushi.init() too — call it from a debug console or installer wizard to surface CSP / endpoint problems with zero risk of accidentally booting the widget.

Presets and widget anchor

Mushi.init({
  projectId: 'proj_xxx',
  apiKey: 'mushi_xxx',
  // production-calm = manual trigger, screenshot only on report, no proactive prompts
  // beta-loud       = proactive triggers + console + network always on
  // internal-debug  = above + verbose debug + always-on screenshot
  // manual-only     = trigger only, every proactive surface off
  preset: 'production-calm',
  widget: {
    // Raw CSS strings (including `var()` and `env()`) win over `position` /
    // `inset` so the launcher tracks your app shell's tab bars or mini-player.
    anchor: {
      bottom: 'calc(var(--app-dock-h, 0px) + env(safe-area-inset-bottom))',
      right: 'calc(0.75rem + env(safe-area-inset-right))',
    },
    brandFooter: true,
    outdatedBanner: 'auto',
  },
});

Privacy and screenshot redaction

Mushi.init({
  projectId: 'proj_xxx',
  apiKey: 'mushi_xxx',
  privacy: {
    maskSelectors: ['[data-private]', 'input', '.thai-answer-draft'],
    blockSelectors: ['[data-payment]', '[data-auth-token]'],
    allowUserRemoveScreenshot: true,
  },
});

maskSelectors paints a solid block over matching elements before serialisation; blockSelectors removes them entirely. allowUserRemoveScreenshot adds a "Remove screenshot" affordance next to the attachment chip in the panel, so the reporter can yank a screenshot they didn't realise contained sensitive data.

Repro timeline and setScreen()

const mushi = Mushi.init({ /* ... */ });
mushi.setScreen({ name: 'Chat', route: '/chat', feature: 'roleplay' });

The SDK auto-records route (initial + pushState / popstate / hashchange), click (with selector + text snippet), and screen events into a 120-entry ring buffer. Submissions ship the trail as MushiReport.timeline and the admin console renders it as a chronological "what happened before the report" card on /reports/:id.

Two-way replies (Your reports)

The widget mounts a "Your reports" tab that lists this reporter's history, unread admin replies (with a count badge on the trigger), and a reply input. Calls are signed with an HMAC over projectId.timestamp.sha256(reporterToken) using the public API key as the secret, so no Supabase auth is required.

Mushi.init({
  projectId: 'proj_xxx',
  apiKey: 'mushi_xxx',
  widget: { brandFooter: true, outdatedBanner: 'auto' },
});

Endpoints (Edge Function): GET /v1/reporter/reports, GET /v1/reporter/reports/:id/comments, POST /v1/reporter/reports/:id/reply. The DB-side report_comments_fanout_to_reporter trigger creates a reporter_notifications row whenever a visible_to_reporter admin comment lands, so the unread count stays in sync without polling.

Test utilities (./test-utils)

Deterministic Playwright / jsdom helpers, published as a separate entry-point so production bundles pay nothing for them:

import { triggerBug, openReport, waitForQueueDrain } from '@mushi-mushi/web/test-utils';
Export Purpose
triggerBug(opts?) Submit a report bypassing the widget. Returns the server-assigned id.
openReport(cat?) Open the widget programmatically without submitting.
openMushiWidget(cat?) Alias for openReport — Playwright-friendly name for the dogfood contract suite.
waitForQueueDrain Resolve once the offline queue is empty (number remaining at timeout).
expectMushiReady(opts?) Resolve with a MushiDiagnosticsResult once the SDK is initialised and reachable. Fails if apiEndpointReachable === false.
expectNoMushiSelfCascade() Run an action and assert no internal Mushi request fired the api_cascade proactive trigger. Catches CSP / runtime-config self-noise.

Every helper no-ops when Mushi.getInstance() returns null, so conditional-wiring tests (e.g. cloud vs local targets) don't need to branch. For browser-context use in Playwright's page.evaluate, import the SDK via the app's own bundle (window.__mushi__ in dev builds) or POST to /v1/reports directly — page.evaluate has no npm resolver in the browser context.

License

MIT