React Native SDK
The Syntropy React Native SDK captures errors from React Native and Expo applications.
npm install @eclosion-tech/syntropy-react-native
Expo Setup
Initialize as early as possible in your app, before other imports:
// App.tsx - at the very top
import { Syntropy } from '@eclosion-tech/syntropy-react-native';
import * as Application from 'expo-application';
import * as Updates from 'expo-updates';
Syntropy.init({
projectId: 'your-project-id',
// Release tracking
release: Application.nativeApplicationVersion ?? '1.0.0',
dist: Application.nativeBuildVersion ?? undefined,
environment: __DEV__ ? 'development' : 'production',
// OTA updates (Expo)
updateId: Updates.updateId ?? undefined,
updateChannel: Updates.channel ?? undefined,
});
// Rest of your imports
import { StatusBar } from 'expo-status-bar';
import { NavigationContainer } from '@react-navigation/native';
// ...
Bare React Native Setup
// index.js - at the very top
import { Syntropy } from '@eclosion-tech/syntropy-react-native';
import { version } from './package.json';
Syntropy.init({
projectId: 'your-project-id',
release: version,
environment: __DEV__ ? 'development' : 'production',
});
import { AppRegistry } from 'react-native';
import App from './App';
// ...
Configuration Options
Syntropy.init({
// Required
projectId: 'your-project-id',
dsn: 'https://syntropy.chat/api/ingest', // Optional endpoint override
// Release tracking
release: '1.0.0',
dist: '42',
environment: 'production',
updateId: 'expo-update-id', // OTA update ID
updateChannel: 'production', // OTA channel
// Behavior
enabled: true,
autoCapture: true, // Capture unhandled errors
breadcrumbs: true, // Collect breadcrumbs
maxBreadcrumbs: 50,
// Performance
flushInterval: 5000,
maxBatchSize: 20,
sampleRate: 1.0,
// Filtering
beforeSend: (event) => event, // Return null to drop
// Debugging
debug: false,
});
Automatic Error Capture
With autoCapture: true, the SDK captures:
- JavaScript errors via React Native's ErrorUtils
- Unhandled Promise rejections
- Native crashes (requires additional setup)
Manual Error Capture
import { Syntropy } from '@eclosion-tech/syntropy-react-native';
try {
await riskyAsyncOperation();
} catch (error) {
Syntropy.captureError(error, {
tags: { screen: 'Profile' },
extra: { userId: user.id },
});
}
Navigation Breadcrumbs
React Navigation
import { NavigationContainer } from '@react-navigation/native';
import { Syntropy } from '@eclosion-tech/syntropy-react-native';
function App() {
const routeNameRef = useRef<string>();
const navigationRef = useNavigationContainerRef();
return (
<NavigationContainer
ref={navigationRef}
onReady={() => {
routeNameRef.current = navigationRef.getCurrentRoute()?.name;
}}
onStateChange={() => {
const previousRouteName = routeNameRef.current;
const currentRouteName = navigationRef.getCurrentRoute()?.name;
if (previousRouteName !== currentRouteName && currentRouteName) {
Syntropy.addBreadcrumb({
category: 'navigation',
message: `Navigate to ${currentRouteName}`,
data: { from: previousRouteName, to: currentRouteName },
});
}
routeNameRef.current = currentRouteName;
}}
>
{/* ... */}
</NavigationContainer>
);
}
App State Breadcrumbs
Track when the app goes to background/foreground:
import { AppState, AppStateStatus } from 'react-native';
import { Syntropy } from '@eclosion-tech/syntropy-react-native';
useEffect(() => {
const subscription = AppState.addEventListener('change', (state: AppStateStatus) => {
Syntropy.addBreadcrumb({
category: 'custom',
message: `App state: ${state}`,
data: { state },
});
});
return () => subscription.remove();
}, []);
User Identification
// After login
Syntropy.identify({
id: user.id,
email: user.email,
name: user.displayName,
});
// After logout
Syntropy.reset();
OTA Update Tracking
For Expo apps using EAS Update or classic updates:
import * as Updates from 'expo-updates';
Syntropy.init({
projectId: 'your-project-id',
release: Application.nativeApplicationVersion ?? '1.0.0',
updateId: Updates.updateId ?? undefined,
updateChannel: Updates.channel ?? undefined,
});
This allows you to see if an error was introduced by:
- A native build update (new
release) - An OTA JavaScript update (new
updateId)
Error Boundaries
Wrap your app in an error boundary:
import { Syntropy } from '@eclosion-tech/syntropy-react-native';
class ErrorBoundary extends React.Component {
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
Syntropy.captureError(error, {
extra: { componentStack: errorInfo.componentStack },
});
}
render() {
if (this.state.hasError) {
return <ErrorFallback />;
}
return this.props.children;
}
}
Debugging
Enable debug mode to see SDK activity:
Syntropy.init({
projectId: 'your-project-id',
debug: __DEV__, // Only in development
});
This logs all SDK activity to the console.
Conversation Events
Syntropy.captureConversation('conversation_started', {
conversationId: 'conv_123',
channel: 'mobile_chat',
});
Customer APIs from React Native (Public Client)
Use createSyntropyPublicClient with a publishable key (syn_pk_) for client-side customer operations. Publishable keys are safe to embed in your app -- they only allow identify, track, and device registration.
import { createSyntropyPublicClient } from '@eclosion-tech/syntropy-react-native';
const customers = createSyntropyPublicClient({
accountId: 'your_project_uuid',
publishableKey: 'syn_pk_xxx',
baseUrl: 'https://syntropy.chat/api',
});
// Identify (upsert) a customer
await customers.identify({
externalId: 'user_123',
email: 'user@example.com',
metadata: { source: 'mobile' },
});
// Track an event
await customers.track({
name: 'purchase_completed',
externalId: 'user_123',
payload: { amount: 99.99, currency: 'USD' },
});
// Register a device for push notifications
await customers.registerDevice({
externalId: 'user_123',
platform: 'ios',
pushToken: 'expo_push_token_xxx',
active: true,
});
Create publishable keys in the dashboard under Settings > API Keys by selecting the "Publishable" key type.
Server-Side Customer APIs
For full admin access (listing customers, segments, CSV import, etc.), use createSyntropyApiClient with a secret key (syn_sk_) from your backend:
import { createSyntropyApiClient } from '@eclosion-tech/syntropy-react-native';
const api = createSyntropyApiClient({
baseUrl: 'https://syntropy.chat/api',
apiKey: 'syn_sk_xxx',
});
await api.customers.upsert(
{ organizationId: 'org_uuid', projectId: 'project_uuid' },
{
externalId: 'user_123',
email: 'user@example.com',
metadata: { source: 'mobile' },
}
);
See SDK API Client for full API coverage.
Push Notification Delivery
Once a device is registered, Syntropy can deliver push notifications through workflows. Configure push credentials in your project dashboard under Settings > Push.
Supported Providers
| Provider | Platform | Token Format |
|---|---|---|
| Apple APNs | iOS | Native APNs device token (hex string) |
| Firebase FCM | Android | FCM registration token |
| Expo Push | iOS / Android | ExponentPushToken[...] |
Apple APNs Setup
- In the Apple Developer portal, create an APNs Authentication Key (.p8 file)
- Note your Key ID, Team ID, and app Bundle ID
- In the Syntropy dashboard, go to Settings > Push and add an APNs provider
- Upload the .p8 file and fill in the Key ID, Team ID, and Bundle ID
- Select Production or Sandbox environment
Firebase FCM Setup
- In the Firebase Console, go to Project Settings > Service Accounts
- Click Generate New Private Key to download a service account JSON file
- In the Syntropy dashboard, go to Settings > Push and add an FCM provider
- Upload the service account JSON file
Expo Push Setup
- In the Syntropy dashboard, go to Settings > Push and add an Expo provider
- Optionally provide an Expo access token for higher rate limits
- Register devices using
ExponentPushToken[...]tokens fromexpo-notifications
Token Lifecycle
Syntropy automatically manages device token validity:
- When APNs returns a
410 GoneorUnregisteredresponse, the device is marked inactive - When FCM returns
UNREGISTERED, the device is marked inactive - When Expo returns
DeviceNotRegistered, the device is marked inactive - Inactive devices are excluded from future push deliveries
- Devices can be re-activated by calling
registerDeviceagain with a fresh token