Skip to Content
v1.33.8 · shipped One-screen report · Looks like your app Read the changelog →
SDK reference@mushi-mushi/react-native

@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-native

Optional peer dependencies. Install the ones whose features you want:

PeerEnables
@react-native-async-storage/async-storageOffline report queue (recommended for production)
@react-navigation/nativeAuto-captured navigation timeline via useNavigationCapture()
react-native-view-shotScreenshot on widget open (optional; see Screenshots)
expo-sensorsShake-to-report trigger
expo-secure-storeEncrypts the offline queue and keeps the reporter token in the keychain/keystore (storage.secureStorage, on by default when installed)
@react-native-community/netinfoSends 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 install

Mount the provider

App.tsx
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):

FieldDescription
sessionIdPer-launch UUID; groups all reports from one app open
sdkPackage / sdkVersionPackage name + build-time version stamp
appVersionNative app version from device info
fingerprintHashStable RN device hash (rnfp_* prefix) for anti-gaming
breadcrumbsRing buffer (default 50) of console, navigation, and host events
timelineDerived repro trail rendered in the admin Repro timeline card
metadata.userNested reporter identity; see Identifying users
screenshotDataUrlOptional 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` entry

Reporter 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 / userProvider

Server-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 */) }
MethodReturns
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.

navigation/AuthGate.tsx
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

App.tsx
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.

navigation/RootNavigator.tsx
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.

Last updated on