Skip to Content
v0.8.0 · shippedNative mobile SDKs, optional Sentry enrichment, and bring-your-own keys/storage. Read the changelog →
SDK reference@mushi-mushi/react-native

@mushi-mushi/react-native

Let testers shake their phone when something feels broken — the native SDK 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

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 { submit, submitting } = useMushiReport() return ( <Button title={submitting ? 'Sending…' : 'Report bad response'} onPress={() => submit({ description: 'AI returned an off-topic answer', severity: 'medium', metadata: { screen: 'chat' }, }) } /> ) }

Offline queue

Reports submitted while offline are buffered in AsyncStorage (key @mushi:offline_queue, default cap 50 items) and replayed on the next provider mount or whenever you call useMushi().flush().

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 (1.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).

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, // auto-captures screen changes via setScreen() consentMode: 'explicit', showInWidget: true, }, }} > <AuthGate> <RootNavigator /> </AuthGate> </MushiProvider> ) }

Wiring screen tracking

Call setScreen() on every navigation event to feed page_view and navigate activity events automatically:

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(routeName) }} > {/* … */} </NavigationContainer> ) }

Custom activity events

import { useMushi } from '@mushi-mushi/react-native' function LessonScreen({ lessonId }) { const sdk = useMushi() const onComplete = () => { sdk?.submitActivity([ { action: 'lesson_complete', metadata: { 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