@mushi-mushi/react-native
The Mushi Mushi React Native SDK lets testers shake their phone when something feels broken. It captures console logs, network errors, and screenshots, then queues reports offline until connectivity returns. Works in bare React Native CLI projects and Expo.
import { MushiProvider, useMushi, useMushiReport } from '@mushi-mushi/react-native'Install
npm install @mushi-mushi/react-nativeOptional peer dependencies. Install the ones whose features you want:
| Peer | Enables |
|---|---|
@react-native-async-storage/async-storage | Offline report queue (recommended for production) |
@react-navigation/native | Auto-captured navigation timeline via useNavigationCapture() |
react-native-view-shot | Screenshot on widget open (optional; see Screenshots) |
expo-sensors | Shake-to-report trigger |
expo-secure-store | Encrypts the offline queue and keeps the reporter token in the keychain/keystore (storage.secureStorage, on by default when installed) |
@react-native-community/netinfo | Sends queued reports as soon as the device is back online |
For shake-to-report on a bare RN CLI project, install Expo modules first:
npx install-expo-modules@latest
npm install expo-sensors
cd ios && bundle exec pod installMount the provider
import { MushiProvider } from '@mushi-mushi/react-native'
export default function App() {
return (
<MushiProvider
projectId="YOUR_PROJECT_ID"
apiKey="YOUR_PUBLIC_API_KEY"
config={{
widget: { trigger: 'both' }, // 'shake' | 'button' | 'both' | 'manual'
capture: { console: true, network: true },
}}
>
<RootNavigator />
</MushiProvider>
)
}A floating bug button is rendered automatically for trigger: 'button' | 'both' | 'auto'.
Set trigger: 'manual' if you want to drive the widget yourself.
Submit a report from a screen
import { useMushiReport, useMushiWidget } from '@mushi-mushi/react-native'
function ChatScreen() {
const { open } = useMushiWidget()
const { submitReport } = useMushiReport()
return (
<Button
title="Report bad response"
onPress={() =>
submitReport({
description: 'AI returned an off-topic answer',
category: 'bug',
})
}
/>
)
}Offline queue
Reports submitted while offline are buffered in AsyncStorage (key
@mushi:offline_queue_enc, encrypted when expo-secure-store is available;
default cap 50 items, oldest dropped first). They are replayed on the next
provider mount and, with @react-native-community/netinfo installed, as soon
as the connection returns. There is no manual flush method.
For pure-native projects with no JS bundle, use the iOS or Android SDKs directly. Capacitor users have a dedicated plugin; see Capacitor → React Native migration if you’re moving from one to the other.
See Quickstart → React Native for the end-to-end setup.
What gets captured (v0.19+)
Every report submitted through the bottom sheet or submitReport() carries
Sentry-grade context when running SDK ≥ 0.17.0 (screenshot preview caption
added in 0.19.0):
| Field | Description |
|---|---|
sessionId | Per-launch UUID; groups all reports from one app open |
sdkPackage / sdkVersion | Package name + build-time version stamp |
appVersion | Native app version from device info |
fingerprintHash | Stable RN device hash (rnfp_* prefix) for anti-gaming |
breadcrumbs | Ring buffer (default 50) of console, navigation, and host events |
timeline | Derived repro trail rendered in the admin Repro timeline card |
metadata.user | Nested reporter identity; see Identifying users |
screenshotDataUrl | Optional JPEG data-URI when capture is enabled |
Configure the ring buffer and screenshot gate under config.capture:
<MushiProvider
projectId="YOUR_PROJECT_ID"
apiKey="YOUR_PUBLIC_API_KEY"
config={{
capture: {
console: true,
network: true,
screenshot: true, // default; set false on sensitive screens
maxBreadcrumbs: 50,
},
}}
>
<RootNavigator />
</MushiProvider>Screenshots (react-native-view-shot)
Install the optional peer when you want auto-capture on widget open:
npx expo install react-native-view-shot
# v4.x works on Expo SDK 55; v5+ recommended for New Architecture (Fabric)The SDK captures before the bottom sheet overlays the screen. Users see a thumbnail preview, can Remove it before submit, and read an optional privacy caption under the image (0.19+).
<MushiProvider
config={{
widget: {
screenshotSensitiveHint: true, // default caption
// screenshotSensitiveHint: 'Remove if account numbers are visible.',
// screenshotSensitiveHint: false, // hide caption; preview still shows
},
capture: { screenshot: true },
}}
>Console operators can override the caption via Projects → SDK install → Screenshot privacy caption (runtime config, no app rebuild).
Theming the report sheet
The report sheet takes the host’s look through widget.theme. Every token is optional; unset tokens use neutral defaults with an ink accent, and when only accent is set the text on it is picked for contrast.
<MushiProvider
config={{
widget: {
theme: { accent: '#C9A227', fontFamily: 'Georgia', radius: 8 },
// also: bg, fg, muted, surface, border, accentFg, success, error
},
}}
>“Your reports” shows the same status labels as the web widget (“Received”, “Looking into it”, “Fix in progress”, “Fixed in v1.4”), from @mushi-mushi/core/reporter-ui. A thread that fails or does not answer within 12 s shows Retry, and opening a thread marks its updates read.
On finance/health apps, keeping capture on with a clear caption is often better
than disabling screenshots entirely. For screens that must never be captured,
use capture.screenshot: false while those routes are focused.
Maintainer deep-dive: SDK_SCREENSHOT_PREVIEW.md
Manual breadcrumbs
const sdk = useMushi()
sdk?.addBreadcrumb({
category: 'user',
message: 'Opened FX conversion sheet',
level: 'info',
})
sdk?.setScreen({ name: 'accounts/[id]', route: '/accounts/abc' })
// ^ also emits a navigation breadcrumb → timeline `route` entryReporter identity contract
Call identify() or the web-parity setUser() alias whenever auth state changes.
The SDK emits both shapes for back-compat:
// Canonical (server reads this first)
metadata.user = { id, email, name, provider }
// Legacy flat keys (RN ≤ 0.16)
metadata.userId / userEmail / userName / userProviderServer-side resolveReporterIdentity() in the ingest path reads nested or flat
keys plus Sentry scope user, then links reports.reporter_user_id to end_users.
A signed-in Supabase user should never appear as an anonymous token hash on the
report detail page when identity is wired correctly.
sdk?.setUser({
id: session.user.id,
email: session.user.email,
name: session.user.user_metadata?.full_name ?? session.user.email?.split('@')[0],
provider: 'supabase',
})Only a SHA-256 hash of the email is stored at rest; the console shows name as the
human-facing reporter label.
End-user progress (in-app inbox)
The bottom sheet (v0.17+) includes Your reports and Community tabs. Host apps can also surface the same data on a dedicated screen:
import { useMushi } from '@mushi-mushi/react-native'
import { useEffect, useState } from 'react'
function MyReportsScreen() {
const sdk = useMushi()
const [reports, setReports] = useState([])
const [reputation, setReputation] = useState(null)
useEffect(() => {
if (!sdk) return
Promise.all([sdk.listMyReports(), sdk.getReputation()])
.then(([r, rep]) => { setReports(r); setReputation(rep) })
}, [sdk])
return (/* render status pills + points */)
}| Method | Returns |
|---|---|
listMyReports() | User’s submitted reports with status, category, timestamps |
listMyComments(reportId) | Thread between reporter and admin |
replyToReport(reportId, body) | Post a follow-up from the app |
getReputation() / getTier() | Points balance and tier for contribution UI |
getHallOfFame(limit?) | Leaderboard entries for community context |
yen-yen reference: apps/mobile/app/feedback.tsx renders contribution +
My reports sections below the native Supabase feedback inbox.
Identifying users
Call identify() inside your auth state listener. On bare React Native this
is typically a Supabase onAuthStateChange subscriber; on Expo it’s wherever
you consume the session from your auth context.
import { useMushi } from '@mushi-mushi/react-native'
import { useEffect } from 'react'
import { supabase } from '../lib/supabase'
export function AuthGate({ children }) {
const sdk = useMushi()
useEffect(() => {
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(_event, session) => {
if (session?.user) {
sdk?.setUser({
id: session.user.id,
email: session.user.email,
name: session.user.user_metadata?.full_name,
provider: 'supabase',
})
// equivalent: sdk?.identify(session.user.id, { email, name, provider })
}
}
)
return () => subscription.unsubscribe()
}, [sdk])
return children
}identify() is idempotent. Calling it with the same userId on each session
restore is safe: the server upserts the end_users row and updates
last_seen_at.
Enabling the Rewards program
import { MushiProvider } from '@mushi-mushi/react-native'
export default function App() {
return (
<MushiProvider
projectId="YOUR_PROJECT_ID"
apiKey="YOUR_PUBLIC_API_KEY"
config={{
widget: { trigger: 'both' },
capture: { console: true, network: true },
rewards: {
enabled: true,
trackActivity: true, // required on React Native: without it no activity is sent
consentMode: 'explicit',
showInWidget: true,
},
}}
>
<AuthGate>
<RootNavigator />
</AuthGate>
</MushiProvider>
)
}On React Native, trackActivity: true starts the timer that sends queued
activity (every flushIntervalMs, default 5 minutes, minimum 30 seconds). It
does not record anything by itself; activity comes from recordActivity()
calls (below).
Wiring screen tracking
Call setScreen() on every navigation event. It adds a navigation breadcrumb
for the repro timeline and a page view for product analytics.
It does not record rewards activity.
import { useMushi } from '@mushi-mushi/react-native'
import { NavigationContainer } from '@react-navigation/native'
export function RootNavigator() {
const sdk = useMushi()
return (
<NavigationContainer
onStateChange={(state) => {
const routeName = getActiveRouteName(state)
sdk?.setScreen({ name: routeName })
}}
>
{/* … */}
</NavigationContainer>
)
}Custom activity events
import { useMushi } from '@mushi-mushi/react-native'
function LessonScreen({ lessonId }) {
const sdk = useMushi()
const onComplete = () => {
sdk?.recordActivity('lesson_complete', { lessonId })
}
return <Button title="Complete" onPress={onComplete} />
}Querying reputation & tier
import { useMushi } from '@mushi-mushi/react-native'
import { useEffect, useState } from 'react'
function ProfileBadge() {
const sdk = useMushi()
const [tier, setTier] = useState(null)
useEffect(() => {
sdk?.getTier().then(setTier)
}, [sdk])
if (!tier) return null
return <Text>{tier.displayName}</Text>
}The MushiRewardsBadge component from @mushi-mushi/react is web-only
(uses <span>). On React Native, render tier data from getTier() and
getReputation() directly using your own <Text> / <View> elements.