Skip to main content

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)
Compatibility flag

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 SDKReact NativeDefault KotlinSet kotlin19Compat?
500.731.9.xtrue
510.741.9.xtrue
520.761.9.xtrue
53+0.77+2.0+❌ Leave default

If you have manually overridden kotlinVersion in your android/build.gradle, follow that version — not the table.

Known limitation — Kotlin 1.9 apps with AndroidX 2.9+ on the classpath

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-router on Expo SDK 52
  • Jetpack Compose 1.7+
  • Direct deps on androidx.fragment:fragment-ktx:1.7+, androidx.lifecycle:*:2.9+, or androidx.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
note

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

iOS Image & FastImage Monitoring: Enabled by default.

Reduce Noise

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

Verify your integration

Once setup is done, walk through the Integration Checklist to make sure nothing is missing — features, navigation, source maps, and release artifacts.