diff --git a/CHANGELOG.md b/CHANGELOG.md
index d6a3b55b40..22a9a8eefa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Changelog
+## Unreleased
+
+### Features
+
+- Add opt-in binder (IPC) tracing and logging instrumentation for Android ([#5515](https://github.com/getsentry/sentry-java/pull/5515))
+ - Enable spans via `options.isEnableBinderTracing = true` or manifest: ``
+ - Enable logs via `options.isEnableBinderLogging = true` or manifest: ``
+
## 8.43.1
### Fixes
diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api
index 249549f836..846635db04 100644
--- a/sentry-android-core/api/sentry-android-core.api
+++ b/sentry-android-core/api/sentry-android-core.api
@@ -386,6 +386,8 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
public fun isEnableAppLifecycleBreadcrumbs ()Z
public fun isEnableAutoActivityLifecycleTracing ()Z
public fun isEnableAutoTraceIdGeneration ()Z
+ public fun isEnableBinderLogging ()Z
+ public fun isEnableBinderTracing ()Z
public fun isEnableFramesTracking ()Z
public fun isEnableNdk ()Z
public fun isEnableNetworkEventBreadcrumbs ()Z
@@ -417,6 +419,8 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
public fun setEnableAppLifecycleBreadcrumbs (Z)V
public fun setEnableAutoActivityLifecycleTracing (Z)V
public fun setEnableAutoTraceIdGeneration (Z)V
+ public fun setEnableBinderLogging (Z)V
+ public fun setEnableBinderTracing (Z)V
public fun setEnableFramesTracking (Z)V
public fun setEnableNdk (Z)V
public fun setEnableNetworkEventBreadcrumbs (Z)V
diff --git a/sentry-android-core/proguard-rules.pro b/sentry-android-core/proguard-rules.pro
index 4cd76f9a20..40187f1f6c 100644
--- a/sentry-android-core/proguard-rules.pro
+++ b/sentry-android-core/proguard-rules.pro
@@ -92,3 +92,5 @@
-dontwarn io.sentry.spotlight.SpotlightIntegration
-keepnames class io.sentry.spotlight.SpotlightIntegration
##---------------End: proguard configuration for sentry-spotlight ----------
+
+-keepnames class io.sentry.android.core.internal.binder.SentryBinderAdapter { *; }
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java
index e16d4b312f..5ecd201159 100644
--- a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java
+++ b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java
@@ -188,6 +188,10 @@ final class ManifestMetadataReader {
static final String ENABLE_ANR_FINGERPRINTING = "io.sentry.anr.enable-fingerprinting";
+ static final String ENABLE_BINDER_TRACING = "io.sentry.traces.binder.enable";
+
+ static final String ENABLE_BINDER_LOGGING = "io.sentry.logs.binder.enable";
+
/** ManifestMetadataReader ctor */
private ManifestMetadataReader() {}
@@ -725,6 +729,12 @@ static void applyMetadata(
options.setEnableAnrFingerprinting(
readBool(
metadata, logger, ENABLE_ANR_FINGERPRINTING, options.isEnableAnrFingerprinting()));
+
+ options.setEnableBinderTracing(
+ readBool(metadata, logger, ENABLE_BINDER_TRACING, options.isEnableBinderTracing()));
+
+ options.setEnableBinderLogging(
+ readBool(metadata, logger, ENABLE_BINDER_LOGGING, options.isEnableBinderLogging()));
}
options
.getLogger()
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java
index 0d249f7379..ddb77ef979 100644
--- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java
+++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroid.java
@@ -14,6 +14,7 @@
import io.sentry.SentryLevel;
import io.sentry.SentryOptions;
import io.sentry.Session;
+import io.sentry.android.core.internal.binder.SentryBinderAdapter;
import io.sentry.android.core.performance.AppStartMetrics;
import io.sentry.android.core.performance.TimeSpan;
import io.sentry.android.fragment.FragmentLifecycleIntegration;
@@ -151,6 +152,9 @@ public static void init(
t);
}
+ SentryBinderAdapter.setEnabled(
+ options.isEnableBinderTracing(), options.isEnableBinderLogging());
+
// if SentryPerformanceProvider was disabled or removed,
// we set the app start / sdk init time here instead
final @NotNull AppStartMetrics appStartMetrics = AppStartMetrics.getInstance();
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java
index bb9ec17aab..9655edd99b 100644
--- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java
+++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java
@@ -263,6 +263,12 @@ public interface BeforeCaptureCallback {
private boolean enableAnrFingerprinting = true;
+ /** Enable or disable creating spans for binder (IPC) calls. Default is disabled. */
+ private boolean enableBinderTracing = false;
+
+ /** Enable or disable logging of binder (IPC) calls. Default is disabled. */
+ private boolean enableBinderLogging = false;
+
public SentryAndroidOptions() {
setSentryClientName(BuildConfig.SENTRY_ANDROID_SDK_NAME + "/" + BuildConfig.VERSION_NAME);
setSdkVersion(createSdkVersion());
@@ -755,6 +761,42 @@ public void setEnableAnrFingerprinting(final boolean enableAnrFingerprinting) {
this.enableAnrFingerprinting = enableAnrFingerprinting;
}
+ /**
+ * Returns whether creating spans for binder (IPC) calls is enabled. Default is disabled.
+ *
+ * @return true if binder spans are enabled
+ */
+ public boolean isEnableBinderTracing() {
+ return enableBinderTracing;
+ }
+
+ /**
+ * Enables or disables creating spans for binder (IPC) calls.
+ *
+ * @param enableBinderTracing true to enable binder spans
+ */
+ public void setEnableBinderTracing(final boolean enableBinderTracing) {
+ this.enableBinderTracing = enableBinderTracing;
+ }
+
+ /**
+ * Returns whether logging of binder (IPC) calls is enabled. Default is disabled.
+ *
+ * @return true if binder logging is enabled
+ */
+ public boolean isEnableBinderLogging() {
+ return enableBinderLogging;
+ }
+
+ /**
+ * Enables or disables logging of binder (IPC) calls.
+ *
+ * @param enableBinderLogging true to enable binder logging
+ */
+ public void setEnableBinderLogging(final boolean enableBinderLogging) {
+ this.enableBinderLogging = enableBinderLogging;
+ }
+
static class AndroidUserFeedbackFormHandler implements SentryFeedbackOptions.IFormHandler {
@Override
public void showForm(
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/binder/SentryBinderAdapter.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/binder/SentryBinderAdapter.java
new file mode 100644
index 0000000000..76d81f750f
--- /dev/null
+++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/binder/SentryBinderAdapter.java
@@ -0,0 +1,118 @@
+package io.sentry.android.core.internal.binder;
+
+import android.os.Build;
+import io.sentry.ISpan;
+import io.sentry.Sentry;
+import io.sentry.SentryAttributes;
+import io.sentry.SentryLogLevel;
+import io.sentry.SpanDataConvention;
+import io.sentry.logger.SentryLogParameters;
+import java.util.HashMap;
+import java.util.Map;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+@SuppressWarnings({"unused", "deprecation"})
+@ApiStatus.Internal
+public final class SentryBinderAdapter {
+
+ private static volatile boolean tracingEnabled = false;
+ private static volatile boolean loggingEnabled = false;
+
+ /** Configures which binder features are active. Expected to be called once during SDK init. */
+ public static void setEnabled(final boolean tracingEnabled, final boolean loggingEnabled) {
+ SentryBinderAdapter.tracingEnabled = tracingEnabled;
+ SentryBinderAdapter.loggingEnabled = loggingEnabled;
+ }
+
+ /**
+ * This method is used by the Sentry Android Gradle plugin for binder instrumentation. Called
+ * right before a binder call starts. Returns an opaque token that must be passed back to {@link
+ * #onCallEnd(Object)} once the call completes, or {@code null} if nothing was recorded.
+ *
+ * @param component the component being called, e.g. "ActivityManager"
+ * @param name the method being called, e.g. "startActivity"
+ * @return an opaque token which must be later passed to {@link #onCallEnd(Object)},
+ */
+ public static @Nullable Object onCallStart(
+ final @NotNull String component, final @NotNull String name) {
+ if (!tracingEnabled && !loggingEnabled) {
+ return null;
+ }
+
+ try {
+ final @NotNull Thread currentThread = Thread.currentThread();
+ final long threadId;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) {
+ threadId = currentThread.threadId();
+ } else {
+ threadId = currentThread.getId();
+ }
+ final @Nullable String threadName = currentThread.getName();
+
+ if (loggingEnabled) {
+ recordLog(component, name, threadId, threadName);
+ }
+ if (tracingEnabled) {
+ return recordSpan(component, name, threadId, threadName);
+ }
+ } catch (Throwable t) {
+ // ignored, as instrumentation should never crash
+ }
+ return null;
+ }
+
+ /**
+ * This method is used by the Sentry Android Gradle plugin for binder instrumentation. Called
+ * right after a binder call ends.
+ *
+ * @param token the token returned by {@link #onCallStart(String, String)}
+ */
+ public static void onCallEnd(final @Nullable Object token) {
+ if (token == null) {
+ return;
+ }
+ try {
+ if (token instanceof ISpan) {
+ ((ISpan) token).finish();
+ }
+ } catch (Throwable t) {
+ // ignored
+ }
+ }
+
+ private static @Nullable ISpan recordSpan(
+ final @NotNull String component,
+ final @NotNull String name,
+ final long threadId,
+ final @Nullable String threadName) {
+
+ final @Nullable ISpan parent = Sentry.getCurrentScopes().getTransaction();
+ if (parent == null) {
+ return null;
+ }
+ final @NotNull ISpan span = parent.startChild("binder", component + "." + name);
+ span.setData(SpanDataConvention.THREAD_ID, String.valueOf(threadId));
+ span.setData(SpanDataConvention.THREAD_NAME, threadName);
+ return span;
+ }
+
+ private static void recordLog(
+ final @NotNull String component,
+ final @NotNull String name,
+ final long threadId,
+ final @Nullable String threadName) {
+ final @NotNull Map logAttributes = new HashMap<>();
+ logAttributes.put(SpanDataConvention.THREAD_ID, threadId);
+ logAttributes.put(SpanDataConvention.THREAD_NAME, threadName);
+
+ Sentry.logger()
+ .log(
+ SentryLogLevel.INFO,
+ SentryLogParameters.create(SentryAttributes.fromMap(logAttributes)),
+ "binder call: %s.%s",
+ component,
+ name);
+ }
+}
diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt
index d8ac959601..35df105c43 100644
--- a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt
+++ b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt
@@ -2586,4 +2586,54 @@ class ManifestMetadataReaderTest {
// Assert
assertEquals("12345", fixture.options.orgId)
}
+
+ @Test
+ fun `applyMetadata reads enableBinderTracing to options`() {
+ // Arrange
+ val bundle = bundleOf(ManifestMetadataReader.ENABLE_BINDER_TRACING to true)
+ val context = fixture.getContext(metaData = bundle)
+
+ // Act
+ ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
+
+ // Assert
+ assertTrue(fixture.options.isEnableBinderTracing)
+ }
+
+ @Test
+ fun `applyMetadata keeps enableBinderTracing default when not set in manifest`() {
+ // Arrange
+ val context = fixture.getContext()
+
+ // Act
+ ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
+
+ // Assert
+ assertFalse(fixture.options.isEnableBinderTracing)
+ }
+
+ @Test
+ fun `applyMetadata reads enableBinderLogging to options`() {
+ // Arrange
+ val bundle = bundleOf(ManifestMetadataReader.ENABLE_BINDER_LOGGING to true)
+ val context = fixture.getContext(metaData = bundle)
+
+ // Act
+ ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
+
+ // Assert
+ assertTrue(fixture.options.isEnableBinderLogging)
+ }
+
+ @Test
+ fun `applyMetadata keeps enableBinderLogging default when not set in manifest`() {
+ // Arrange
+ val context = fixture.getContext()
+
+ // Act
+ ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
+
+ // Assert
+ assertFalse(fixture.options.isEnableBinderLogging)
+ }
}
diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt
index 819928dcdc..d7db142948 100644
--- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt
+++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt
@@ -114,6 +114,36 @@ class SentryAndroidOptionsTest {
assertFalse(sentryOptions.isAttachViewHierarchy)
}
+ @Test
+ fun `binder tracing is disabled by default for Android`() {
+ val sentryOptions = SentryAndroidOptions()
+
+ assertFalse(sentryOptions.isEnableBinderTracing)
+ }
+
+ @Test
+ fun `binder logging is disabled by default for Android`() {
+ val sentryOptions = SentryAndroidOptions()
+
+ assertFalse(sentryOptions.isEnableBinderLogging)
+ }
+
+ @Test
+ fun `binder tracing can be enabled`() {
+ val sentryOptions = SentryAndroidOptions()
+ sentryOptions.isEnableBinderTracing = true
+
+ assertTrue(sentryOptions.isEnableBinderTracing)
+ }
+
+ @Test
+ fun `binder logging can be enabled`() {
+ val sentryOptions = SentryAndroidOptions()
+ sentryOptions.isEnableBinderLogging = true
+
+ assertTrue(sentryOptions.isEnableBinderLogging)
+ }
+
@Test
fun `native sdk name is null by default`() {
val sentryOptions = SentryAndroidOptions()
diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/binder/SentryBinderAdapterTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/binder/SentryBinderAdapterTest.kt
new file mode 100644
index 0000000000..516c0a8a6d
--- /dev/null
+++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/binder/SentryBinderAdapterTest.kt
@@ -0,0 +1,112 @@
+package io.sentry.android.core.internal.binder
+
+import io.sentry.IScopes
+import io.sentry.ISpan
+import io.sentry.ITransaction
+import io.sentry.Sentry
+import io.sentry.SentryLogLevel
+import io.sentry.logger.ILoggerApi
+import io.sentry.logger.SentryLogParameters
+import kotlin.test.AfterTest
+import kotlin.test.BeforeTest
+import kotlin.test.Test
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import org.mockito.Mockito.mockStatic
+import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.verifyNoInteractions
+import org.mockito.kotlin.whenever
+
+class SentryBinderAdapterTest {
+ class Fixture {
+ val mockedSentry = mockStatic(Sentry::class.java)
+ val scopes = mock()
+ val transaction = mock()
+ val span = mock()
+ val logger = mock()
+
+ fun setUp(hasTransaction: Boolean = true) {
+ mockedSentry.`when` { Sentry.getCurrentScopes() }.thenReturn(scopes)
+ mockedSentry.`when` { Sentry.logger() }.thenReturn(logger)
+ whenever(scopes.transaction).thenReturn(if (hasTransaction) transaction else null)
+ whenever(transaction.startChild(any(), any())).thenReturn(span)
+ }
+ }
+
+ private val fixture = Fixture()
+
+ @BeforeTest
+ fun setUp() {
+ fixture.setUp()
+ }
+
+ @AfterTest
+ fun cleanup() {
+ SentryBinderAdapter.setEnabled(false, false)
+ fixture.mockedSentry.close()
+ }
+
+ @Test
+ fun `returns no token and does nothing when both features disabled`() {
+ SentryBinderAdapter.setEnabled(false, false)
+
+ val token = SentryBinderAdapter.onCallStart("ActivityManager", "getRunningTasks")
+
+ assertNull(token)
+ verify(fixture.transaction, never()).startChild(any(), any())
+ verifyNoInteractions(fixture.logger)
+ }
+
+ @Test
+ fun `starts and finishes a span when tracing is enabled`() {
+ SentryBinderAdapter.setEnabled(true, false)
+
+ val token = SentryBinderAdapter.onCallStart("ActivityManager", "getRunningTasks")
+
+ assertNotNull(token)
+ verify(fixture.transaction).startChild(eq("binder"), eq("ActivityManager.getRunningTasks"))
+
+ SentryBinderAdapter.onCallEnd(token)
+ verify(fixture.span).finish()
+ }
+
+ @Test
+ fun `does not start a span when there is no active transaction`() {
+ fixture.setUp(hasTransaction = false)
+ SentryBinderAdapter.setEnabled(true, false)
+
+ SentryBinderAdapter.onCallStart("ActivityManager", "getRunningTasks")
+
+ verify(fixture.transaction, never()).startChild(any(), any())
+ }
+
+ @Test
+ fun `records a log when logging is enabled`() {
+ SentryBinderAdapter.setEnabled(false, true)
+
+ val token = SentryBinderAdapter.onCallStart("ActivityManager", "getRunningTasks")
+
+ assertNull(token)
+ verify(fixture.logger)
+ .log(
+ eq(SentryLogLevel.INFO),
+ any(),
+ eq("binder call: %s.%s"),
+ eq("ActivityManager"),
+ eq("getRunningTasks"),
+ )
+ }
+
+ @Test
+ fun `onCallEnd with null token is a no-op`() {
+ SentryBinderAdapter.setEnabled(true, false)
+
+ SentryBinderAdapter.onCallEnd(null)
+
+ verify(fixture.span, never()).finish()
+ }
+}
diff --git a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml
index e5b5ed2250..4eda47e96a 100644
--- a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml
+++ b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml
@@ -279,5 +279,11 @@
+
+