Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
f6fcbcd
feat(android): Add service-managed NDK crash daemon
jpnurmi May 20, 2026
222a934
Update CHANGELOG.md
jpnurmi Oct 21, 2026
e34fbfe
fix(android): Narrow exception handling in the NDK crash service
jpnurmi Oct 22, 2026
2ee8e51
fix(android): Stop the NDK crash service when its daemon exits
jpnurmi Oct 22, 2026
a4f1feb
fix(android): Match the NDK crash service stop to its own start
jpnurmi Oct 22, 2026
b407d9f
fix(android): Write the NDK tombstone marker after a successful send
jpnurmi Oct 22, 2026
b2beb30
fix(android): Bind the NDK crash service for the app's lifetime
jpnurmi Oct 22, 2026
2a7640e
fix(android): Match NDK tombstones by the app's pid
jpnurmi Oct 22, 2026
1f126ed
fix(android): Start an NDK crash daemon for each relaunch
jpnurmi Oct 22, 2026
81997e9
ci(temp): Publish sentry-native-ndk-outproc to Maven Local
jpnurmi Oct 22, 2026
05cc24e
fix(android): Wait for the NDK tombstone outside the send budget
jpnurmi Oct 22, 2026
b4dd86a
fix(android): Remove stale NDK crash IPC files
jpnurmi Oct 22, 2026
c8564cb
fix(android): Fall back to the 1-argument NDK init
jpnurmi Oct 22, 2026
b8cc09d
ref(android): Stop passing the tombstone flag to NdkOptions
jpnurmi Oct 22, 2026
f10770f
fix(android): Skip SDK init in the NDK crash service process
jpnurmi Oct 22, 2026
5069fbd
ref(android): Remove the NDK crash service process check
jpnurmi Oct 22, 2026
a4942d0
fix(android): Keep a newer NDK tombstone marker
jpnurmi Oct 22, 2026
5955c4b
ci: Stop the sentry-native Gradle daemon after publishing
jpnurmi Oct 22, 2026
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
14 changes: 14 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,20 @@ jobs:
with:
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}

# TODO: remove once sentry-native publishes sentry-native-ndk-outproc
- name: Checkout sentry-native
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: getsentry/sentry-native
ref: jpnurmi/feat/native-android
path: sentry-native
submodules: 'recursive'

# TODO: remove once sentry-native publishes sentry-native-ndk-outproc
- name: Publish sentry-native-ndk-outproc to Maven Local
working-directory: sentry-native/ndk
run: ./gradlew --no-daemon :sentry-native-ndk-outproc:publishToMavenLocal

- name: Run Tests and Lint
run: make preMerge

Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Features

