Skip to content

Commit 732864b

Browse files
committed
feat(android): add binder IPC tracing adapter for Gradle plugin instrumentation
Adds `SentryIpcTracer`, the static bridge invoked by bytecode emitted by the Sentry Android Gradle plugin's binder IPC instrumentation, plus two opt-in Android options: - `enableBinderTracing`: when on and a transaction is active, creates a child span (op `binder.ipc`) per instrumented binder call, annotated with the `thread.id` and `thread.name` span data attributes. - `enableBinderLogs`: when on, emits a Sentry log entry at INFO level per instrumented binder call, with `thread.id`/`thread.name` attributes. Both options are also readable from `AndroidManifest.xml` via `io.sentry.traces.binder-ipc.enable` and `io.sentry.logs.binder-ipc.enable`. `SentryIpcTracer` is kept from obfuscation via a ProGuard rule since the instrumented bytecode references it by symbol. Requires the companion branch https://github.com/getsentry/sentry-android-gradle-plugin/tree/markushi/feat/binder-tracing
1 parent 5b1a06b commit 732864b

9 files changed

Lines changed: 464 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Features
6+
7+
- Add binder IPC instrumentation support in `sentry-android-core`
8+
- Introduces `SentryAndroidOptions.enableBinderTracing` (off by default) — when enabled and a transaction is active, each binder IPC call instrumented by the Sentry Android Gradle plugin produces a child span with op `binder.ipc`, enriched with `thread.id` and `thread.name` attributes.
9+
- Introduces `SentryAndroidOptions.enableBinderLogs` (off by default) — when enabled, each instrumented binder IPC call emits a Sentry log entry at `INFO` level with `thread.id` and `thread.name` attributes.
10+
- Adds `SentryIpcTracer`, the static bridge invoked by the instrumented bytecode.
11+
- Requires the companion changes in the Sentry Android Gradle plugin ([markushi/feat/binder-tracing](https://github.com/getsentry/sentry-android-gradle-plugin/tree/markushi/feat/binder-tracing)).
12+
313
## 8.39.1
414

515
### Fixes

sentry-android-core/api/sentry-android-core.api

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,8 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
385385
public fun isEnableAppLifecycleBreadcrumbs ()Z
386386
public fun isEnableAutoActivityLifecycleTracing ()Z
387387
public fun isEnableAutoTraceIdGeneration ()Z
388+
public fun isEnableBinderLogs ()Z
389+
public fun isEnableBinderTracing ()Z
388390
public fun isEnableFramesTracking ()Z
389391
public fun isEnableNdk ()Z
390392
public fun isEnableNetworkEventBreadcrumbs ()Z
@@ -415,6 +417,8 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr
415417
public fun setEnableAppLifecycleBreadcrumbs (Z)V
416418
public fun setEnableAutoActivityLifecycleTracing (Z)V
417419
public fun setEnableAutoTraceIdGeneration (Z)V
420+
public fun setEnableBinderLogs (Z)V
421+
public fun setEnableBinderTracing (Z)V
418422
public fun setEnableFramesTracking (Z)V
419423
public fun setEnableNdk (Z)V
420424
public fun setEnableNetworkEventBreadcrumbs (Z)V
@@ -443,6 +447,11 @@ public final class io/sentry/android/core/SentryInitProvider {
443447
public fun shutdown ()V
444448
}
445449

450+
public final class io/sentry/android/core/SentryIpcTracer {
451+
public static fun onCallEnd (I)V
452+
public static fun onCallStart (Ljava/lang/String;Ljava/lang/String;)I
453+
}
454+
446455
public final class io/sentry/android/core/SentryLogcatAdapter {
447456
public fun <init> ()V
448457
public static fun d (Ljava/lang/String;Ljava/lang/String;)I

sentry-android-core/proguard-rules.pro

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@
5353

5454
-keepnames class io.sentry.android.core.ApplicationNotResponding
5555

56+
# Bytecode-instrumented IPC tracer — the Sentry Android Gradle plugin emits
57+
# direct static calls to these symbols, so both the class name and its static
58+
# methods must be preserved.
59+
-keep class io.sentry.android.core.SentryIpcTracer { *; }
60+
5661

5762
##---------------End: proguard configuration for android-core ----------
5863

sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,12 @@ final class ManifestMetadataReader {
153153
static final String ENABLE_AUTO_TRACE_ID_GENERATION =
154154
"io.sentry.traces.enable-auto-id-generation";
155155

156+
@ApiStatus.Experimental
157+
static final String ENABLE_BINDER_TRACING = "io.sentry.traces.binder-ipc.enable";
158+
159+
@ApiStatus.Experimental
160+
static final String ENABLE_BINDER_LOGS = "io.sentry.logs.binder-ipc.enable";
161+
156162
static final String DEADLINE_TIMEOUT = "io.sentry.traces.deadline-timeout";
157163

158164
static final String FEEDBACK_NAME_REQUIRED = "io.sentry.feedback.is-name-required";
@@ -508,6 +514,12 @@ static void applyMetadata(
508514
ENABLE_AUTO_TRACE_ID_GENERATION,
509515
options.isEnableAutoTraceIdGeneration()));
510516

517+
options.setEnableBinderTracing(
518+
readBool(metadata, logger, ENABLE_BINDER_TRACING, options.isEnableBinderTracing()));
519+
520+
options.setEnableBinderLogs(
521+
readBool(metadata, logger, ENABLE_BINDER_LOGS, options.isEnableBinderLogs()));
522+
511523
options.setDeadlineTimeout(
512524
readLong(metadata, logger, DEADLINE_TIMEOUT, options.getDeadlineTimeout()));
513525

sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,21 @@ public interface BeforeCaptureCallback {
257257

258258
private boolean enableAnrFingerprinting = true;
259259

260+
/**
261+
* Enables Binder IPC tracing. When enabled and a transaction is active, the SDK creates a child
262+
* span (op {@code binder.ipc}) around each binder call instrumented by the Sentry Android Gradle
263+
* plugin. Requires the Sentry Android Gradle plugin with binder IPC instrumentation enabled.
264+
* Defaults to {@code false}.
265+
*/
266+
private boolean enableBinderTracing = false;
267+
268+
/**
269+
* Enables Binder IPC logs. When enabled, the SDK emits a Sentry log entry for each binder call
270+
* instrumented by the Sentry Android Gradle plugin. Requires the Sentry Android Gradle plugin
271+
* with binder IPC instrumentation enabled and Sentry logs enabled. Defaults to {@code false}.
272+
*/
273+
private boolean enableBinderLogs = false;
274+
260275
public SentryAndroidOptions() {
261276
setSentryClientName(BuildConfig.SENTRY_ANDROID_SDK_NAME + "/" + BuildConfig.VERSION_NAME);
262277
setSdkVersion(createSdkVersion());
@@ -741,6 +756,26 @@ public void setEnableAnrFingerprinting(final boolean enableAnrFingerprinting) {
741756
this.enableAnrFingerprinting = enableAnrFingerprinting;
742757
}
743758

759+
@ApiStatus.Experimental
760+
public boolean isEnableBinderTracing() {
761+
return enableBinderTracing;
762+
}
763+
764+
@ApiStatus.Experimental
765+
public void setEnableBinderTracing(final boolean enableBinderTracing) {
766+
this.enableBinderTracing = enableBinderTracing;
767+
}
768+
769+
@ApiStatus.Experimental
770+
public boolean isEnableBinderLogs() {
771+
return enableBinderLogs;
772+
}
773+
774+
@ApiStatus.Experimental
775+
public void setEnableBinderLogs(final boolean enableBinderLogs) {
776+
this.enableBinderLogs = enableBinderLogs;
777+
}
778+
744779
static class AndroidUserFeedbackIDialogHandler implements SentryFeedbackOptions.IDialogHandler {
745780
@Override
746781
public void showDialog(
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package io.sentry.android.core;
2+
3+
import io.sentry.IScopes;
4+
import io.sentry.ISpan;
5+
import io.sentry.Sentry;
6+
import io.sentry.SentryAttribute;
7+
import io.sentry.SentryAttributes;
8+
import io.sentry.SentryLogLevel;
9+
import io.sentry.SentryOptions;
10+
import io.sentry.SpanDataConvention;
11+
import io.sentry.logger.SentryLogParameters;
12+
import io.sentry.util.thread.IThreadChecker;
13+
import java.util.concurrent.ConcurrentHashMap;
14+
import java.util.concurrent.atomic.AtomicInteger;
15+
import org.jetbrains.annotations.ApiStatus;
16+
import org.jetbrains.annotations.NotNull;
17+
import org.jetbrains.annotations.Nullable;
18+
import org.jetbrains.annotations.TestOnly;
19+
20+
/**
21+
* Entry point invoked by bytecode instrumented by the Sentry Android Gradle plugin around binder
22+
* IPC call sites. The instrumentation emits calls to {@link #onCallStart(String, String)} and
23+
* {@link #onCallEnd(int)} wrapped in a try/finally, so both methods MUST NOT throw and MUST stay
24+
* cheap when the feature is disabled.
25+
*/
26+
@ApiStatus.Internal
27+
public final class SentryIpcTracer {
28+
29+
private static final String OP_BINDER = "binder.ipc";
30+
private static final int DISABLED = -1;
31+
32+
private static final AtomicInteger COUNTER = new AtomicInteger();
33+
private static final ConcurrentHashMap<Integer, ISpan> IN_FLIGHT = new ConcurrentHashMap<>();
34+
35+
private SentryIpcTracer() {}
36+
37+
public static int onCallStart(final @NotNull String component, final @NotNull String method) {
38+
try {
39+
final @NotNull IScopes scopes = Sentry.getCurrentScopes();
40+
final @NotNull SentryOptions options = scopes.getOptions();
41+
if (!(options instanceof SentryAndroidOptions)) {
42+
return DISABLED;
43+
}
44+
final SentryAndroidOptions androidOptions = (SentryAndroidOptions) options;
45+
final boolean tracingEnabled = androidOptions.isEnableBinderTracing();
46+
final boolean logsEnabled = androidOptions.isEnableBinderLogs();
47+
if (!tracingEnabled && !logsEnabled) {
48+
return DISABLED;
49+
}
50+
51+
final @NotNull IThreadChecker threadChecker = options.getThreadChecker();
52+
final @NotNull String threadId = String.valueOf(threadChecker.currentThreadSystemId());
53+
final @NotNull String threadName = threadChecker.getCurrentThreadName();
54+
55+
if (logsEnabled) {
56+
final @NotNull SentryAttributes attributes =
57+
SentryAttributes.of(
58+
SentryAttribute.stringAttribute(SpanDataConvention.THREAD_ID, threadId),
59+
SentryAttribute.stringAttribute(SpanDataConvention.THREAD_NAME, threadName));
60+
scopes
61+
.logger()
62+
.log(
63+
SentryLogLevel.INFO,
64+
SentryLogParameters.create(attributes),
65+
"Binder IPC %s.%s",
66+
component,
67+
method);
68+
}
69+
70+
if (tracingEnabled) {
71+
final @Nullable ISpan parent = scopes.getSpan();
72+
if (parent == null) {
73+
return DISABLED;
74+
}
75+
final ISpan child = parent.startChild(OP_BINDER, component + "." + method);
76+
child.setData(SpanDataConvention.THREAD_ID, threadId);
77+
child.setData(SpanDataConvention.THREAD_NAME, threadName);
78+
final int cookie = COUNTER.incrementAndGet();
79+
IN_FLIGHT.put(cookie, child);
80+
return cookie;
81+
}
82+
} catch (Throwable ignored) {
83+
// never throw from an instrumented call site
84+
}
85+
return DISABLED;
86+
}
87+
88+
public static void onCallEnd(final int cookie) {
89+
if (cookie == DISABLED) {
90+
return;
91+
}
92+
try {
93+
final @Nullable ISpan span = IN_FLIGHT.remove(cookie);
94+
if (span != null) {
95+
span.finish();
96+
}
97+
} catch (Throwable ignored) {
98+
// never throw from an instrumented call site
99+
}
100+
}
101+
102+
@TestOnly
103+
static void resetForTest() {
104+
IN_FLIGHT.clear();
105+
COUNTER.set(0);
106+
}
107+
108+
@TestOnly
109+
static int inFlightCount() {
110+
return IN_FLIGHT.size();
111+
}
112+
}

sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2511,4 +2511,54 @@ class ManifestMetadataReaderTest {
25112511
// Assert
25122512
assertEquals("12345", fixture.options.orgId)
25132513
}
2514+
2515+
@Test
2516+
fun `applyMetadata reads enableBinderTracing to options`() {
2517+
// Arrange
2518+
val bundle = bundleOf(ManifestMetadataReader.ENABLE_BINDER_TRACING to true)
2519+
val context = fixture.getContext(metaData = bundle)
2520+
2521+
// Act
2522+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
2523+
2524+
// Assert
2525+
assertTrue(fixture.options.isEnableBinderTracing)
2526+
}
2527+
2528+
@Test
2529+
fun `applyMetadata reads enableBinderTracing and keeps default if not found`() {
2530+
// Arrange
2531+
val context = fixture.getContext()
2532+
2533+
// Act
2534+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
2535+
2536+
// Assert
2537+
assertFalse(fixture.options.isEnableBinderTracing)
2538+
}
2539+
2540+
@Test
2541+
fun `applyMetadata reads enableBinderLogs to options`() {
2542+
// Arrange
2543+
val bundle = bundleOf(ManifestMetadataReader.ENABLE_BINDER_LOGS to true)
2544+
val context = fixture.getContext(metaData = bundle)
2545+
2546+
// Act
2547+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
2548+
2549+
// Assert
2550+
assertTrue(fixture.options.isEnableBinderLogs)
2551+
}
2552+
2553+
@Test
2554+
fun `applyMetadata reads enableBinderLogs and keeps default if not found`() {
2555+
// Arrange
2556+
val context = fixture.getContext()
2557+
2558+
// Act
2559+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
2560+
2561+
// Assert
2562+
assertFalse(fixture.options.isEnableBinderLogs)
2563+
}
25142564
}

sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidOptionsTest.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,22 @@ class SentryAndroidOptionsTest {
233233
sentryOptions.anrProfilingSampleRate = 2.0
234234
}
235235

236+
@Test
237+
fun `binder tracing and logs are disabled by default`() {
238+
val sentryOptions = SentryAndroidOptions()
239+
assertFalse(sentryOptions.isEnableBinderTracing)
240+
assertFalse(sentryOptions.isEnableBinderLogs)
241+
}
242+
243+
@Test
244+
fun `binder tracing and logs can be toggled`() {
245+
val sentryOptions = SentryAndroidOptions()
246+
sentryOptions.isEnableBinderTracing = true
247+
sentryOptions.isEnableBinderLogs = true
248+
assertTrue(sentryOptions.isEnableBinderTracing)
249+
assertTrue(sentryOptions.isEnableBinderLogs)
250+
}
251+
236252
private class CustomDebugImagesLoader : IDebugImagesLoader {
237253
override fun loadDebugImages(): List<DebugImage>? = null
238254

0 commit comments

Comments
 (0)