@@ -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 }
0 commit comments