Skip to content

Commit 238420b

Browse files
authored
Merge 35e1010 into 382d6c1
2 parents 382d6c1 + 35e1010 commit 238420b

File tree

4 files changed

+241
-28
lines changed

4 files changed

+241
-28
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Fixes
66

77
- Fix crash when unregistering `SystemEventsBroadcastReceiver` with try-catch block. ([#5106](https://github.com/getsentry/sentry-java/pull/5106))
8+
- Identify and correctly structure Java/Kotlin frames in mixed Tombstone stack traces. ([#5116](https://github.com/getsentry/sentry-java/pull/5116))
89

910
## 8.33.0
1011

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

Lines changed: 88 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,20 @@ public class TombstoneParser implements Closeable {
3333
@Nullable private final String nativeLibraryDir;
3434
private final Map<String, String> excTypeValueMap = new HashMap<>();
3535

36+
private static boolean isJavaFrame(@NonNull final TombstoneProtos.BacktraceFrame frame) {
37+
final String fileName = frame.getFileName();
38+
return !fileName.endsWith(".so")
39+
&& !fileName.endsWith("app_process64")
40+
&& (fileName.endsWith(".jar")
41+
|| fileName.endsWith(".odex")
42+
|| fileName.endsWith(".vdex")
43+
|| fileName.endsWith(".oat")
44+
|| fileName.startsWith("[anon:dalvik-")
45+
|| fileName.startsWith("<anonymous:")
46+
|| fileName.startsWith("[anon_shmem:dalvik-")
47+
|| fileName.startsWith("/memfd:jit-cache"));
48+
}
49+
3650
private static String formatHex(long value) {
3751
return String.format("0x%x", value);
3852
}
@@ -108,7 +122,8 @@ private SentryStackTrace createStackTrace(@NonNull final TombstoneProtos.Thread
108122
final List<SentryStackFrame> frames = new ArrayList<>();
109123

110124
for (TombstoneProtos.BacktraceFrame frame : thread.getCurrentBacktraceList()) {
111-
if (frame.getFileName().endsWith("libart.so")) {
125+
if (frame.getFileName().endsWith("libart.so")
126+
|| Objects.equals(frame.getFunctionName(), "art_jni_trampoline")) {
112127
// We ignore all ART frames for time being because they aren't actionable for app developers
113128
continue;
114129
}
@@ -118,27 +133,29 @@ private SentryStackTrace createStackTrace(@NonNull final TombstoneProtos.Thread
118133
continue;
119134
}
120135
final SentryStackFrame stackFrame = new SentryStackFrame();
121-
stackFrame.setPackage(frame.getFileName());
122-
stackFrame.setFunction(frame.getFunctionName());
123-
stackFrame.setInstructionAddr(formatHex(frame.getPc()));
124-
125-
// inAppIncludes/inAppExcludes filter by Java/Kotlin package names, which don't overlap
126-
// with native C/C++ function names (e.g., "crash", "__libc_init"). For native frames,
127-
// isInApp() returns null, making nativeLibraryDir the effective in-app check.
128-
// Protobuf returns "" for unset function names, which would incorrectly return true
129-
// from isInApp(), so we treat empty as false to let nativeLibraryDir decide.
130-
final String functionName = frame.getFunctionName();
131-
@Nullable
132-
Boolean inApp =
133-
functionName.isEmpty()
134-
? Boolean.FALSE
135-
: SentryStackTraceFactory.isInApp(functionName, inAppIncludes, inAppExcludes);
136-
137-
final boolean isInNativeLibraryDir =
138-
nativeLibraryDir != null && frame.getFileName().startsWith(nativeLibraryDir);
139-
inApp = (inApp != null && inApp) || isInNativeLibraryDir;
140-
141-
stackFrame.setInApp(inApp);
136+
if (isJavaFrame(frame)) {
137+
stackFrame.setPlatform("java");
138+
final String module = extractJavaModuleName(frame.getFunctionName());
139+
stackFrame.setFunction(extractJavaFunctionName(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);
150+
} else {
151+
stackFrame.setPackage(frame.getFileName());
152+
stackFrame.setFunction(frame.getFunctionName());
153+
stackFrame.setInstructionAddr(formatHex(frame.getPc()));
154+
155+
final boolean isInNativeLibraryDir =
156+
nativeLibraryDir != null && frame.getFileName().startsWith(nativeLibraryDir);
157+
stackFrame.setInApp(isInNativeLibraryDir);
158+
}
142159
frames.add(0, stackFrame);
143160
}
144161

@@ -159,6 +176,53 @@ private SentryStackTrace createStackTrace(@NonNull final TombstoneProtos.Thread
159176
return stacktrace;
160177
}
161178

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+
208+
private static @Nullable String extractJavaModuleName(String fqFunctionName) {
209+
final String normalized = normalizeFunctionName(fqFunctionName);
210+
if (normalized.contains(".")) {
211+
return normalized.substring(0, normalized.lastIndexOf("."));
212+
} else {
213+
return "";
214+
}
215+
}
216+
217+
private static @Nullable String extractJavaFunctionName(String fqFunctionName) {
218+
final String normalized = normalizeFunctionName(fqFunctionName);
219+
if (normalized.contains(".")) {
220+
return normalized.substring(normalized.lastIndexOf(".") + 1);
221+
} else {
222+
return normalized;
223+
}
224+
}
225+
162226
@NonNull
163227
private List<SentryException> createException(@NonNull TombstoneProtos.Tombstone tombstone) {
164228
final SentryException exception = new SentryException();
@@ -296,7 +360,7 @@ private DebugMeta createDebugMeta(@NonNull final TombstoneProtos.Tombstone tombs
296360
// Check for duplicated mappings: On Android, the same ELF can have multiple
297361
// mappings at offset 0 with different permissions (r--p, r-xp, r--p).
298362
// If it's the same file as the current module, just extend it.
299-
if (currentModule != null && mappingName.equals(currentModule.mappingName)) {
363+
if (currentModule != null && Objects.equals(mappingName, currentModule.mappingName)) {
300364
currentModule.extendTo(mapping.getEndAddress());
301365
continue;
302366
}
@@ -311,7 +375,7 @@ private DebugMeta createDebugMeta(@NonNull final TombstoneProtos.Tombstone tombs
311375

312376
// Start a new module
313377
currentModule = new ModuleAccumulator(mapping);
314-
} else if (currentModule != null && mappingName.equals(currentModule.mappingName)) {
378+
} else if (currentModule != null && Objects.equals(mappingName, currentModule.mappingName)) {
315379
// Extend the current module with this mapping (same file, continuation)
316380
currentModule.extendTo(mapping.getEndAddress());
317381
}

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

Lines changed: 152 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,14 +101,20 @@ class TombstoneParserTest {
101101

102102
for (frame in thread.stacktrace!!.frames!!) {
103103
assertNotNull(frame.function)
104-
assertNotNull(frame.`package`)
105-
assertNotNull(frame.instructionAddr)
104+
if (frame.platform == "java") {
105+
// Java frames have module instead of package/instructionAddr
106+
assertNotNull(frame.module)
107+
} else {
108+
assertNotNull(frame.`package`)
109+
assertNotNull(frame.instructionAddr)
110+
}
106111

107112
if (thread.id == crashedThreadId) {
108113
if (frame.isInApp!!) {
109114
assert(
110-
frame.function!!.startsWith(inAppIncludes[0]) ||
111-
frame.`package`!!.startsWith(nativeLibraryDir)
115+
frame.module?.startsWith(inAppIncludes[0]) == true ||
116+
frame.function!!.startsWith(inAppIncludes[0]) ||
117+
frame.`package`?.startsWith(nativeLibraryDir) == true
112118
)
113119
}
114120
}
@@ -429,6 +435,148 @@ class TombstoneParserTest {
429435
}
430436
}
431437

438+
@Test
439+
fun `java frames snapshot test for all threads`() {
440+
val tombstoneStream =
441+
GZIPInputStream(TombstoneParserTest::class.java.getResourceAsStream("/tombstone.pb.gz"))
442+
val parser = TombstoneParser(tombstoneStream, inAppIncludes, inAppExcludes, nativeLibraryDir)
443+
val event = parser.parse()
444+
445+
val logger = mock<ILogger>()
446+
val writer = StringWriter()
447+
val jsonWriter = JsonObjectWriter(writer, 100)
448+
jsonWriter.beginObject()
449+
for (thread in event.threads!!) {
450+
val javaFrames = thread.stacktrace!!.frames!!.filter { it.platform == "java" }
451+
if (javaFrames.isEmpty()) continue
452+
jsonWriter.name(thread.id.toString())
453+
jsonWriter.beginArray()
454+
for (frame in javaFrames) {
455+
frame.serialize(jsonWriter, logger)
456+
}
457+
jsonWriter.endArray()
458+
}
459+
jsonWriter.endObject()
460+
461+
val actualJson = writer.toString()
462+
val expectedJson = readGzippedResourceFile("/tombstone_java_frames.json.gz")
463+
464+
assertEquals(expectedJson, actualJson)
465+
}
466+
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+
432580
private fun serializeDebugMeta(debugMeta: DebugMeta): String {
433581
val logger = mock<ILogger>()
434582
val writer = StringWriter()
Binary file not shown.

0 commit comments

Comments
 (0)