Skip to content

Commit 864e730

Browse files
committed
Add React Native Expo support
1 parent daf40ff commit 864e730

53 files changed

Lines changed: 9063 additions & 367 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.prettierignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
node_modules
44
minver
55
example/svelte-kit/.svelte-kit
6+
example/expo/.expo
7+
example/expo/android
8+
example/expo/ios
69

710
# Ignore files for PNPM, NPM and YARN
811
package-lock.json

.vscode/launch.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
{
22
"version": "0.2.0",
33
"configurations": [
4+
{
5+
"name": "Expo iOS Example",
6+
"request": "launch",
7+
"type": "node",
8+
"runtimeExecutable": "npm",
9+
"runtimeArgs": ["run", "ios:ipad", "--workspace=example/expo"],
10+
"console": "integratedTerminal",
11+
"internalConsoleOptions": "neverOpen",
12+
"cwd": "${workspaceRoot}",
13+
"skipFiles": ["<node_internals>/**"]
14+
},
415
{
516
"name": "Express",
617
"program": "${workspaceRoot}/example/express/app.js",

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,29 @@ try {
6363
}
6464
```
6565

66+
## React Native / Expo
67+
68+
You can install the npm package via
69+
`npm install @exceptionless/react-native @react-native-async-storage/async-storage`.
70+
Next, you just need to call startup during your apps startup to automatically
71+
capture unhandled errors, promise rejections, and native iOS crashes.
72+
73+
```tsx
74+
import { Exceptionless, toError } from "@exceptionless/react-native";
75+
76+
await Exceptionless.startup((c) => {
77+
c.apiKey = "API_KEY_HERE";
78+
c.setUserIdentity("12345678", "Blake");
79+
c.defaultTags.push("Example", "React Native");
80+
});
81+
82+
try {
83+
throw new Error("test");
84+
} catch (error) {
85+
await Exceptionless.submitException(toError(error));
86+
}
87+
```
88+
6689
## Using Exceptionless
6790

6891
### Installation

eslint.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import vitest from "@vitest/eslint-plugin";
55
import tseslint from "typescript-eslint";
66

77
export default defineConfig(
8-
{ ignores: ["**/dist/", "**/node_modules/", ".agents/", "example/"] },
8+
{ ignores: ["**/dist/", "**/node_modules/", ".agents/", "example/", "**/expo-plugin/", "**/react-native.config.*"] },
99
eslint.configs.recommended,
1010
{
1111
extends: tseslint.configs.recommendedTypeChecked,

example/expo/.gitignore

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
node_modules/
2+
.expo/
3+
dist/
4+
ios/
5+
android/
6+
*.xcworkspace
7+
Pods/
8+
9+
# @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb
10+
# The following patterns were generated by expo-cli
11+
12+
expo-env.d.ts
13+
# @end expo-cli

example/expo/App.tsx

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
2+
import { NavigationContainer } from "@react-navigation/native";
3+
import Constants from "expo-constants";
4+
import { GlassView } from "expo-glass-effect";
5+
import { StatusBar } from "expo-status-bar";
6+
import { useEffect, useMemo, useState } from "react";
7+
import { Platform, StyleSheet, Text, View } from "react-native";
8+
import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
9+
import { Exceptionless } from "@exceptionless/react-native";
10+
11+
import { callbackLog, getLogEntries, subscribeToLogs } from "./logging";
12+
import ErrorsScreen from "./screens/ErrorsScreen";
13+
import EventsScreen from "./screens/EventsScreen";
14+
import LogsScreen from "./screens/LogsScreen";
15+
16+
type TabParamList = {
17+
Errors: undefined;
18+
Events: undefined;
19+
Logs: undefined;
20+
};
21+
22+
const Tab = createBottomTabNavigator<TabParamList>();
23+
24+
const serverUrl = getServerUrl();
25+
26+
function TabIcon({ label, focused }: { label: string; focused: boolean }) {
27+
return <Text style={[styles.tabIcon, focused && styles.tabIconFocused]}>{label}</Text>;
28+
}
29+
30+
/**
31+
* Resolves the dev server URL based on the current platform.
32+
* - Web: localhost works directly.
33+
* - iOS Simulator: shares the host Mac's network.
34+
* - Real iOS device: needs the dev machine's IP, extracted from Expo's hostUri.
35+
*/
36+
function getServerUrl(): string {
37+
if (__DEV__ && Platform.OS !== "web") {
38+
const hostUri = Constants.expoConfig?.hostUri;
39+
if (hostUri) {
40+
try {
41+
const hostname = new URL(`http://${hostUri}`).hostname;
42+
return `http://${hostname}:7110`;
43+
} catch {
44+
// Fall through to default
45+
}
46+
}
47+
}
48+
return "http://localhost:7110";
49+
}
50+
51+
function TopDiagnostics() {
52+
const [logs, setLogs] = useState(() => getLogEntries());
53+
const latestLog = logs.at(-1);
54+
const errorCount = useMemo(() => logs.filter((entry) => entry.level === "error").length, [logs]);
55+
56+
useEffect(() => subscribeToLogs(() => setLogs(getLogEntries())), []);
57+
58+
return (
59+
<SafeAreaView edges={["top"]} style={styles.diagnosticsSafeArea}>
60+
<View style={styles.diagnostics}>
61+
<View style={styles.diagnosticsTitleRow}>
62+
<Text style={styles.diagnosticsTitle}>Exceptionless Expo</Text>
63+
<Text style={styles.diagnosticsPill}>SDK 56</Text>
64+
</View>
65+
<Text style={styles.diagnosticsServer} numberOfLines={1}>
66+
{serverUrl}
67+
</Text>
68+
<View style={styles.diagnosticsMetaRow}>
69+
<Text style={styles.diagnosticsMeta}>{logs.length} logs</Text>
70+
<Text style={styles.diagnosticsMeta}>{errorCount} errors</Text>
71+
<Text style={styles.diagnosticsMeta}>sessions on</Text>
72+
</View>
73+
<Text style={styles.latestLog} numberOfLines={2}>
74+
{latestLog ? `[${latestLog.level.toUpperCase()}] ${latestLog.message}` : "Waiting for Exceptionless startup logs..."}
75+
</Text>
76+
</View>
77+
</SafeAreaView>
78+
);
79+
}
80+
81+
export default function App() {
82+
useEffect(() => {
83+
void Exceptionless.startup((config) => {
84+
config.apiKey = "LhhP1C9gijpSKCslHHCvwdSIz298twx271nTest";
85+
config.serverUrl = serverUrl;
86+
config.services.log = callbackLog;
87+
config.defaultTags.push("Example", "Expo");
88+
config.useSessions(true, 60000, true);
89+
});
90+
}, []);
91+
92+
return (
93+
<SafeAreaProvider>
94+
<NavigationContainer>
95+
<View style={styles.appShell}>
96+
<TopDiagnostics />
97+
<Tab.Navigator
98+
screenOptions={{
99+
headerShown: false,
100+
tabBarActiveTintColor: "#0f172a",
101+
tabBarInactiveTintColor: "#64748b",
102+
tabBarLabelStyle: styles.tabLabel,
103+
tabBarStyle: styles.tabBar,
104+
tabBarBackground: () => <GlassView glassEffectStyle="regular" isInteractive style={StyleSheet.absoluteFill} tintColor="rgba(255,255,255,0.58)" />
105+
}}
106+
>
107+
<Tab.Screen
108+
name="Errors"
109+
component={ErrorsScreen}
110+
options={{
111+
title: "Errors",
112+
tabBarIcon: ({ focused }) => <TabIcon label="!" focused={focused} />
113+
}}
114+
/>
115+
<Tab.Screen
116+
name="Events"
117+
component={EventsScreen}
118+
options={{
119+
title: "Events",
120+
tabBarIcon: ({ focused }) => <TabIcon label="|" focused={focused} />
121+
}}
122+
/>
123+
<Tab.Screen
124+
name="Logs"
125+
component={LogsScreen}
126+
options={{
127+
title: "Logs",
128+
tabBarIcon: ({ focused }) => <TabIcon label="#" focused={focused} />
129+
}}
130+
/>
131+
</Tab.Navigator>
132+
</View>
133+
<StatusBar style="auto" />
134+
</NavigationContainer>
135+
</SafeAreaProvider>
136+
);
137+
}
138+
139+
const styles = StyleSheet.create({
140+
appShell: {
141+
flex: 1,
142+
backgroundColor: "#fff"
143+
},
144+
diagnosticsSafeArea: {
145+
backgroundColor: "#fff"
146+
},
147+
diagnostics: {
148+
borderBottomColor: "#e5e7eb",
149+
borderBottomWidth: StyleSheet.hairlineWidth,
150+
paddingBottom: 10,
151+
paddingHorizontal: 16,
152+
paddingTop: 8
153+
},
154+
diagnosticsTitleRow: {
155+
alignItems: "center",
156+
flexDirection: "row",
157+
justifyContent: "space-between"
158+
},
159+
diagnosticsTitle: {
160+
color: "#111827",
161+
fontSize: 17,
162+
fontWeight: "700"
163+
},
164+
diagnosticsPill: {
165+
backgroundColor: "#eef2ff",
166+
borderRadius: 999,
167+
color: "#3730a3",
168+
fontSize: 12,
169+
fontWeight: "700",
170+
overflow: "hidden",
171+
paddingHorizontal: 10,
172+
paddingVertical: 4
173+
},
174+
diagnosticsServer: {
175+
color: "#475569",
176+
fontSize: 12,
177+
marginTop: 4
178+
},
179+
diagnosticsMetaRow: {
180+
flexDirection: "row",
181+
gap: 8,
182+
marginTop: 8
183+
},
184+
diagnosticsMeta: {
185+
backgroundColor: "#f8fafc",
186+
borderColor: "#e2e8f0",
187+
borderRadius: 999,
188+
borderWidth: StyleSheet.hairlineWidth,
189+
color: "#334155",
190+
fontSize: 11,
191+
fontWeight: "600",
192+
overflow: "hidden",
193+
paddingHorizontal: 8,
194+
paddingVertical: 3
195+
},
196+
latestLog: {
197+
color: "#111827",
198+
fontFamily: Platform.select({ ios: "Menlo", default: "monospace" }),
199+
fontSize: 11,
200+
lineHeight: 15,
201+
marginTop: 8
202+
},
203+
tabBar: {
204+
backgroundColor: "rgba(255,255,255,0.5)",
205+
borderTopColor: "rgba(148,163,184,0.22)",
206+
borderTopWidth: StyleSheet.hairlineWidth,
207+
elevation: 0,
208+
height: 78,
209+
paddingBottom: 14,
210+
paddingTop: 8,
211+
position: "absolute",
212+
shadowColor: "#0f172a",
213+
shadowOffset: { height: -4, width: 0 },
214+
shadowOpacity: 0.08,
215+
shadowRadius: 18
216+
},
217+
tabIcon: {
218+
color: "#64748b",
219+
fontSize: 18,
220+
fontWeight: "800",
221+
lineHeight: 20
222+
},
223+
tabIconFocused: {
224+
color: "#0f172a"
225+
},
226+
tabLabel: {
227+
fontSize: 12,
228+
fontWeight: "700"
229+
}
230+
});

