Skip to content

Commit 35e1010

Browse files
committed
support PrettyMethod with_signature formatted java frames
1 parent 1ee2738 commit 35e1010

File tree

2 files changed

+164
-24
lines changed

2 files changed

+164
-24
lines changed

sentry-android-core/src/main/java/io/sentry/android/core/internal/tombstone/TombstoneParser.java

Lines changed: 51 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -135,31 +135,27 @@ private SentryStackTrace createStackTrace(@NonNull final TombstoneProtos.Thread
135135
final SentryStackFrame stackFrame = new SentryStackFrame();
136136
if (isJavaFrame(frame)) {
137137
stackFrame.setPlatform("java");
138+
final String module = extractJavaModuleName(frame.getFunctionName());
138139
stackFrame.setFunction(extractJavaFunctionName(frame.getFunctionName()));
139-
stackFrame.setModule(extractJavaModuleName(frame.getFunctionName()));
140+
stackFrame.setModule(module);
141+
142+
// For Java frames, check in-app against the module (package name), which is what
143+
// inAppIncludes/inAppExcludes are designed to match against.
144+
@Nullable
145+
Boolean inApp =
146+
(module == null || module.isEmpty())
147+
? Boolean.FALSE
148+
: SentryStackTraceFactory.isInApp(module, inAppIncludes, inAppExcludes);
149+
stackFrame.setInApp(inApp != null && inApp);
140150
} else {
141151
stackFrame.setPackage(frame.getFileName());
142152
stackFrame.setFunction(frame.getFunctionName());
143153
stackFrame.setInstructionAddr(formatHex(frame.getPc()));
144-
}
145154

146-
// inAppIncludes/inAppExcludes filter by Java/Kotlin package names, which don't overlap
147-
// with native C/C++ function names (e.g., "crash", "__libc_init"). For native frames,
148-
// isInApp() returns null, making nativeLibraryDir the effective in-app check.
149-
// Protobuf returns "" for unset function names, which would incorrectly return true
150-
// from isInApp(), so we treat empty as false to let nativeLibraryDir decide.
151-
final String functionName = frame.getFunctionName();
152-
@Nullable
153-
Boolean inApp =
154-
functionName.isEmpty()
155-
? Boolean.FALSE
156-
: SentryStackTraceFactory.isInApp(functionName, inAppIncludes, inAppExcludes);
157-
158-
final boolean isInNativeLibraryDir =
159-
nativeLibraryDir != null && frame.getFileName().startsWith(nativeLibraryDir);
160-
inApp = (inApp != null && inApp) || isInNativeLibraryDir;
161-
162-
stackFrame.setInApp(inApp);
155+
final boolean isInNativeLibraryDir =
156+
nativeLibraryDir != null && frame.getFileName().startsWith(nativeLibraryDir);
157+
stackFrame.setInApp(isInNativeLibraryDir);
158+
}
163159
frames.add(0, stackFrame);
164160
}
165161

@@ -180,19 +176,50 @@ private SentryStackTrace createStackTrace(@NonNull final TombstoneProtos.Thread
180176
return stacktrace;
181177
}
182178

