Skip to content

Commit b1f1d23

Browse files
authored
Merge f7473a5 into 62b579c
2 parents 62b579c + f7473a5 commit b1f1d23

File tree

7 files changed

+337
-117
lines changed

7 files changed

+337
-117
lines changed

CHANGELOG.md

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

3+
## Unreleased
4+
5+
### Fixes
6+
7+
- Android: Identify and correctly structure Java/Kotlin frames in mixed Tombstone stack traces. ([#5116](https://github.com/getsentry/sentry-java/pull/5116))
8+
39
## 8.35.0
410

511
### Fixes

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

Lines changed: 83 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,20 @@ public class TombstoneParser implements Closeable {
4040
@Nullable private final String nativeLibraryDir;
4141
private final Map<String, String> excTypeValueMap = new HashMap<>();
4242

43+
private static boolean isJavaFrame(@NonNull final BacktraceFrame frame) {
44+
final String fileName = frame.fileName;
45+
return !fileName.endsWith(".so")
46+
&& !fileName.endsWith("app_process64")
47+
&& (fileName.endsWith(".jar")
48+
|| fileName.endsWith(".odex")
49+
|| fileName.endsWith(".vdex")
50+
|| fileName.endsWith(".oat")
51+
|| fileName.startsWith("[anon:dalvik-")
52+
|| fileName.startsWith("<anonymous:")
53+
|| fileName.startsWith("[anon_shmem:dalvik-")
54+
|| fileName.startsWith("/memfd:jit-cache"));
55+
}
56+
4357
private static String formatHex(long value) {
4458
return String.format("0x%x", value);
4559
}
@@ -125,7 +139,8 @@ private SentryStackTrace createStackTrace(@NonNull final TombstoneThread thread)
125139
final List<SentryStackFrame> frames = new ArrayList<>();
126140

127141
for (BacktraceFrame frame : thread.backtrace) {
128-
if (frame.fileName.endsWith("libart.so")) {
142+
if (frame.fileName.endsWith("libart.so")
143+
|| Objects.equals(frame.functionName, "art_jni_trampoline")) {
129144
// We ignore all ART frames for time being because they aren't actionable for app developers
130145
continue;
131146
}
@@ -135,27 +150,29 @@ private SentryStackTrace createStackTrace(@NonNull final TombstoneThread thread)
135150
continue;
136151
}
137152
final SentryStackFrame stackFrame = new SentryStackFrame();
138-
stackFrame.setPackage(frame.fileName);
139-
stackFrame.setFunction(frame.functionName);
140-
stackFrame.setInstructionAddr(formatHex(frame.pc));
141-
142-
// inAppIncludes/inAppExcludes filter by Java/Kotlin package names, which don't overlap
143-
// with native C/C++ function names (e.g., "crash", "__libc_init"). For native frames,
144-
// isInApp() returns null, making nativeLibraryDir the effective in-app check.
145-
// epitaph returns "" for unset function names, which would incorrectly return true
146-
// from isInApp(), so we treat empty as false to let nativeLibraryDir decide.
147-
final String functionName = frame.functionName;
148-
@Nullable
149-
Boolean inApp =
150-
functionName.isEmpty()
151-
? Boolean.FALSE
152-
: SentryStackTraceFactory.isInApp(functionName, inAppIncludes, inAppExcludes);
153-
154-
final boolean isInNativeLibraryDir =
155-
nativeLibraryDir != null && frame.fileName.startsWith(nativeLibraryDir);
156-
inApp = (inApp != null && inApp) || isInNativeLibraryDir;
157-
158-
stackFrame.setInApp(inApp);
153+
if (isJavaFrame(frame)) {
154+
stackFrame.setPlatform("java");
155+
final String module = extractJavaModuleName(frame.functionName);
156+
stackFrame.setFunction(extractJavaFunctionName(frame.functionName));
157+
stackFrame.setModule(module);
158+
159+
// For Java frames, check in-app against the module (package name), which is what
160+
// inAppIncludes/inAppExcludes are designed to match against.
161+
@Nullable
162+
Boolean inApp =
163+
(module == null || module.isEmpty())
164+
? Boolean.FALSE
165+
: SentryStackTraceFactory.isInApp(module, inAppIncludes, inAppExcludes);
166+
stackFrame.setInApp(inApp != null && inApp);
167+
} else {
168+
stackFrame.setPackage(frame.fileName);
169+
stackFrame.setFunction(frame.functionName);
170+
stackFrame.setInstructionAddr(formatHex(frame.pc));
171+
172+
final boolean isInNativeLibraryDir =
173+
nativeLibraryDir != null && frame.fileName.startsWith(nativeLibraryDir);
174+
stackFrame.setInApp(isInNativeLibraryDir);
175+
}
159176
frames.add(0, stackFrame);
160177
}
161178

@@ -176,6 +193,48 @@ private SentryStackTrace createStackTrace(@NonNull final TombstoneThread thread)
176193
return stacktrace;
177194
}
178195

196+
/**
197+
* Normalizes a PrettyMethod-formatted function name by stripping the return type prefix and
198+
* parameter list suffix that dex2oat may include when compiling AOT frames into the symtab.
199+
*
200+
* <p>e.g. "void com.example.MyClass.myMethod(int, java.lang.String)" ->
201+
* "com.example.MyClass.myMethod"
202+
*/
203+
private static String normalizeFunctionName(String fqFunctionName) {
204+
String normalized = fqFunctionName.trim();
205+
206+
// When dex2oat compiles AOT frames, PrettyMethod with_signature format may be used:
207+
// "void com.example.MyClass.myMethod(int, java.lang.String)"
208+
// A space is never part of a normal fully-qualified method name, so its presence
209+
// reliably indicates the with_signature format.
210+
final int spaceIndex = normalized.indexOf(' ');
211+
if (spaceIndex >= 0) {
212+
final int parenIndex = normalized.indexOf('(', spaceIndex);
213+
normalized =
214+
normalized.substring(spaceIndex + 1, parenIndex >= 0 ? parenIndex : normalized.length());
215+
}
216+
217+
return normalized;
218+
}
219+
220+
private static @Nullable String extractJavaModuleName(String fqFunctionName) {
221+
final String normalized = normalizeFunctionName(fqFunctionName);
222+
if (normalized.contains(".")) {
223+
return normalized.substring(0, normalized.lastIndexOf("."));
224+
} else {
225+
return null;
226+
}
227+
}
228+
229+
private static @Nullable String extractJavaFunctionName(String fqFunctionName) {
230+
final String normalized = normalizeFunctionName(fqFunctionName);
231+
if (normalized.contains(".")) {
232+
return normalized.substring(normalized.lastIndexOf(".") + 1);
233+
} else {
234+
return normalized;
235+
}
236+
}
237+
179238
@NonNull
180239
private List<SentryException> createException(@NonNull Tombstone tombstone) {
181240
final SentryException exception = new SentryException();
@@ -312,7 +371,7 @@ private DebugMeta createDebugMeta(@NonNull final Tombstone tombstone) {
312371
// Check for duplicated mappings: On Android, the same ELF can have multiple
313372
// mappings at offset 0 with different permissions (r--p, r-xp, r--p).
314373
// If it's the same file as the current module, just extend it.
315-
if (currentModule != null && mappingName.equals(currentModule.mappingName)) {
374+
if (currentModule != null && Objects.equals(mappingName, currentModule.mappingName)) {
316375
currentModule.extendTo(mapping.endAddress);
317376
continue;
318377
}
@@ -327,7 +386,7 @@ private DebugMeta createDebugMeta(@NonNull final Tombstone tombstone) {
327386

328387
// Start a new module
329388
currentModule = new ModuleAccumulator(mapping);
330-
} else if (currentModule != null && mappingName.equals(currentModule.mappingName)) {
389+
} else if (currentModule != null && Objects.equals(mappingName, currentModule.mappingName)) {
331390
// Extend the current module with this mapping (same file, continuation)
332391
currentModule.extendTo(mapping.endAddress);
333392
}

0 commit comments

Comments
 (0)