BugSplat's @bugsplat/expo package provides crash and error reporting for Expo apps across iOS, Android, and Web. BugSplat provides you with invaluable insight into the issues tripping up your users. Our Expo integration collects native crash reports, JavaScript errors, and custom metadata so that you can fix bugs and deliver a better user experience.
npx expo install @bugsplat/expoAdd the config plugin to your app.json or app.config.js:
Credentials for symbol upload can be set via environment variables (BUGSPLAT_CLIENT_ID, BUGSPLAT_CLIENT_SECRET) or in the plugin config.
{
"expo": {
"plugins": [
["@bugsplat/expo", {
"database": "your-database",
"enableSymbolUpload": true
}],
["expo-build-properties", {
"android": {
"minSdkVersion": 26
}
}]
]
}
}The bugsplat-android SDK requires Android minSdk 26 (Android 8.0+). If your project's minSdk is already >= 26, the expo-build-properties plugin is not needed.
The plugin sets up required native permissions (Android) and optionally configures automatic symbol uploads for both platforms. Configure your database in code via init().
| Option | Required | Description |
|---|---|---|
database |
No | BugSplat database name (can also be set via init() or BUGSPLAT_DATABASE env var) |
enableSymbolUpload |
No | Enable automatic symbol upload for iOS (dSYMs) and Android (.so files) |
symbolUploadClientId |
No | BugSplat API client ID (or set BUGSPLAT_CLIENT_ID env var) |
symbolUploadClientSecret |
No | BugSplat API client secret (or set BUGSPLAT_CLIENT_SECRET env var) |
Production crash reports require debug symbols to produce readable stack traces. When enableSymbolUpload is set, the config plugin automatically uploads symbols during iOS and Android release builds.
For manual uploads or CI/CD workflows, use @bugsplat/symbol-upload directly:
npm install --save-dev @bugsplat/symbol-upload# Upload iOS dSYMs
npx @bugsplat/symbol-upload \
-b your-database -a YourApp -v 1.0.0 \
-i $BUGSPLAT_CLIENT_ID -s $BUGSPLAT_CLIENT_SECRET \
-d /path/to/build/Products/Release-iphoneos \
-f "**/*.dSYM"
# Upload Android .so files (converted to .sym)
npx @bugsplat/symbol-upload \
-b your-database -a YourApp -v 1.0.0 \
-i $BUGSPLAT_CLIENT_ID -s $BUGSPLAT_CLIENT_SECRET \
-d android/app/build/intermediates/merged_native_libs \
-f "**/*.so" -m
# Upload JavaScript source maps (after npx expo export --source-maps)
npx @bugsplat/symbol-upload \
-b your-database -a YourApp -v 1.0.0 \
-i $BUGSPLAT_CLIENT_ID -s $BUGSPLAT_CLIENT_SECRET \
-d dist \
-f "**/*.map"Run npx @bugsplat/symbol-upload --help for all options.
import { init } from '@bugsplat/expo';
await init('your-database', 'YourApp', '1.0.0', {
userName: 'user@example.com',
userEmail: 'user@example.com',
appKey: 'optional-key',
});import { post } from '@bugsplat/expo';
try {
riskyOperation();
} catch (error) {
const result = await post(error);
console.log(result.success ? 'Reported!' : result.error);
}import { setUser } from '@bugsplat/expo';
setUser('Jane Doe', 'jane@example.com');import { setAttribute } from '@bugsplat/expo';
setAttribute('environment', 'production');import { crash } from '@bugsplat/expo';
// Triggers a native crash (iOS/Android) or throws an error (web)
crash();Wrap your component tree in <ErrorBoundary> to catch React render errors and report them to BugSplat automatically. Works identically on iOS, Android, and Web.
import { ErrorBoundary } from '@bugsplat/expo';
function App() {
return (
<ErrorBoundary fallback={<Text>Something went wrong</Text>}>
<MyComponent />
</ErrorBoundary>
);
}The fallback prop accepts a React node or a render function:
<ErrorBoundary
fallback={({ error, resetErrorBoundary }) => (
<View>
<Text>{error.message}</Text>
<Button title="Try again" onPress={resetErrorBoundary} />
</View>
)}
>
<MyComponent />
</ErrorBoundary>By default, <ErrorBoundary> posts to BugSplat the moment it catches an error. If you'd rather give the user a chance to describe what they were doing first — and bundle that into a single report instead of two — set disablePost on the boundary and post manually from your fallback:
<ErrorBoundary> is a class component, so hooks can't live directly inside its fallback render prop — extract the fallback into its own functional component:
import { useRef, useState } from 'react';
import { ErrorBoundary, post, type FallbackProps } from '@bugsplat/expo';
import { Text, TextInput, Button, View } from 'react-native';
function ErrorFallback({ error, componentStack, resetErrorBoundary }: FallbackProps) {
const [description, setDescription] = useState('');
const posted = useRef(false);
const submit = async () => {
if (posted.current) return;
posted.current = true;
await post(error, {
description,
attributes: { route: 'tasks/123' },
attachments: componentStack
? [{
filename: 'componentStack.txt',
data: new Blob([componentStack], { type: 'text/plain' }),
}]
: undefined,
});
};
return (
<View>
<Text>Something went wrong: {error.message}</Text>
<TextInput value={description} onChangeText={setDescription} />
<Button title="Submit" onPress={submit} />
<Button title="Dismiss" onPress={() => { submit(); resetErrorBoundary(); }} />
</View>
);
}
<ErrorBoundary disablePost fallback={(props) => <ErrorFallback {...props} />}>
<App />
</ErrorBoundary>A few notes on this pattern:
post()is not idempotent. TheuseRefguard is the consumer's responsibility — without it, a fast double-tap (or "Submit then Dismiss") would fire two reports.useRefupdates synchronously, so it guards taps that land in the same render window;useStatewould not.componentStackis wrapped in aBlob. This works cross-platform because@bugsplat/expoincludesexpo-blob, which polyfills the web-standardBlobAPI on native.attributesbecomes a queryable column in the BugSplat dashboard — useful for filtering crashes by route, feature flag, build channel, etc.- If posting fails and you want retry, check the
successproperty of the value returned bypost()and resetposted.currentaccordingly. The recipe doesn't show this to keep it minimal.
Submit user feedback tied to your BugSplat database. Works on iOS, Android, and Web.
Imperative API — call from anywhere after init():
import { postFeedback } from '@bugsplat/expo';
const result = await postFeedback('Login button broken', {
description: 'Nothing happens when I tap sign in',
});
console.log(result.success ? `Feedback #${result.crashId} posted` : result.error);React hook — useful for driving a feedback form with built-in loading/error state:
import { useFeedback } from '@bugsplat/expo';
function FeedbackForm() {
const { postFeedback, loading, error } = useFeedback();
return (
<Button
title={loading ? 'Sending…' : 'Send feedback'}
disabled={loading}
onPress={() =>
postFeedback('Login button broken', {
description: 'Nothing happens when I tap sign in',
})
}
/>
);
}| Platform | Native Crashes | JS Error Reporting |
|---|---|---|
| iOS | bugsplat-apple (PLCrashReporter) | HTTP POST to /post/js/ |
| Android | bugsplat-android (Crashpad) | HTTP POST to /post/js/ |
| Web | N/A | bugsplat-js |
Initialize BugSplat crash reporting. Must be called before other functions.
Options:
appKey?: string- Queryable metadata keyuserName?: string- User name for reportsuserEmail?: string- User email for reportsautoSubmitCrashReport?: boolean- Auto-submit crashes (iOS only, default: true)attributes?: Record<string, string>- Custom key-value attributesattachments?: string[]- File paths to attach (native only)description?: string- Default description
Manually report an error. Returns { success: boolean, error?: string }.
Submit user feedback. title is a short summary (required); options.description holds the longer body. Returns { success: boolean, error?: string, crashId?: number }.
React hook returning { postFeedback, loading, response, error } for driving feedback forms.
React error boundary that reports render errors to BugSplat automatically. Accepts a fallback (ReactNode or render function receiving { error, componentStack, response, resetErrorBoundary }).
Update user info for subsequent reports.
Set a custom attribute. Note: not supported on web.
Trigger a test crash to verify integration.
@bugsplat/expo works in Expo Go with reduced functionality. Since native modules are not available in Expo Go, native crash reporting is disabled. JS error reporting (init(), post(), postFeedback(), setUser(), setAttribute(), ErrorBoundary) still works via an HTTP fallback. A warning is logged at init() to let you know native crash reporting is inactive.
To test full native crash reporting, use a release build (see Testing Native Crashes below). Development builds include a debugger that intercepts crashes before BugSplat can capture them.
To test native crash reporting, you must run a release build — the debugger intercepts crashes in debug builds.
# iOS
npx expo run:ios --configuration Release
# Android
npx expo run:android --variant releaseiOS: Crash reports are captured at crash time by PLCrashReporter and uploaded on the next app launch when init() is called again. After triggering a test crash, relaunch the app and call init() to upload the pending report.
Android: Crash reports are captured and uploaded immediately at crash time by the Crashpad handler process.
The Crashpad handler process requires native libraries to be extracted to disk. The @bugsplat/expo config plugin sets extractNativeLibs=true automatically. If you're still not seeing crashes:
- Use a
google_apisemulator image (notgoogle_apis_playstore). The Play Store emulator images have restrictions that prevent Crashpad's handler process from executing. - Alternatively, test on a physical Android device where this is not an issue.
Make sure you relaunch the app and call init() again after the crash. PLCrashReporter saves the crash to disk and uploads it on the next launch.
MIT
