Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

### Fixes

- Capture native exceptions consumed by Expo's bridgeless error handling on Android ([#5871](https://github.com/getsentry/sentry-react-native/pull/5871))
- Fix SIGABRT crash on launch when `mobileReplayIntegration` is not configured and iOS deployment target >= 16.0 ([#5858](https://github.com/getsentry/sentry-react-native/pull/5858))
- Reduce `reactNavigationIntegration` performance overhead ([#5840](https://github.com/getsentry/sentry-react-native/pull/5840), [#5842](https://github.com/getsentry/sentry-react-native/pull/5842), [#5849](https://github.com/getsentry/sentry-react-native/pull/5849))
- Fix duplicated breadcrumbs on Android ([#5841](https://github.com/getsentry/sentry-react-native/pull/5841))
Expand Down
1 change: 1 addition & 0 deletions packages/core/.npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,4 @@
!/expo.d.ts
!/app.plugin.js
!/plugin/build/**/*
!/expo-module.config.json
3 changes: 3 additions & 0 deletions packages/core/RNSentryAndroidTester/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,12 @@ android {
}
}

android.sourceSets.main.java.srcDirs += ['../../android/src/expo/java']

dependencies {
implementation project(':RNSentry')
implementation 'com.facebook.react:react-android:0.72.0'
implementation files('../../android/libs/expo-stubs.jar')
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.5.0'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package io.sentry.react.expo

import io.sentry.Sentry
import io.sentry.exception.ExceptionMechanismException
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.MockedStatic
import org.mockito.Mockito.mockStatic
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.never
import org.mockito.kotlin.verify

@RunWith(JUnit4::class)
class SentryReactNativeHostHandlerTest {
private var sentryMock: MockedStatic<Sentry>? = null

@After
fun tearDown() {
sentryMock?.close()
}

@Test
fun `does not capture when in developer support mode`() {
sentryMock =
mockStatic(Sentry::class.java).also {
it.`when`<Boolean> { Sentry.isEnabled() }.thenReturn(true)
}

val handler = SentryReactNativeHostHandler()
handler.onReactInstanceException(true, RuntimeException("test"))

sentryMock!!.verify({ Sentry.captureException(any()) }, never())
}

@Test
fun `does not capture when sentry is not enabled`() {
sentryMock =
mockStatic(Sentry::class.java).also {
it.`when`<Boolean> { Sentry.isEnabled() }.thenReturn(false)
}

val handler = SentryReactNativeHostHandler()
handler.onReactInstanceException(false, RuntimeException("test"))

sentryMock!!.verify({ Sentry.captureException(any()) }, never())
}

@Test
fun `captures exception with unhandled mechanism when sentry is enabled`() {
sentryMock =
mockStatic(Sentry::class.java).also {
it.`when`<Boolean> { Sentry.isEnabled() }.thenReturn(true)
}

val handler = SentryReactNativeHostHandler()
val originalException = IllegalStateException("Fabric crash")

handler.onReactInstanceException(false, originalException)

val captor = argumentCaptor<Throwable>()
sentryMock!!.verify { Sentry.captureException(captor.capture()) }

val captured = captor.firstValue
assertTrue(
"Expected ExceptionMechanismException but got ${captured::class.java}",
captured is ExceptionMechanismException,
)

val mechanismException = captured as ExceptionMechanismException
val mechanism = mechanismException.exceptionMechanism
assertEquals("expoReactHost", mechanism.type)
assertFalse("Mechanism should be unhandled", mechanism.isHandled!!)
assertEquals(originalException, mechanismException.throwable)
assertNotNull(mechanismException.thread)
}

@Test
fun `does not throw when sentry capture fails`() {
sentryMock =
mockStatic(Sentry::class.java).also {
it.`when`<Boolean> { Sentry.isEnabled() }.thenReturn(true)
it.`when`<Any> { Sentry.captureException(any()) }.thenThrow(RuntimeException("Sentry internal error"))
}

val handler = SentryReactNativeHostHandler()
// Should not throw
handler.onReactInstanceException(false, IllegalStateException("test"))
}
}
2 changes: 2 additions & 0 deletions packages/core/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,14 @@ android {
} else {
java.srcDirs += ['src/oldarch']
}
java.srcDirs += ['src/expo']
}
}
}

dependencies {
compileOnly files('libs/replay-stubs.jar')
compileOnly files('libs/expo-stubs.jar')
implementation 'com.facebook.react:react-native:+'
api 'io.sentry:sentry-android:8.36.0'
debugImplementation 'io.sentry:sentry-spotlight:8.36.0'
Expand Down
7 changes: 7 additions & 0 deletions packages/core/android/expo-stubs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
This module provides stubs for `expo-modules-core` interfaces (`Package` and `ReactNativeHostHandler`) needed to compile the Expo-specific source set (`android/src/expo/`).

The Expo source set registers a `ReactNativeHostHandler` that captures native exceptions swallowed by Expo's bridgeless error handling (`ExpoReactHostDelegate.handleInstanceException`). These stubs are added as a `compileOnly` dependency to `android/build.gradle` (meaning, they are not present at runtime). In Expo projects, the real `expo-modules-core` classes are available at runtime via Expo's autolinking.

## Updating the stubs

To update the stubs, just run `yarn build` from the root of the repo and it will recompile the classes and put them under `packages/core/android/libs/expo-stubs.jar`. Check this newly generated `.jar` in and push.
23 changes: 23 additions & 0 deletions packages/core/android/expo-stubs/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
allprojects {
repositories {
mavenCentral()
google()
}
}

apply plugin: 'java-library'

java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

tasks.named('jar', Jar) {
archiveBaseName.set('expo-stubs')
archiveVersion.set('')
destinationDirectory.set(file("$rootDir/../libs"))
}

dependencies {
compileOnly 'com.google.android:android:4.1.1.4'
}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Loading
Loading