From 55837d2c18d11d9a2d62bd7861b7c98124dd120d Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Mon, 8 Jun 2026 14:38:57 +0200 Subject: [PATCH 1/2] feat(tracing): Add adapter class for Android binder instrumentation --- .../api/sentry-android-core.api | 4 + sentry-android-core/proguard-rules.pro | 2 + .../android/core/ManifestMetadataReader.java | 10 ++ .../io/sentry/android/core/SentryAndroid.java | 4 + .../android/core/SentryAndroidOptions.java | 42 ++++++ .../internal/binder/SentryBinderAdapter.java | 126 ++++++++++++++++++ .../core/ManifestMetadataReaderTest.kt | 50 +++++++ .../android/core/SentryAndroidOptionsTest.kt | 30 +++++ .../binder/SentryBinderAdapterTest.kt | 112 ++++++++++++++++ .../src/main/AndroidManifest.xml | 6 + 10 files changed, 386 insertions(+) create mode 100644 sentry-android-core/src/main/java/io/sentry/android/core/internal/binder/SentryBinderAdapter.java create mode 100644 sentry-android-core/src/test/java/io/sentry/android/core/internal/binder/SentryBinderAdapterTest.kt diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index 249549f8366..846635db049 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 4cd76f9a20d..40187f1f6c2 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 e16d4b312fc..5ecd201159d 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 0d249f73790..ddb77ef979b 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 bb9ec17aabd..9655edd99b2 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 00000000000..bf04a5b4d0c --- /dev/null +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/binder/SentryBinderAdapter.java @@ -0,0 +1,126 @@ +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 java.util.concurrent.atomic.AtomicInteger; +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 final AtomicInteger cookieCounter = new AtomicInteger(); + private static final int NO_COOKIE = -1; + + 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; + } + + private static final ThreadLocal> spanMap = + new ThreadLocal>() { + @Override + protected Map initialValue() { + return new HashMap<>(); + } + }; + + public static int onCallStart(final @NotNull String component, final @NotNull String name) { + if (!tracingEnabled && !loggingEnabled) { + return NO_COOKIE; + } + + 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) { + final int cookie = cookieCounter.incrementAndGet(); + recordSpan(component, name, threadId, threadName, cookie); + return cookie; + } + } catch (Throwable t) { + // ignored, as instrumentation should never crash + } + return NO_COOKIE; + } + + public static void onCallEnd(final int cookie) { + if (cookie == NO_COOKIE) { + return; + } + try { + final @Nullable Map map = spanMap.get(); + if (map == null) { + return; + } + final @Nullable ISpan span = map.remove(cookie); + if (span != null) { + span.finish(); + } + } catch (Throwable t) { + // ignored + } + } + + private static void recordSpan( + final @NotNull String component, + final @NotNull String name, + final long threadId, + final @Nullable String threadName, + final int cookie) { + + final @Nullable ISpan parent = Sentry.getCurrentScopes().getTransaction(); + if (parent == null) { + return; + } + final @Nullable Map map = spanMap.get(); + if (map == null) { + return; + } + final @NotNull ISpan span = parent.startChild("binder", component + "." + name); + span.setData(SpanDataConvention.THREAD_ID, String.valueOf(threadId)); + span.setData(SpanDataConvention.THREAD_NAME, threadName); + map.put(cookie, 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 d8ac959601a..35df105c43a 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 819928dcdc4..d7db1429483 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 00000000000..8e6737ace63 --- /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.assertEquals +import kotlin.test.assertNotEquals +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 cookie and does nothing when both features disabled`() { + SentryBinderAdapter.setEnabled(false, false) + + val cookie = SentryBinderAdapter.onCallStart("ActivityManager", "getRunningTasks") + + assertEquals(-1, cookie) + 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 cookie = SentryBinderAdapter.onCallStart("ActivityManager", "getRunningTasks") + + assertNotEquals(-1, cookie) + verify(fixture.transaction).startChild(eq("binder"), eq("ActivityManager.getRunningTasks")) + + SentryBinderAdapter.onCallEnd(cookie) + 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 cookie = SentryBinderAdapter.onCallStart("ActivityManager", "getRunningTasks") + + assertEquals(-1, cookie) + verify(fixture.logger) + .log( + eq(SentryLogLevel.INFO), + any(), + eq("binder call: %s.%s"), + eq("ActivityManager"), + eq("getRunningTasks"), + ) + } + + @Test + fun `onCallEnd with no cookie is a no-op`() { + SentryBinderAdapter.setEnabled(true, false) + + SentryBinderAdapter.onCallEnd(-1) + + 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 e5b5ed2250b..4eda47e96a6 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 @@ + + From bdaa73949676bfb2f3f12a028438e59be523e20a Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Mon, 8 Jun 2026 14:42:10 +0200 Subject: [PATCH 2/2] changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6a3b55b403..22a9a8eefa9 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