Expo Setup
The Pulse config plugin automatically injects native initialization into your iOS and Android projects during expo prebuild — no manual AppDelegate or MainApplication edits needed.
Step 1 — Install
Use expo install so Expo can resolve compatible native versions:
npx expo install @dreamhorizonorg/pulse-react-native
Step 2 — Add Plugin to app.json
The plugin requires apiKey and dataCollectionState at the top level of the plugin options.
{
"expo": {
"plugins": [
[
"@dreamhorizonorg/pulse-react-native",
{
"apiKey": "your-api-key",
"dataCollectionState": "ALLOWED"
}
]
]
}
}
Optional (Android Only) — minSdk below 26
If minSdkVersion < 26. Add coreLibraryDesugaring under the Pulse plugin’s android key:
{
"expo": {
"plugins": [
[
"@dreamhorizonorg/pulse-react-native",
{
"apiKey": "your-api-key",
"dataCollectionState": "ALLOWED",
"android": {
"coreLibraryDesugaring": {
"enabled": true // Enable true to add desugaring library
}
}
}
]
]
}
}
Android library desugaring (background).
Optional (Android Only) — Kotlin 1.9.x apps (Expo SDK ≤ 52 / RN ≤ 0.76)
Only set this if your app is still compiled with Kotlin 1.9.x and expo run:android fails with:
Module was compiled with an incompatible version of Kotlin. The binary version of its metadata is 2.1.0, expected version is 1.9.0.
Leave it off (default) on Expo SDK 53+ / RN 0.77+ / Kotlin 2.0+. The cap is incompatible with a strict requirement on Kotlin 2.1.x and will break those builds.
Add kotlin19Compat under the Pulse plugin's android key:
{
"expo": {
"plugins": [
[
"@dreamhorizonorg/pulse-react-native",
{
"apiKey": "your-api-key",
"dataCollectionState": "ALLOWED",
"android": {
"kotlin19Compat": true // Enable for apps compiling with Kotlin 1.9.x
}
}
]
]
}
}
What it does: at expo prebuild, the plugin writes PulseReactNativeOtel_kotlin19Compat=true into android/gradle.properties. The SDK then caps its transitive Kotlin runtime artifacts (stdlib, coroutines, serialization) to versions a Kotlin-1.9.x compiler can read. Toggle the flag off (or remove it) and re-run expo prebuild --clean to clean the property back out.
Typical Expo / RN ↔ Kotlin mapping:
| Expo SDK | React Native | Default Kotlin | Set kotlin19Compat? |
|---|---|---|---|
| 50 | 0.73 | 1.9.x | ✅ true |
| 51 | 0.74 | 1.9.x | ✅ true |
| 52 | 0.76 | 1.9.x | ✅ true |
| 53+ | 0.77+ | 2.0+ | ❌ Leave default |
If you have manually overridden kotlinVersion in your android/build.gradle, follow that version — not the table.
The cap is a strict [1.9, 2.1) range on kotlin-stdlib. If your resolved dependency tree contains androidx.savedstate:savedstate-android:1.3.0 (or higher), it transitively imports kotlinx-serialization-bom:1.7.3, which pins kotlin-stdlib to 2.1.21. The two strict constraints have no overlap and expo prebuild / gradle assembleDebug fails with:
Could not resolve org.jetbrains.kotlin:kotlin-stdlib. Cannot find a version of 'org.jetbrains.kotlin:kotlin-stdlib' that satisfies the version constraints:
Common triggers (any of these usually push savedstate-android to 1.3+):
expo-routeron Expo SDK 52- Jetpack Compose 1.7+
- Direct deps on
androidx.fragment:fragment-ktx:1.7+,androidx.lifecycle:*:2.9+, orandroidx.navigation:*:2.8+
Confirm whether you're affected:
cd android && ./gradlew :app:dependencies --configuration debugRuntimeClasspath | grep "savedstate-android"
If the resolved version is 1.3.0 or higher, the cap will not work for your app.
Workaround — override Kotlin to 2.0.21 via expo-build-properties (the consumer compiler then reads Pulse's metadata directly, no cap needed):
{
"expo": {
"plugins": [
["expo-build-properties", {
"android": { "kotlinVersion": "2.0.21" }
}],
[
"@dreamhorizonorg/pulse-react-native",
{
"apiKey": "your-api-key",
"dataCollectionState": "ALLOWED"
// remove `android.kotlin19Compat` — it is not needed with this override
}
]
]
}
}
Then re-run npx expo prebuild --clean. Kotlin 2.0.21 can read Pulse Android SDK's 2.1.0 metadata natively, so the cap and its strict range are no longer in play.
Step 3 — Start Pulse in JS
Call Pulse.start() at module scope in your root file so it runs before the rest of the app. In Expo Router, this is app/_layout.tsx:
import { Pulse } from '@dreamhorizonorg/pulse-react-native';
Pulse.start();
Step 4 — Run Prebuild
npx expo prebuild --clean
npx expo run:ios
# or
npx expo run:android
Re-run prebuild whenever you change the plugin config in app.json.
What starts collecting immediately
With the basic setup above, the following works with no additional code:
- Crashes — native iOS/Android crashes and JS exceptions
- ANR detection (Android)
- App startup timing
- HTTP traffic:
fetch/XMLHttpRequest/axios(JS); - Screen lifecycle (UIViewControllers / Activities)
- Session tracking
- Slow/Jank frames (Android)
- Unhandled promise rejections
React Navigation / Expo Router screen tracking requires one extra step. See Expo Router for Expo Router setup, or Navigation Instrumentation for bare React Navigation.
Android Image & FastImage Monitoring: Built-in Image and libraries like FastImage use OkHttp, so those requests are not covered by JS fetch/XHR — enable android.okHttpInstrumentation.enabled in the plugin (Plugin Reference).
Native screen lifecycle events (Fragment transitions on Android, UIViewController transitions on iOS) duplicate what Expo Router already tracks. Disable them via app.json to avoid noisy data:
{
"android": {
"instrumentation": {
"fragment": { "enabled": false }
}
},
"ios": {
"instrumentation": {
"screenLifecycle": { "enabled": false }
}
}
}
Keep activity enabled on Android — it powers the AppStart span.
Next Steps
Once setup is done, walk through the Integration Checklist to make sure nothing is missing — features, navigation, source maps, and release artifacts.
- Integration Checklist — Verify your full setup, opt-in features, and release artifacts
- Expo Router — Navigation tracking with Expo Router
- Plugin Reference — Full
app.jsonoptions - Configuration — JS-layer
Pulse.start()options