@mushi-mushi/web
Catch what users felt — screenshots, console logs, and a shake-to-report widget — without bolting on another dashboard. The browser SDK runs in a Shadow DOM so your CSS never leaks in or out.
Migrating from another bug-capture tool? We have step-by-step guides for Instabug / Luciq, Shake, LogRocket Feedback, BugHerd, and Pendo Feedback. Each maps the competitor’s API to the Mushi equivalent and includes an interactive checklist.
See Quickstart → Vanilla JS for setup. Notable extras:
onProactiveTrigger(({ context }) => …)— fires when the SDK detects user friction (rage clicks, repeated navigation, console errors during the same interaction). Use it to surface the report widget contextually.onBeforeSubmit((report) => report | null)— last-mile transform. Returnnullto drop the report client-side.pii— built-in scrubber masks emails, phones, SSNs, credit-card-shaped strings, and JWTs by default. Add custom regex viapii.customPatterns.
Screenshot preview & consent (1.19+)
When screenshot capture is enabled (capture.screenshot: 'on-report' or
'auto'), the details step shows a visible preview of the image that will
be attached — not just a “Screenshot attached ✓” label. Reporters can Remove
the screenshot before submit and optionally read a privacy caption beneath
the preview.
Mushi.init({
projectId: '00000000-0000-0000-0000-000000000000', // UUID from Projects page
apiKey: 'mushi_xxxxxxxxxxxxxxxxxxxxxxxxxxxx', // report:write key
widget: {
// true → localized default caption (en/es/ja/th)
// string → your compliance copy verbatim
// false → hide caption (preview + Remove still show)
screenshotSensitiveHint: true,
},
capture: { screenshot: 'on-report' },
})Tune the caption from the admin console (Projects → SDK install → Screenshot
privacy caption) without rebuilding — it travels in GET /v1/sdk/config under
widget.screenshotSensitiveHint.
The preview stays in sync if the reporter uses Mark up (blur/highlight).
Keep img-src data: in your CSP when using screenshots on locked-down pages.
See the Next.js App Router + CSP recipe.
Maintainer deep-dive: SDK_SCREENSHOT_PREVIEW.md
Identifying users & the Rewards program
Call identify() as soon as your auth state resolves. The SDK links all
subsequent reports and activity events to that user identity server-side.
// On login / auth state change
const { user } = await supabase.auth.getUser()
if (user) {
mushi.identify(user.id, {
email: user.email,
name: user.user_metadata?.full_name,
provider: 'supabase',
})
}identify() is idempotent — calling it again with the same userId updates
the stored traits. Calling it with a new userId flushes any buffered events
for the previous session first.
Enabling the Rewards program
Add a rewards block to your init() config:
import { Mushi } from '@mushi-mushi/web'
const mushi = Mushi.init({
projectId: 'YOUR_PROJECT_ID',
apiKey: 'YOUR_API_KEY',
rewards: {
enabled: true,
trackActivity: true, // auto-capture page_view, navigate, session_start
consentMode: 'explicit', // 'explicit' | 'auto'
showInWidget: true, // show tier + points in the bug-report widget
showNotifications: true, // "+X pts" toast on each award
flushIntervalMs: 300_000, // how often to POST activity (min 30s)
},
})trackActivity: true automatically captures:
| Action | Trigger |
|---|---|
session_start | First SDK init after 30 min idle, capped 3×/day |
page_view | Every history.pushState / popstate event |
navigate | <a> clicks and programmatic router pushes |
button_press | Clicks on [data-mushi-track] or [data-testid] elements |
For custom actions:
mushi.submitActivity([
{ action: 'lesson_complete', metadata: { lessonId: 'l_123', score: 0.9 } },
])Querying points & tier
// Current user's points
const points = await mushi.getReputation()
// → { totalPoints, points30d, reputation, confirmedBugs, totalReports }
// Current tier
const tier = await mushi.getTier()
// → { id, slug, displayName, pointsThreshold, perks } | nullSee Concepts → Rewards & contributor identity for the full data model, anti-gaming integration, and webhook reference.
Reporter API
Let reporters see and follow up on their own submissions without a login. Every
method is keyed to the persistent anonymous reporterToken the SDK stores in
localStorage, so no auth wiring is required.
// The reporter's own report history (newest first)
const reports = await mushi.listMyReports()
// → MushiReporterReport[] (each carries `unread_count` for a badge)
// The comment thread on one of their reports
const comments = await mushi.listMyComments(reportId)
// → MushiReporterComment[] (their comments + team replies)
// Post a follow-up comment on their own report
const comment = await mushi.replyToReport(reportId, 'Still happening on iOS 18')
// → MushiReporterComment | null
// The project's public contributor leaderboard
const leaders = await mushi.getHallOfFame(10)
// → MushiHallOfFameEntry[] (display_name, tier_name, points_30d, total_points)All four methods fail soft: they return [] / null (never throw) when the
network is down or no reporter token exists yet, so they’re safe to call on
first render.
React
The same methods are exposed on the useMushi() hook, memoised so they’re
stable across renders:
import { useMushi } from '@mushi-mushi/react'
function MyReports() {
const { listMyReports, replyToReport } = useMushi()
// ... call inside an effect or event handler
}useMushi() returns no-op fallbacks (() => Promise.resolve([])) before the
SDK finishes initialising, so you never need to null-check the instance.