- Add experimental, opt-in out-of-process crash reporting on Android ([#6347](https://github.com/getsentry/sentry-java/pull/6347))

### Fixes

- Report the number of dropped spans in client reports when a batched span envelope item is discarded, instead of a quantity of 1 (e.g. for Flutter span streaming) ([#6265](https://github.com/getsentry/sentry-java/pull/6265))
Expand Down
1 change: 1 addition & 0 deletions buildSrc/src/main/java/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ object Config {
val androidLibs = setOf(
"sentry-android-core",
"sentry-android-ndk",
"sentry-android-ndk-outproc",
"sentry-android-fragment",
"sentry-android-navigation",
"sentry-android-navigation3",
Expand Down
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ reactor-core = { module = "io.projectreactor:reactor-core", version = "3.5.3" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
retrofit-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" }
sentry-native-ndk = { module = "io.sentry:sentry-native-ndk", version = "0.17.4" }
sentry-native-ndk-outproc = { module = "io.sentry:sentry-native-ndk-outproc", version = "0.17.4" }
servlet-api = { module = "javax.servlet:javax.servlet-api", version = "3.1.0" }
servlet-jakarta-api = { module = "jakarta.servlet:jakarta.servlet-api", version = "6.1.0" }
slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
Expand Down
2 changes: 1 addition & 1 deletion scripts/update-sentry-native-ndk.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ set-version)

echo "Setting sentry-native-ndk version to '$version'"

PATTERN='(module = "io\.sentry:sentry-native-ndk", version = ")[0-9.]+(")'
PATTERN='(module = "io\.sentry:sentry-native-ndk(?:-outproc)?", version = ")[0-9.]+(")'
perl -pi -e "s/$PATTERN/\${1}$version\${2}/" "$GRADLE_NDK_FILEPATH"
;;
*)
Expand Down
1 change: 1 addition & 0 deletions sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,7 @@ public class io/sentry/android/core/TombstoneIntegration$TombstonePolicy : io/se
public fun getLastReportedTimestamp ()Ljava/lang/Long;
public fun markReported (J)V
public fun matches (Landroid/app/ApplicationExitInfo;)Z
public static fun mergeNativeCrashes (Lio/sentry/SentryEvent;Lio/sentry/SentryEvent;)V
public fun shouldReportHistorical ()Z
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,7 @@ static void installDefaultIntegrations(
// Integrations are registered in the same order. NDK before adding Watch outbox,
// because sentry-native move files around and we don't want to watch that.
final Class<?> sentryNdkClass = loadClass.loadClass(SENTRY_NDK_CLASS_NAME, options.getLogger());
options.addIntegration(new NdkIntegration(sentryNdkClass));
options.addIntegration(new NdkIntegration(context, sentryNdkClass));

if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.S) {
options.addIntegration(new TombstoneIntegration(context));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion;

import android.content.Context;
import io.sentry.IScopes;
import io.sentry.Integration;
import io.sentry.SentryLevel;
Expand All @@ -19,11 +20,17 @@ public final class NdkIntegration implements Integration, Closeable {

public static final String SENTRY_NDK_CLASS_NAME = "io.sentry.android.ndk.SentryNdk";

private final @Nullable Context context;
private final @Nullable Class<?> sentryNdkClass;

private @Nullable SentryAndroidOptions options;

public NdkIntegration(final @Nullable Class<?> sentryNdkClass) {
this(null, sentryNdkClass);
}

NdkIntegration(final @Nullable Context context, final @Nullable Class<?> sentryNdkClass) {
this.context = context != null ? ContextUtils.getApplicationContext(context) : null;
this.sentryNdkClass = sentryNdkClass;
}

Expand All @@ -50,9 +57,25 @@ public final void register(final @NotNull IScopes scopes, final @NotNull SentryO
}

try {
final Method method = sentryNdkClass.getMethod("init", SentryAndroidOptions.class);
final Object[] args = new Object[1];
args[0] = this.options;
Method method = null;
Object[] args = null;
if (context != null) {
try {
method = sentryNdkClass.getMethod("init", Context.class, SentryAndroidOptions.class);
args = new Object[] {context, this.options};
} catch (NoSuchMethodException e) {
// an older sentry-android-ndk
this.options
.getLogger()
.log(
SentryLevel.WARNING,
"SentryNdk.init(Context, SentryAndroidOptions) not found, using SentryNdk.init(SentryAndroidOptions).");
}
}
if (method == null) {
method = sentryNdkClass.getMethod("init", SentryAndroidOptions.class);
args = new Object[] {this.options};
}
method.invoke(null, args);

this.options.getLogger().log(SentryLevel.DEBUG, "NdkIntegration installed.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import android.annotation.SuppressLint;
import android.app.Application;
import android.content.Context;
import android.os.Build;
import android.os.Process;
import android.os.SystemClock;
import android.os.Trace;
Expand Down Expand Up @@ -48,6 +49,9 @@ public final class SentryAndroid {
private static final String FRAGMENT_CLASS_NAME =
"androidx.fragment.app.FragmentManager$FragmentLifecycleCallbacks";

// the process of SentryNdkCrashService in sentry-android-ndk-outproc
private static final String NDK_CRASH_SERVICE_PROCESS_SUFFIX = ".sentry_ndk_crash";

protected static final @NotNull AutoClosableReentrantLock staticLock =
new AutoClosableReentrantLock();

Expand Down Expand Up @@ -96,6 +100,12 @@ public static void init(
@NotNull final Context context,
@NotNull ILogger logger,
@NotNull Sentry.OptionsConfiguration<SentryAndroidOptions> configuration) {
if (isNdkCrashServiceProcess()) {
logger.log(
SentryLevel.DEBUG, "Skipping SentryAndroid.init in the NDK crash service process.");
return;
}

// Started before acquiring the lock so it stays balanced with the endSection() in the finally
// even if acquire() throws.
Trace.beginSection("SentryAndroid.init");
Expand Down Expand Up @@ -281,4 +291,14 @@ private static void deduplicateIntegrations(
}
}
}

// the crash service only runs on API 30+
@SuppressLint("NewApi")
private static boolean isNdkCrashServiceProcess() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
return false;
}
final @Nullable String processName = Application.getProcessName();
return processName != null && processName.endsWith(NDK_CRASH_SERVICE_PROCESS_SUFFIX);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ private void addNativeAttachmentsToTombstoneHint(
}
}

private void mergeNativeCrashes(
public static void mergeNativeCrashes(
final @NotNull SentryEvent nativeEvent, final @NotNull SentryEvent tombstoneEvent) {
// we take the event data verbatim from the Native SDK and only apply tombstone data where we
// are sure that it will improve the outcome:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.sentry.android.core

import android.content.Context
import io.sentry.ILogger
import io.sentry.IScopes
import io.sentry.SentryLevel
Expand All @@ -18,6 +19,8 @@ class NdkIntegrationTest {
val logger = mock<ILogger>()

fun getSut(clazz: Class<*>? = SentryNdk::class.java): NdkIntegration = NdkIntegration(clazz)

fun getSut(context: Context, clazz: Class<*>): NdkIntegration = NdkIntegration(context, clazz)
}

private val fixture = Fixture()
Expand Down Expand Up @@ -95,6 +98,20 @@ class NdkIntegrationTest {
assertFalse(options.isEnableScopeSync)
}

@Test
fun `NdkIntegration falls back to init without context`() {
val integration = fixture.getSut(mock<Context>(), SentryNdk::class.java)

val options = getOptions()

integration.register(fixture.scopes, options)

verify(fixture.logger).log(eq(SentryLevel.WARNING), any<String>())
verify(fixture.logger, never()).log(eq(SentryLevel.ERROR), any<String>(), any())
assertTrue(options.isEnableNdk)
assertTrue(options.isEnableScopeSync)
}

@Test
fun `NdkIntegration won't close if close method is not available`() {
val integration = fixture.getSut(SentryNdkNoClose::class.java)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ import org.robolectric.annotation.Config
import org.robolectric.shadow.api.Shadow
import org.robolectric.shadows.ShadowActivityManager
import org.robolectric.shadows.ShadowActivityManager.ApplicationExitInfoBuilder
import org.robolectric.shadows.ShadowApplication

@RunWith(AndroidJUnit4::class)
@Config(sdk = [Build.VERSION_CODES.N], shadows = [SentryShadowProcess::class])
Expand Down Expand Up @@ -573,6 +574,18 @@ class SentryAndroidTest {
assertTrue(optionsRef.eventProcessors.any { it is ApplicationExitInfoEventProcessor })
}

@Test
@Config(sdk = [30])
fun `init does nothing in the NDK crash service process`() {
ShadowApplication.setProcessName("${context.packageName}.sentry_ndk_crash")
var configured = false

fixture.initSut { configured = true }

assertFalse(configured)
assertFalse(Sentry.isEnabled())
}

private fun prefillScopeCache(options: SentryOptions, cacheDir: String) {
val scopeDir = File(cacheDir, SCOPE_CACHE).also { it.mkdirs() }
val queueFile = QueueFile.Builder(File(scopeDir, BREADCRUMBS_FILENAME)).build()
Expand Down
16 changes: 16 additions & 0 deletions sentry-android-ndk-outproc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# sentry-android-ndk-outproc

Experimental opt-in alternative to `sentry-android-ndk`, with an Android service
that runs the native crash daemon. Depends on `sentry-native-ndk-outproc`. Select
it through the Sentry Gradle plugin:

```kotlin
sentry {
ndkBackend.set("native")
}
```

The default is the inproc backend.

The crash service needs API 30+. On older versions, native crash reporting is
disabled.
40 changes: 40 additions & 0 deletions sentry-android-ndk-outproc/api/sentry-android-ndk-outproc.api
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
public final class io/sentry/android/ndk/BuildConfig {
public static final field BUILD_TYPE Ljava/lang/String;
public static final field DEBUG Z
public static final field LIBRARY_PACKAGE_NAME Ljava/lang/String;
public static final field SENTRY_PACKAGE_NAME Ljava/lang/String;
public static final field VERSION_NAME Ljava/lang/String;
public fun <init> ()V
}

public final class io/sentry/android/ndk/DebugImagesLoader : io/sentry/android/core/IDebugImagesLoader {
public fun <init> (Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/ndk/NativeModuleListLoader;)V
public fun clearDebugImages ()V
public fun loadDebugImages ()Ljava/util/List;
public fun loadDebugImagesForAddresses (Ljava/util/Set;)Ljava/util/Set;
}

public final class io/sentry/android/ndk/NdkScopeObserver : io/sentry/ScopeObserverAdapter {
public fun <init> (Lio/sentry/SentryOptions;)V
public fun addAttachment (Lio/sentry/Attachment;)V
public fun addBreadcrumb (Lio/sentry/Breadcrumb;)V
public fun clearAttachments ()V
public fun removeExtra (Ljava/lang/String;)V
public fun removeTag (Ljava/lang/String;)V
public fun setExtra (Ljava/lang/String;Ljava/lang/String;)V
public fun setTag (Ljava/lang/String;Ljava/lang/String;)V
public fun setTrace (Lio/sentry/SpanContext;Lio/sentry/IScope;)V
public fun setUser (Lio/sentry/protocol/User;)V
}

public final class io/sentry/android/ndk/SentryNdk {
public static fun close ()V
public static fun init (Landroid/content/Context;Lio/sentry/android/core/SentryAndroidOptions;)V
public static fun init (Lio/sentry/android/core/SentryAndroidOptions;)V
}

public final class io/sentry/android/ndk/SentryNdkCrashService : android/app/Service {
public fun <init> ()V
public fun onBind (Landroid/content/Intent;)Landroid/os/IBinder;
}

Loading
Loading