example/expo/README.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Exceptionless Expo Example
2+
3+
This example exercises `@exceptionless/react-native` from an Expo app. It covers JavaScript errors, promise rejections, manual events, logs, sessions, user identity, the React error boundary, and native iOS crash submission.
4+
5+
Native iOS crash reporting uses the package's custom native module, so it requires an Expo development build or a standalone app. Expo Go can run JavaScript reporting paths only; it cannot load the native crash reporter.
6+
7+
This app tracks Expo SDK 56.
8+
9+
## Prerequisites
10+
11+
- Install dependencies from the repository root with `npm install`.
12+
- Run an Exceptionless server on `http://localhost:7110`, or update `getServerUrl()` in `App.tsx`.
13+
- Use a development build for native iOS crash reporting.
14+
15+
## Run
16+
17+
From the repository root:
18+
19+
```bash
20+
npm install
21+
npm run ios --workspace=example/expo
22+
```
23+
24+
`npm run ios` runs `expo run:ios`, which prebuilds native files when needed, installs the development build, and starts Metro.
25+
26+
For the checked-in VS Code launch profile and iPad dogfooding, use:
27+
28+
```bash
29+
npm run ios:ipad --workspace=example/expo
30+
```
31+
32+
`ios:ipad` launches the `iPad Air 11-inch (M3)` simulator on Metro port `8082`, which avoids colliding with another React Native app already using the default `8081` port.
33+
34+
If the development build is already installed, start Metro directly:
35+
36+
```bash
37+
npm run start --workspace=example/expo
38+
```
39+
40+
Use the web build for JavaScript event flows:
41+
42+
```bash
43+
npm run start:web --workspace=example/expo
44+
```
45+
46+
The web build does not include native crash reporting.
47+
48+
## Verify Reporting
49+
50+
With an Exceptionless server listening on `http://localhost:7110`, use the sample tabs to submit:
51+
52+
- caught errors and unhandled errors
53+
- unhandled promise rejections
54+
- logs and feature usage events
55+
- session start, heartbeat, and end events
56+
57+
The in-app Logs tab should show events being enqueued and sent to the configured server. Native iOS crash reports are captured by the development build and submitted on the next launch.
58+
59+
## Notes
60+
61+
- The example points at `http://localhost:7110` by default and derives the host IP for physical iOS devices when Expo provides `hostUri`.
62+
- Native iOS crashes are written by the native module and submitted on the next launch.
63+
- Android currently exercises JavaScript events only; Android native crash reporting is not implemented yet.
64+
- Generated native folders are intentionally ignored by `example/expo/.gitignore`; run `npm run prebuild --workspace=example/expo` or `npm run ios --workspace=example/expo` to recreate them locally.

0 commit comments

Comments
 (0)