179+
/**
180+
* Normalizes a PrettyMethod-formatted function name by stripping the return type prefix and
181+
* parameter list suffix that dex2oat may include when compiling AOT frames into the symtab.
182+
*
183+
* <p>e.g. "void com.example.MyClass.myMethod(int, java.lang.String)" ->
184+
* "com.example.MyClass.myMethod"
185+
*/
186+
private static String normalizeFunctionName(String fqFunctionName) {
187+
String normalized = fqFunctionName.trim();
188+
189+
// When dex2oat compiles AOT frames, PrettyMethod with_signature format may be used:
190+
// "void com.example.MyClass.myMethod(int, java.lang.String)"
191+
// A space is never part of a normal fully-qualified method name, so its presence
192+
// reliably indicates the with_signature format.
193+
final int spaceIndex = normalized.indexOf(' ');
194+
if (spaceIndex >= 0) {
195+
// Strip return type prefix
196+
normalized = normalized.substring(spaceIndex + 1).trim();
197+
198+
// Strip parameter list suffix
199+
final int parenIndex = normalized.indexOf('(');
200+
if (parenIndex >= 0) {
201+
normalized = normalized.substring(0, parenIndex);
202+
}
203+
}
204+
205+
return normalized;
206+
}
207+
183208
private static @Nullable String extractJavaModuleName(String fqFunctionName) {
184-
if (fqFunctionName.contains(".")) {
185-
return fqFunctionName.substring(0, fqFunctionName.lastIndexOf("."));
209+
final String normalized = normalizeFunctionName(fqFunctionName);
210+
if (normalized.contains(".")) {
211+
return normalized.substring(0, normalized.lastIndexOf("."));
186212
} else {
187213
return "";
188214
}
189215
}
190216

191217
private static @Nullable String extractJavaFunctionName(String fqFunctionName) {
192-
if (fqFunctionName.contains(".")) {
193-
return fqFunctionName.substring(fqFunctionName.lastIndexOf(".") + 1);
218+
final String normalized = normalizeFunctionName(fqFunctionName);
219+
if (normalized.contains(".")) {
220+
return normalized.substring(normalized.lastIndexOf(".") + 1);
194221
} else {
195-
return fqFunctionName;
222+
return normalized;
196223
}
197224
}
198225

sentry-android-core/src/test/java/io/sentry/android/core/internal/tombstone/TombstoneParserTest.kt

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,119 @@ class TombstoneParserTest {
464464
assertEquals(expectedJson, actualJson)
465465
}
466466

467+
@Test
468+
fun `extracts java function and module from plain PrettyMethod format`() {
469+
val event = parseTombstoneWithJavaFunctionName("com.example.MyClass.myMethod")
470+
val frame = event.threads!![0].stacktrace!!.frames!![0]
471+
assertEquals("java", frame.platform)
472+
assertEquals("myMethod", frame.function)
473+
assertEquals("com.example.MyClass", frame.module)
474+
}
475+
476+
@Test
477+
fun `extracts java function and module from PrettyMethod with_signature format`() {
478+
val event =
479+
parseTombstoneWithJavaFunctionName("void com.example.MyClass.myMethod(int, java.lang.String)")
480+
val frame = event.threads!![0].stacktrace!!.frames!![0]
481+
assertEquals("java", frame.platform)
482+
assertEquals("myMethod", frame.function)
483+
assertEquals("com.example.MyClass", frame.module)
484+
}
485+
486+
@Test
487+
fun `extracts java function and module from PrettyMethod with_signature with object return type`() {
488+
val event =
489+
parseTombstoneWithJavaFunctionName("java.lang.String com.example.MyClass.myMethod(int)")
490+
val frame = event.threads!![0].stacktrace!!.frames!![0]
491+
assertEquals("java", frame.platform)
492+
assertEquals("myMethod", frame.function)
493+
assertEquals("com.example.MyClass", frame.module)
494+
}
495+
496+
@Test
497+
fun `extracts java function and module from PrettyMethod with_signature with no params`() {
498+
val event = parseTombstoneWithJavaFunctionName("void com.example.MyClass.myMethod()")
499+
val frame = event.threads!![0].stacktrace!!.frames!![0]
500+
assertEquals("java", frame.platform)
501+
assertEquals("myMethod", frame.function)
502+
assertEquals("com.example.MyClass", frame.module)
503+
}
504+
505+
@Test
506+
fun `handles bare function name without package`() {
507+
val event = parseTombstoneWithJavaFunctionName("myMethod")
508+
val frame = event.threads!![0].stacktrace!!.frames!![0]
509+
assertEquals("java", frame.platform)
510+
assertEquals("myMethod", frame.function)
511+
assertEquals("", frame.module)
512+
}
513+
514+
@Test
515+
fun `handles PrettyMethod with_signature bare function name`() {
516+
val event = parseTombstoneWithJavaFunctionName("void myMethod()")
517+
val frame = event.threads!![0].stacktrace!!.frames!![0]
518+
assertEquals("java", frame.platform)
519+
assertEquals("myMethod", frame.function)
520+
assertEquals("", frame.module)
521+
}
522+
523+
@Test
524+
fun `java frame with_signature format is correctly detected as inApp`() {
525+
val event =
526+
parseTombstoneWithJavaFunctionName("void io.sentry.samples.android.MyClass.myMethod(int)")
527+
val frame = event.threads!![0].stacktrace!!.frames!![0]
528+
assertEquals("java", frame.platform)
529+
assertEquals(true, frame.isInApp)
530+
}
531+
532+
@Test
533+
fun `java frame with_signature format is correctly detected as not inApp`() {
534+
val event =
535+
parseTombstoneWithJavaFunctionName(
536+
"void android.os.Handler.handleCallback(android.os.Message)"
537+
)
538+
val frame = event.threads!![0].stacktrace!!.frames!![0]
539+
assertEquals("java", frame.platform)
540+
assertEquals(false, frame.isInApp)
541+
}
542+
543+
private fun parseTombstoneWithJavaFunctionName(functionName: String): io.sentry.SentryEvent {
544+
val tombstone =
545+
TombstoneProtos.Tombstone.newBuilder()
546+
.setPid(1234)
547+
.setTid(1234)
548+
.setSignalInfo(
549+
TombstoneProtos.Signal.newBuilder()
550+
.setNumber(11)
551+
.setName("SIGSEGV")
552+
.setCode(1)
553+
.setCodeName("SEGV_MAPERR")
554+
)
555+
.putThreads(
556+
1234,
557+
TombstoneProtos.Thread.newBuilder()
558+
.setId(1234)
559+
.setName("main")
560+
.addCurrentBacktrace(
561+
TombstoneProtos.BacktraceFrame.newBuilder()
562+
.setPc(0x1000)
563+
.setFunctionName(functionName)
564+
.setFileName("/data/app/base.apk!classes.oat")
565+
)
566+
.build(),
567+
)
568+
.build()
569+
570+
val parser =
571+
TombstoneParser(
572+
ByteArrayInputStream(tombstone.toByteArray()),
573+
inAppIncludes,
574+
inAppExcludes,
575+
nativeLibraryDir,
576+
)
577+
return parser.parse()
578+
}
579+
467580
private fun serializeDebugMeta(debugMeta: DebugMeta): String {
468581
val logger = mock<ILogger>()
469582
val writer = StringWriter()

0 commit comments

Comments
 (0)